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)
|
self.db.execute("delete from revlog where id = ?", last)
|
||||||
# and finally, update daily counts
|
# and finally, update daily counts
|
||||||
# fixme: what to do in cramming case?
|
# 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)
|
self.sched._updateStats(c, type, -1)
|
||||||
return c.id
|
return c.id
|
||||||
|
|
||||||
|
|
|
@ -11,12 +11,9 @@ from anki.lang import _, ngettext
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
from anki.hooks import runHook
|
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
|
# revlog types: 0=lrn, 1=rev, 2=relrn, 3=cram
|
||||||
# queue types: 0=new/cram, 1=lrn, 2=rev, -1=suspended, -2=buried
|
# positive revlog intervals are in days (rev), negative in seconds (lrn)
|
||||||
# positive intervals are in days (rev), negative intervals in seconds (lrn)
|
|
||||||
|
|
||||||
# fixme:
|
|
||||||
# - should log cram reps as cramming
|
|
||||||
|
|
||||||
class Scheduler(object):
|
class Scheduler(object):
|
||||||
name = "std"
|
name = "std"
|
||||||
|
@ -69,7 +66,7 @@ class Scheduler(object):
|
||||||
card.ivl = self._dynIvlBoost(card)
|
card.ivl = self._dynIvlBoost(card)
|
||||||
card.odue = self.today + card.ivl
|
card.odue = self.today + card.ivl
|
||||||
self._updateStats(card, 'new')
|
self._updateStats(card, 'new')
|
||||||
if card.queue == 1:
|
if card.queue in (1, 3):
|
||||||
self._answerLrnCard(card, ease)
|
self._answerLrnCard(card, ease)
|
||||||
if not wasNew:
|
if not wasNew:
|
||||||
self._updateStats(card, 'lrn')
|
self._updateStats(card, 'lrn')
|
||||||
|
@ -272,6 +269,10 @@ order by due""" % self._deckLimit(),
|
||||||
return self._getNewCard()
|
return self._getNewCard()
|
||||||
# card due for review?
|
# card due for review?
|
||||||
c = self._getRevCard()
|
c = self._getRevCard()
|
||||||
|
if c:
|
||||||
|
return c
|
||||||
|
# day learning card due?
|
||||||
|
c = self._getLrnDayCard()
|
||||||
if c:
|
if c:
|
||||||
return c
|
return c
|
||||||
# new cards left?
|
# new cards left?
|
||||||
|
@ -383,20 +384,29 @@ select count() from
|
||||||
c = self.col.decks.confForDid(g['id'])
|
c = self.col.decks.confForDid(g['id'])
|
||||||
return max(0, c['new']['perDay'] - g['newToday'][1])
|
return max(0, c['new']['perDay'] - g['newToday'][1])
|
||||||
|
|
||||||
# Learning queue
|
# Learning queues
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def _resetLrnCount(self):
|
def _resetLrnCount(self):
|
||||||
|
# sub-day
|
||||||
self.lrnCount = self.col.db.scalar("""
|
self.lrnCount = self.col.db.scalar("""
|
||||||
select sum(left/1000) from (select left from cards where
|
select sum(left/1000) from (select left from cards where
|
||||||
did in %s and queue = 1 and due < ? limit %d)""" % (
|
did in %s and queue = 1 and due < ? limit %d)""" % (
|
||||||
self._deckLimit(), self.reportLimit),
|
self._deckLimit(), self.reportLimit),
|
||||||
self.dayCutoff) or 0
|
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):
|
def _resetLrn(self):
|
||||||
self._resetLrnCount()
|
self._resetLrnCount()
|
||||||
self._lrnQueue = []
|
self._lrnQueue = []
|
||||||
|
self._lrnDayQueue = []
|
||||||
|
self._lrnDids = self.col.decks.active()[:]
|
||||||
|
|
||||||
|
# sub-day learning
|
||||||
def _fillLrn(self):
|
def _fillLrn(self):
|
||||||
if not self.lrnCount:
|
if not self.lrnCount:
|
||||||
return False
|
return False
|
||||||
|
@ -421,6 +431,36 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff)
|
||||||
self.lrnCount -= card.left/1000
|
self.lrnCount -= card.left/1000
|
||||||
return card
|
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):
|
def _answerLrnCard(self, card, ease):
|
||||||
# ease 1=no, 2=yes, 3=remove
|
# ease 1=no, 2=yes, 3=remove
|
||||||
conf = self._lrnConf(card)
|
conf = self._lrnConf(card)
|
||||||
|
@ -463,15 +503,23 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff)
|
||||||
# not collapsed; add some randomness
|
# not collapsed; add some randomness
|
||||||
delay *= random.uniform(1, 1.25)
|
delay *= random.uniform(1, 1.25)
|
||||||
card.due = int(time.time() + delay)
|
card.due = int(time.time() + delay)
|
||||||
|
# due today?
|
||||||
if card.due < self.dayCutoff:
|
if card.due < self.dayCutoff:
|
||||||
self.lrnCount += card.left/1000
|
self.lrnCount += card.left/1000
|
||||||
# if the queue is not empty and there's nothing else to do, make
|
# 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
|
# sure we don't put it at the head of the queue and end up showing
|
||||||
# it twice in a row
|
# it twice in a row
|
||||||
if self._lrnQueue and not self.revCount and not self.newCount:
|
card.queue = 1
|
||||||
smallestDue = self._lrnQueue[0][0]
|
if self._lrnQueue and not self.revCount and not self.newCount:
|
||||||
card.due = max(card.due, smallestDue+1)
|
smallestDue = self._lrnQueue[0][0]
|
||||||
heappush(self._lrnQueue, (card.due, card.id))
|
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)
|
self._logLrn(card, ease, conf, leaving, type, lastLeft)
|
||||||
|
|
||||||
def _delayForGrade(self, conf, left):
|
def _delayForGrade(self, conf, left):
|
||||||
|
@ -1019,7 +1067,7 @@ your short-term review workload will become."""))
|
||||||
|
|
||||||
def nextIvl(self, card, ease):
|
def nextIvl(self, card, ease):
|
||||||
"Return the next interval for CARD, in seconds."
|
"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)
|
return self._nextLrnIvl(card, ease)
|
||||||
elif ease == 1:
|
elif ease == 1:
|
||||||
# lapsed
|
# lapsed
|
||||||
|
|
|
@ -197,6 +197,53 @@ def test_learn_collapsed():
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
assert not c.q().endswith("2")
|
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():
|
def test_reviews():
|
||||||
d = getEmptyDeck()
|
d = getEmptyDeck()
|
||||||
# add a note
|
# add a note
|
||||||
|
|
Loading…
Reference in a new issue