minor refactor of count handling

- drop lrnCount; rename lrnRepCount to lrnCount
- on card fetch, decr count by card.left
- drop cardCounts(), rename repCounts() to just counts()
- fix lrn count bugs
This commit is contained in:
Damien Elmes 2011-12-05 19:15:46 +09:00
parent e7101faff7
commit 8308c79fa6
5 changed files with 77 additions and 110 deletions

View file

@ -29,11 +29,10 @@ class Scheduler(object):
def getCard(self): def getCard(self):
"Pop the next card from the queue. None if finished." "Pop the next card from the queue. None if finished."
self._checkDay() self._checkDay()
id = self._getCardId() card = self._getCard()
if id: if card:
c = self.col.getCard(id) card.startTimer()
c.startTimer() return card
return c
def reset(self): def reset(self):
self._updateCutoff() self._updateCutoff()
@ -52,7 +51,6 @@ class Scheduler(object):
card.queue = 1 card.queue = 1
card.type = 1 card.type = 1
card.left = self._startingLeft(card) card.left = self._startingLeft(card)
self.lrnRepCount += card.left
self._updateStats(card, 'new') self._updateStats(card, 'new')
if card.queue == 1: if card.queue == 1:
self._answerLrnCard(card, ease) self._answerLrnCard(card, ease)
@ -68,10 +66,7 @@ class Scheduler(object):
card.usn = self.col.usn() card.usn = self.col.usn()
card.flushSched() card.flushSched()
def repCounts(self): def counts(self):
return (self.newCount, self.lrnRepCount, self.revCount)
def cardCounts(self):
return (self.newCount, self.lrnCount, self.revCount) return (self.newCount, self.lrnCount, self.revCount)
def dueForecast(self, days=7): def dueForecast(self, days=7):
@ -175,23 +170,23 @@ order by due""" % self._deckLimit(),
# Getting the next card # Getting the next card
########################################################################## ##########################################################################
def _getCardId(self): def _getCard(self):
"Return the next due card id, or None." "Return the next due card id, or None."
# learning card due? # learning card due?
id = self._getLrnCard() c = self._getLrnCard()
if id: if c:
return id return c
# new first, or time for one? # new first, or time for one?
if self._timeForNewCard(): if self._timeForNewCard():
return self._getNewCard() return self._getNewCard()
# card due for review? # card due for review?
id = self._getRevCard() c = self._getRevCard()
if id: if c:
return id return c
# new cards left? # new cards left?
id = self._getNewCard() c = self._getNewCard()
if id: if c:
return id return c
# collapse or finish # collapse or finish
return self._getLrnCard(collapse=True) return self._getLrnCard(collapse=True)
@ -266,7 +261,7 @@ select id, due from cards where did = ? and queue = 0 limit ?""", did, lim)
# we only have one note in the queue; stop rotating # we only have one note in the queue; stop rotating
break break
self.newCount -= 1 self.newCount -= 1
return id return self.col.getCard(id)
def _updateNewCardRatio(self): def _updateNewCardRatio(self):
if self.col.decks.top()['newSpread'] == NEW_CARDS_DISTRIBUTE: if self.col.decks.top()['newSpread'] == NEW_CARDS_DISTRIBUTE:
@ -316,12 +311,11 @@ select id, due from cards where did = ? and queue = 0 limit ?""", did, lim)
########################################################################## ##########################################################################
def _resetLrnCount(self): def _resetLrnCount(self):
(self.lrnCount, self.lrnRepCount) = self.col.db.first(""" self.lrnCount = self.col.db.scalar("""
select count(), sum(left) from (select left from cards where select sum(left) 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) self.dayCutoff) or 0
self.lrnRepCount = self.lrnRepCount or 0
def _resetLrn(self): def _resetLrn(self):
self._resetLrnCount() self._resetLrnCount()
@ -347,9 +341,9 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff)
cutoff += self.col.decks.top()['collapseTime'] cutoff += self.col.decks.top()['collapseTime']
if self._lrnQueue[0][0] < cutoff: if self._lrnQueue[0][0] < cutoff:
id = heappop(self._lrnQueue)[1] id = heappop(self._lrnQueue)[1]
self.lrnCount -= 1 card = self.col.getCard(id)
self.lrnRepCount -= 1 self.lrnCount -= card.left
return id return card
def _answerLrnCard(self, card, ease): def _answerLrnCard(self, card, ease):
# ease 1=no, 2=yes, 3=remove # ease 1=no, 2=yes, 3=remove
@ -359,29 +353,30 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff)
else: else:
type = 0 type = 0
leaving = False leaving = False
# lrnCount was decremented once when card was fetched
lastLeft = card.left lastLeft = card.left
# immediate graduate?
if ease == 3: if ease == 3:
self._rescheduleAsRev(card, conf, True) self._rescheduleAsRev(card, conf, True)
self.lrnRepCount -= lastLeft
leaving = True leaving = True
# graduation time?
elif ease == 2 and card.left-1 <= 0: elif ease == 2 and card.left-1 <= 0:
self._rescheduleAsRev(card, conf, False) self._rescheduleAsRev(card, conf, False)
self.lrnRepCount -= 1
leaving = True leaving = True
else: else:
# one step towards graduation
if ease == 2: if ease == 2:
card.left -= 1 card.left -= 1
self.lrnRepCount -= 1 # failed
else: else:
card.left = self._startingLeft(card) card.left = self._startingLeft(card)
self.lrnRepCount += card.left - lastLeft self.lrnCount += card.left
delay = self._delayForGrade(conf, card.left) delay = self._delayForGrade(conf, card.left)
if card.due < time.time(): if card.due < time.time():
# 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)
heappush(self._lrnQueue, (card.due, card.id)) heappush(self._lrnQueue, (card.due, card.id))
self.lrnCount += 1
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):
@ -501,7 +496,7 @@ did in %s and queue = 2 and due <= :lim %s limit %d""" % (
def _getRevCard(self): def _getRevCard(self):
if self._fillRev(): if self._fillRev():
self.revCount -= 1 self.revCount -= 1
return self._revQueue.pop() return self.col.getCard(self._revQueue.pop())
def _revOrder(self): def _revOrder(self):
if self.col.conf['revOrder']: if self.col.conf['revOrder']:
@ -531,8 +526,7 @@ did in %s and queue = 2 and due <= :lim %s limit %d""" % (
card.due = int(self._delayForGrade(conf, 0) + time.time()) card.due = int(self._delayForGrade(conf, 0) + time.time())
card.left = len(conf['delays']) card.left = len(conf['delays'])
card.queue = 1 card.queue = 1
self.lrnCount += 1 self.lrnCount += card.left
self.lrnRepCount += card.left
# leech? # leech?
if not self._checkLeech(card, conf) and conf['relearn']: if not self._checkLeech(card, conf) and conf['relearn']:
heappush(self._lrnQueue, (card.due, card.id)) heappush(self._lrnQueue, (card.due, card.id))

View file

@ -140,7 +140,7 @@ select count() from notes where id not in (select distinct nid from cards)""")
assert m['usn'] != -1 assert m['usn'] != -1
self.col.sched.reset() self.col.sched.reset()
return [ return [
list(self.col.sched.repCounts()), list(self.col.sched.counts()),
self.col.db.scalar("select count() from cards"), self.col.db.scalar("select count() from cards"),
self.col.db.scalar("select count() from notes"), self.col.db.scalar("select count() from notes"),
self.col.db.scalar("select count() from revlog"), self.col.db.scalar("select count() from revlog"),

View file

@ -426,10 +426,10 @@ def test_cram():
conf = d.sched._lrnConf(c) conf = d.sched._lrnConf(c)
conf['reset'] = False conf['reset'] = False
conf['resched'] = False conf['resched'] = False
assert d.sched.cardCounts() == (1, 0, 0) assert d.sched.counts() == (1, 0, 0)
c = d.sched.getCard() c = d.sched.getCard()
d.sched._cardConf(c)['cram']['delays'] = [0.5, 3, 10] d.sched._cardConf(c)['cram']['delays'] = [0.5, 3, 10]
assert d.sched.cardCounts() == (0, 0, 0) assert d.sched.counts() == (0, 0, 0)
# check that estimates work # check that estimates work
assert d.sched.nextIvl(c, 1) == 30 assert d.sched.nextIvl(c, 1) == 30
assert d.sched.nextIvl(c, 2) == 180 assert d.sched.nextIvl(c, 2) == 180
@ -439,7 +439,7 @@ def test_cram():
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert c.ivl == 100 assert c.ivl == 100
# and should have incremented lrn count # and should have incremented lrn count
assert d.sched.cardCounts()[1] == 1 assert d.sched.counts()[1] == 1
# reset ivl for exit test, and pass card # reset ivl for exit test, and pass card
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
delta = c.due - time.time() delta = c.due - time.time()
@ -453,7 +453,7 @@ def test_cram():
assert c.due == d.sched.today + c.ivl assert c.due == d.sched.today + c.ivl
# and if the queue is reset, it shouldn't appear in the new queue again # and if the queue is reset, it shouldn't appear in the new queue again
d.reset() d.reset()
assert d.sched.cardCounts() == (0, 0, 0) assert d.sched.counts() == (0, 0, 0)
# now try again with ivl rescheduling # now try again with ivl rescheduling
c = copy.copy(cardcopy) c = copy.copy(cardcopy)
c.flush() c.flush()
@ -484,7 +484,7 @@ def test_cram():
# users should be able to cram entire deck too # users should be able to cram entire deck too
d.conf['decks'] = [] d.conf['decks'] = []
d.cramDecks() d.cramDecks()
assert d.sched.cardCounts()[0] > 0 assert d.sched.counts()[0] > 0
def test_cramLimits(): def test_cramLimits():
d = getEmptyDeck() d = getEmptyDeck()
@ -500,29 +500,29 @@ def test_cramLimits():
# the default cram should return all three # the default cram should return all three
d.conf['decks'] = [1] d.conf['decks'] = [1]
d.cramDecks() d.cramDecks()
assert d.sched.cardCounts()[0] == 3 assert d.sched.counts()[0] == 3
# if we start from the day after tomorrow, it should be 2 # if we start from the day after tomorrow, it should be 2
d.cramDecks(min=1) d.cramDecks(min=1)
assert d.sched.cardCounts()[0] == 2 assert d.sched.counts()[0] == 2
# or after 2 days # or after 2 days
d.cramDecks(min=2) d.cramDecks(min=2)
assert d.sched.cardCounts()[0] == 1 assert d.sched.counts()[0] == 1
# we may get nothing # we may get nothing
d.cramDecks(min=3) d.cramDecks(min=3)
assert d.sched.cardCounts()[0] == 0 assert d.sched.counts()[0] == 0
# tomorrow(0) + dayAfter(1) = 2 # tomorrow(0) + dayAfter(1) = 2
d.cramDecks(max=1) d.cramDecks(max=1)
assert d.sched.cardCounts()[0] == 2 assert d.sched.counts()[0] == 2
# if max is tomorrow, we get only one # if max is tomorrow, we get only one
d.cramDecks(max=0) d.cramDecks(max=0)
assert d.sched.cardCounts()[0] == 1 assert d.sched.counts()[0] == 1
# both should work # both should work
d.cramDecks(min=0, max=0) d.cramDecks(min=0, max=0)
assert d.sched.cardCounts()[0] == 1 assert d.sched.counts()[0] == 1
d.cramDecks(min=1, max=1) d.cramDecks(min=1, max=1)
assert d.sched.cardCounts()[0] == 1 assert d.sched.counts()[0] == 1
d.cramDecks(min=0, max=1) d.cramDecks(min=0, max=1)
assert d.sched.cardCounts()[0] == 2 assert d.sched.counts()[0] == 2
def test_adjIvl(): def test_adjIvl():
d = getEmptyDeck() d = getEmptyDeck()
@ -617,54 +617,27 @@ def test_ordcycle():
assert d.sched.getCard().ord == 1 assert d.sched.getCard().ord == 1
assert d.sched.getCard().ord == 2 assert d.sched.getCard().ord == 2
def test_cardcounts():
d = getEmptyDeck()
# add a second deck
grp = d.decks.id("Default::new deck")
# for each card type
for type in range(3):
# and each of the decks
for did in (1,grp):
# create a new note
f = d.newNote()
f['Front'] = u"one"
d.addNote(f)
c = f.cards()[0]
# set type/did
c.type = type
c.queue = type
c.did = did
c.due = 0
c.flush()
d.reset()
# with the default settings, there's no count limit
assert d.sched.cardCounts() == (2,2,2)
# check limit to one deck
d.decks.select(grp)
d.reset()
assert d.sched.cardCounts() == (1,1,1)
def test_counts_idx(): def test_counts_idx():
d = getEmptyDeck() d = getEmptyDeck()
f = d.newNote() f = d.newNote()
f['Front'] = u"one"; f['Back'] = u"two" f['Front'] = u"one"; f['Back'] = u"two"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
assert d.sched.cardCounts() == (1, 0, 0) assert d.sched.counts() == (1, 0, 0)
c = d.sched.getCard() c = d.sched.getCard()
# counter's been decremented but idx indicates 1 # counter's been decremented but idx indicates 1
assert d.sched.cardCounts() == (0, 0, 0) assert d.sched.counts() == (0, 0, 0)
assert d.sched.countIdx(c) == 0 assert d.sched.countIdx(c) == 0
# answer to move to learn queue # answer to move to learn queue
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert d.sched.cardCounts() == (0, 1, 0) assert d.sched.counts() == (0, 2, 0)
# fetching again will decrement the count # fetching again will decrement the count
c = d.sched.getCard() c = d.sched.getCard()
assert d.sched.cardCounts() == (0, 0, 0) assert d.sched.counts() == (0, 0, 0)
assert d.sched.countIdx(c) == 1 assert d.sched.countIdx(c) == 1
# answering should add it back again # answering should add it back again
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert d.sched.cardCounts() == (0, 1, 0) assert d.sched.counts() == (0, 2, 0)
def test_repCounts(): def test_repCounts():
d = getEmptyDeck() d = getEmptyDeck()
@ -673,37 +646,37 @@ def test_repCounts():
d.addNote(f) d.addNote(f)
d.reset() d.reset()
# lrnReps should be accurate on pass/fail # lrnReps should be accurate on pass/fail
assert d.sched.repCounts() == (1, 0, 0) assert d.sched.counts() == (1, 0, 0)
d.sched.answerCard(d.sched.getCard(), 1) d.sched.answerCard(d.sched.getCard(), 1)
assert d.sched.repCounts() == (0, 2, 0) assert d.sched.counts() == (0, 2, 0)
d.sched.answerCard(d.sched.getCard(), 1) d.sched.answerCard(d.sched.getCard(), 1)
assert d.sched.repCounts() == (0, 2, 0) assert d.sched.counts() == (0, 2, 0)
d.sched.answerCard(d.sched.getCard(), 2) d.sched.answerCard(d.sched.getCard(), 2)
assert d.sched.repCounts() == (0, 1, 0) assert d.sched.counts() == (0, 1, 0)
d.sched.answerCard(d.sched.getCard(), 1) d.sched.answerCard(d.sched.getCard(), 1)
assert d.sched.repCounts() == (0, 2, 0) assert d.sched.counts() == (0, 2, 0)
d.sched.answerCard(d.sched.getCard(), 2) d.sched.answerCard(d.sched.getCard(), 2)
assert d.sched.repCounts() == (0, 1, 0) assert d.sched.counts() == (0, 1, 0)
d.sched.answerCard(d.sched.getCard(), 2) d.sched.answerCard(d.sched.getCard(), 2)
assert d.sched.repCounts() == (0, 0, 0) assert d.sched.counts() == (0, 0, 0)
f = d.newNote() f = d.newNote()
f['Front'] = u"two" f['Front'] = u"two"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
# initial pass should be correct too # initial pass should be correct too
d.sched.answerCard(d.sched.getCard(), 2) d.sched.answerCard(d.sched.getCard(), 2)
assert d.sched.repCounts() == (0, 1, 0) assert d.sched.counts() == (0, 1, 0)
d.sched.answerCard(d.sched.getCard(), 1) d.sched.answerCard(d.sched.getCard(), 1)
assert d.sched.repCounts() == (0, 2, 0) assert d.sched.counts() == (0, 2, 0)
d.sched.answerCard(d.sched.getCard(), 3) d.sched.answerCard(d.sched.getCard(), 3)
assert d.sched.repCounts() == (0, 0, 0) assert d.sched.counts() == (0, 0, 0)
# immediate graduate should work # immediate graduate should work
f = d.newNote() f = d.newNote()
f['Front'] = u"three" f['Front'] = u"three"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
d.sched.answerCard(d.sched.getCard(), 3) d.sched.answerCard(d.sched.getCard(), 3)
assert d.sched.repCounts() == (0, 0, 0) assert d.sched.counts() == (0, 0, 0)
# and failing a review should too # and failing a review should too
f = d.newNote() f = d.newNote()
f['Front'] = u"three" f['Front'] = u"three"
@ -714,9 +687,9 @@ def test_repCounts():
c.due = d.sched.today c.due = d.sched.today
c.flush() c.flush()
d.reset() d.reset()
assert d.sched.repCounts() == (0, 0, 1) assert d.sched.counts() == (0, 0, 1)
d.sched.answerCard(d.sched.getCard(), 1) d.sched.answerCard(d.sched.getCard(), 1)
assert d.sched.repCounts() == (0, 2, 0) assert d.sched.counts() == (0, 2, 0)
def test_timing(): def test_timing():
d = getEmptyDeck() d = getEmptyDeck()
@ -836,7 +809,7 @@ def test_deckFlow():
d.addNote(f) d.addNote(f)
# should get top level one first, then ::1, then ::2 # should get top level one first, then ::1, then ::2
d.reset() d.reset()
assert d.sched.cardCounts() == (3,0,0) assert d.sched.counts() == (3,0,0)
for i in "one", "three", "two": for i in "one", "three", "two":
c = d.sched.getCard() c = d.sched.getCard()
assert c.note()['Front'] == i assert c.note()['Front'] == i
@ -889,10 +862,10 @@ def test_forget():
c.queue = 2; c.type = 2; c.ivl = 100; c.due = 0 c.queue = 2; c.type = 2; c.ivl = 100; c.due = 0
c.flush() c.flush()
d.reset() d.reset()
assert d.sched.cardCounts() == (0, 0, 1) assert d.sched.counts() == (0, 0, 1)
d.sched.forgetCards([c.id]) d.sched.forgetCards([c.id])
d.reset() d.reset()
assert d.sched.cardCounts() == (1, 0, 0) assert d.sched.counts() == (1, 0, 0)
def test_resched(): def test_resched():
d = getEmptyDeck() d = getEmptyDeck()
@ -918,12 +891,12 @@ def test_revlim():
d.addNote(f) d.addNote(f)
d.db.execute("update cards set due = 0, queue = 2, type = 2") d.db.execute("update cards set due = 0, queue = 2, type = 2")
d.reset() d.reset()
assert d.sched.repCounts()[2] == 20 assert d.sched.counts()[2] == 20
for i in range(5): for i in range(5):
d.sched.answerCard(d.sched.getCard(), 3) d.sched.answerCard(d.sched.getCard(), 3)
assert d.sched.repCounts()[2] == 15 assert d.sched.counts()[2] == 15
t = d.decks.top() t = d.decks.top()
t['revLim'] = 10 t['revLim'] = 10
d.reset() d.reset()
assert d.sched.repCounts()[2] == 5 assert d.sched.counts()[2] == 5

View file

@ -44,18 +44,18 @@ def test_review():
d.reset() d.reset()
assert not d.undoName() assert not d.undoName()
# answer # answer
assert d.sched.cardCounts() == (1, 0, 0) assert d.sched.counts() == (1, 0, 0)
c = d.sched.getCard() c = d.sched.getCard()
assert c.queue == 0 assert c.queue == 0
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
assert c.left == 1 assert c.left == 1
assert d.sched.cardCounts() == (0, 1, 0) assert d.sched.counts() == (0, 1, 0)
assert c.queue == 1 assert c.queue == 1
# undo # undo
assert d.undoName() assert d.undoName()
d.undo() d.undo()
d.reset() d.reset()
assert d.sched.cardCounts() == (1, 0, 0) assert d.sched.counts() == (1, 0, 0)
c.load() c.load()
assert c.queue == 0 assert c.queue == 0
assert c.left != 1 assert c.left != 1
@ -64,18 +64,18 @@ def test_review():
f['Front'] = u"two" f['Front'] = u"two"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
assert d.sched.cardCounts() == (2, 0, 0) assert d.sched.counts() == (2, 0, 0)
c = d.sched.getCard() c = d.sched.getCard()
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
c = d.sched.getCard() c = d.sched.getCard()
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
assert d.sched.cardCounts() == (0, 2, 0) assert d.sched.counts() == (0, 2, 0)
d.undo() d.undo()
d.reset() d.reset()
assert d.sched.cardCounts() == (1, 1, 0) assert d.sched.counts() == (1, 1, 0)
d.undo() d.undo()
d.reset() d.reset()
assert d.sched.cardCounts() == (2, 0, 0) assert d.sched.counts() == (2, 0, 0)
# performing a normal op will clear the review queue # performing a normal op will clear the review queue
c = d.sched.getCard() c = d.sched.getCard()
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)

View file

@ -28,7 +28,7 @@ def test_upgrade():
# 3 new, 2 failed, 1 due # 3 new, 2 failed, 1 due
deck.reset() deck.reset()
deck.conf['counts'] = COUNT_REMAINING deck.conf['counts'] = COUNT_REMAINING
assert deck.sched.cardCounts() == (3,2,1) assert deck.sched.counts() == (3,4,1)
# now's a good time to test the integrity check too # now's a good time to test the integrity check too
deck.fixIntegrity() deck.fixIntegrity()
# c = deck.sched.getCard() # c = deck.sched.getCard()