diff --git a/anki/collection.py b/anki/collection.py index 309a8f1b5..700d8b05d 100644 --- a/anki/collection.py +++ b/anki/collection.py @@ -585,7 +585,8 @@ where c.nid == f.id self.db.execute("delete from revlog where id = ?", last) # and finally, update daily counts # fixme: what to do in cramming case? - type = ("new", "lrn", "rev")[c.queue] + n = 1 if c.queue == 3 else c.queue + type = ("new", "lrn", "rev")[n] self.sched._updateStats(c, type, -1) return c.id diff --git a/anki/sched.py b/anki/sched.py index 0803bf112..060d392a4 100644 --- a/anki/sched.py +++ b/anki/sched.py @@ -11,12 +11,9 @@ from anki.lang import _, ngettext from anki.consts import * from anki.hooks import runHook +# queue types: 0=new/cram, 1=lrn, 2=rev, 3=day lrn, -1=suspended, -2=buried # revlog types: 0=lrn, 1=rev, 2=relrn, 3=cram -# queue types: 0=new/cram, 1=lrn, 2=rev, -1=suspended, -2=buried -# positive intervals are in days (rev), negative intervals in seconds (lrn) - -# fixme: -# - should log cram reps as cramming +# positive revlog intervals are in days (rev), negative in seconds (lrn) class Scheduler(object): name = "std" @@ -69,7 +66,7 @@ class Scheduler(object): card.ivl = self._dynIvlBoost(card) card.odue = self.today + card.ivl self._updateStats(card, 'new') - if card.queue == 1: + if card.queue in (1, 3): self._answerLrnCard(card, ease) if not wasNew: self._updateStats(card, 'lrn') @@ -272,6 +269,10 @@ order by due""" % self._deckLimit(), return self._getNewCard() # card due for review? c = self._getRevCard() + if c: + return c + # day learning card due? + c = self._getLrnDayCard() if c: return c # new cards left? @@ -383,20 +384,29 @@ select count() from c = self.col.decks.confForDid(g['id']) return max(0, c['new']['perDay'] - g['newToday'][1]) - # Learning queue + # Learning queues ########################################################################## def _resetLrnCount(self): + # sub-day self.lrnCount = self.col.db.scalar(""" select sum(left/1000) from (select left from cards where did in %s and queue = 1 and due < ? limit %d)""" % ( self._deckLimit(), self.reportLimit), self.dayCutoff) or 0 + # day + self.lrnCount += self.col.db.scalar(""" +select count() from cards where did in %s and queue = 3 +and due <= ? limit %d""" % (self._deckLimit(), self.reportLimit), + self.today) def _resetLrn(self): self._resetLrnCount() self._lrnQueue = [] + self._lrnDayQueue = [] + self._lrnDids = self.col.decks.active()[:] + # sub-day learning def _fillLrn(self): if not self.lrnCount: return False @@ -421,6 +431,36 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff) self.lrnCount -= card.left/1000 return card + # daily learning + def _fillLrnDay(self): + if not self.lrnCount: + return False + if self._lrnDayQueue: + return True + while self._lrnDids: + did = self._lrnDids[0] + # fill the queue with the current did + self._lrnDayQueue = self.col.db.list(""" +select id from cards where +did = ? and queue = 3 and due <= ? limit ?""", + did, self.today, self.queueLimit) + if self._lrnDayQueue: + # order + r = random.Random() + r.seed(self.today) + r.shuffle(self._lrnDayQueue) + # is the current did empty? + if len(self._lrnDayQueue) < self.queueLimit: + self._lrnDids.pop(0) + return True + # nothing left in the deck; move to next + self._lrnDids.pop(0) + + def _getLrnDayCard(self): + if self._fillLrnDay(): + self.lrnCount -= 1 + return self.col.getCard(self._lrnDayQueue.pop()) + def _answerLrnCard(self, card, ease): # ease 1=no, 2=yes, 3=remove conf = self._lrnConf(card) @@ -463,15 +503,23 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff) # not collapsed; add some randomness delay *= random.uniform(1, 1.25) card.due = int(time.time() + delay) + # due today? if card.due < self.dayCutoff: self.lrnCount += card.left/1000 - # if the queue is not empty and there's nothing else to do, make - # sure we don't put it at the head of the queue and end up showing - # it twice in a row - if self._lrnQueue and not self.revCount and not self.newCount: - smallestDue = self._lrnQueue[0][0] - card.due = max(card.due, smallestDue+1) - heappush(self._lrnQueue, (card.due, card.id)) + # if the queue is not empty and there's nothing else to do, make + # sure we don't put it at the head of the queue and end up showing + # it twice in a row + card.queue = 1 + if self._lrnQueue and not self.revCount and not self.newCount: + smallestDue = self._lrnQueue[0][0] + card.due = max(card.due, smallestDue+1) + heappush(self._lrnQueue, (card.due, card.id)) + else: + # the card is due in one or more days, so we need to use the + # day learn queue + ahead = ((card.due - self.dayCutoff) / 86400) + 1 + card.due = self.today + ahead + card.queue = 3 self._logLrn(card, ease, conf, leaving, type, lastLeft) def _delayForGrade(self, conf, left): @@ -1019,7 +1067,7 @@ your short-term review workload will become.""")) def nextIvl(self, card, ease): "Return the next interval for CARD, in seconds." - if card.queue in (0,1): + if card.queue in (0,1,3): return self._nextLrnIvl(card, ease) elif ease == 1: # lapsed diff --git a/tests/test_sched.py b/tests/test_sched.py index a563ad275..c0e848a03 100644 --- a/tests/test_sched.py +++ b/tests/test_sched.py @@ -197,6 +197,53 @@ def test_learn_collapsed(): c = d.sched.getCard() assert not c.q().endswith("2") +def test_learn_day(): + d = getEmptyDeck() + # add a note + f = d.newNote() + f['Front'] = u"one" + f = d.addNote(f) + d.sched.reset() + c = d.sched.getCard() + d.sched._cardConf(c)['new']['delays'] = [1, 10, 1440, 2880] + # pass it + d.sched.answerCard(c, 2) + # two reps to graduate, 1 more today + assert c.left%1000 == 3 + assert c.left/1000 == 1 + assert d.sched.counts() == (0, 1, 0) + c = d.sched.getCard() + ni = d.sched.nextIvl + assert ni(c, 2) == 86400 + # answering it will place it in queue 3 + d.sched.answerCard(c, 2) + assert c.due == d.sched.today+1 + assert c.queue == 3 + assert not d.sched.getCard() + # for testing, move it back a day + c.due -= 1 + c.flush() + d.reset() + assert d.sched.counts() == (0, 1, 0) + c = d.sched.getCard() + # nextIvl should work + assert ni(c, 2) == 86400*2 + # if we fail it, it should be back in the correct queue + d.sched.answerCard(c, 1) + assert c.queue == 1 + d.undo() + d.reset() + c = d.sched.getCard() + d.sched.answerCard(c, 2) + # simulate the passing of another two days + c.due -= 2 + c.flush() + d.reset() + # the last pass should graduate it into a review card + assert ni(c, 2) == 86400 + d.sched.answerCard(c, 2) + assert c.queue == c.type == 2 + def test_reviews(): d = getEmptyDeck() # add a note