mirror of
https://github.com/ankitects/anki.git
synced 2025-09-20 06:52:21 -04:00
daily learning queue
The way we were handling 1 day+ learning intervals was not great - they'd show up at the start of a new day before normal reviews, meaning the hardest cards came first. In previous Anki versions we deliberately sorted the queue in the opposite order to prevent that. When relearning the cards the next day, if you failed a card and expected to see it in 10 minutes that wouldn't happen either, as all the overdue cards took precedence. To fix this, we put cards that are due tomorrow or later into a separate queue (queue 3), and pull cards from that queue only after the reviews are done. In the future it might also be nice to move overdue learning cards into that queue automatically at the start of a session.
This commit is contained in:
parent
32fde2a072
commit
b6bdd4aa21
3 changed files with 112 additions and 16 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue