encode daily steps in left

When a user has learning steps that extend past the daily cutoff, we end up
counting them all instead of only the ones that would be done today. In order
to avoid this without expensive calculations or db schema changes, we
calculate the number of steps until the daily cutoff and pack it into the left
column, as totalLeft + leftToday*1000.
This commit is contained in:
Damien Elmes 2012-05-20 13:55:49 +09:00
parent 33407a6043
commit 32fde2a072
6 changed files with 45 additions and 19 deletions

View file

@ -42,10 +42,10 @@ MODEL_STD = 0
MODEL_CLOZE = 1 MODEL_CLOZE = 1
# deck schema & syncing vars # deck schema & syncing vars
SCHEMA_VERSION = 9 SCHEMA_VERSION = 10
SYNC_ZIP_SIZE = int(2.5*1024*1024) SYNC_ZIP_SIZE = int(2.5*1024*1024)
SYNC_URL = os.environ.get("SYNC_URL") or "https://beta.ankiweb.net/sync/" SYNC_URL = os.environ.get("SYNC_URL") or "https://beta.ankiweb.net/sync/"
SYNC_VER = 3 SYNC_VER = 4
# Labels # Labels
########################################################################## ##########################################################################

View file

@ -88,7 +88,7 @@ class Scheduler(object):
if card: if card:
idx = self.countIdx(card) idx = self.countIdx(card)
if idx == 1: if idx == 1:
counts[1] += card.left counts[1] += card.left/1000
else: else:
counts[idx] += 1 counts[idx] += 1
return tuple(counts) return tuple(counts)
@ -388,7 +388,7 @@ select count() from
def _resetLrnCount(self): def _resetLrnCount(self):
self.lrnCount = self.col.db.scalar(""" self.lrnCount = self.col.db.scalar("""
select sum(left) 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
@ -418,7 +418,7 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff)
if self._lrnQueue[0][0] < cutoff: if self._lrnQueue[0][0] < cutoff:
id = heappop(self._lrnQueue)[1] id = heappop(self._lrnQueue)[1]
card = self.col.getCard(id) card = self.col.getCard(id)
self.lrnCount -= card.left self.lrnCount -= card.left/1000
return card return card
def _answerLrnCard(self, card, ease): def _answerLrnCard(self, card, ease):
@ -438,13 +438,15 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff)
self._rescheduleAsRev(card, conf, True) self._rescheduleAsRev(card, conf, True)
leaving = True leaving = True
# graduation time? # graduation time?
elif ease == 2 and card.left-1 <= 0: elif ease == 2 and (card.left%1000)-1 <= 0:
self._rescheduleAsRev(card, conf, False) self._rescheduleAsRev(card, conf, False)
leaving = True leaving = True
else: else:
# one step towards graduation # one step towards graduation
if ease == 2: if ease == 2:
card.left -= 1 # decrement real left count and recalculate left today
left = (card.left % 1000) - 1
card.left = self._leftToday(conf['delays'], left)*1000 + left
# failed # failed
else: else:
card.left = self._startingLeft(card) card.left = self._startingLeft(card)
@ -462,7 +464,7 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff)
delay *= random.uniform(1, 1.25) delay *= random.uniform(1, 1.25)
card.due = int(time.time() + delay) card.due = int(time.time() + delay)
if card.due < self.dayCutoff: if card.due < self.dayCutoff:
self.lrnCount += card.left 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
@ -473,6 +475,7 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff)
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):
left = left % 1000
try: try:
delay = conf['delays'][-left] delay = conf['delays'][-left]
except IndexError: except IndexError:
@ -501,7 +504,22 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff)
def _startingLeft(self, card): def _startingLeft(self, card):
conf = self._lrnConf(card) conf = self._lrnConf(card)
return len(conf['delays']) tot = len(conf['delays'])
tod = self._leftToday(conf['delays'], tot)
return tot + tod*1000
def _leftToday(self, delays, left, now=None):
"The number of steps that can be completed by the day cutoff."
if not now:
now = intTime()
delays = delays[-left:]
ok = 0
for i in range(len(delays)):
now += delays[i]*60
if now > self.dayCutoff:
break
ok = i
return ok+1
def _graduatingIvl(self, card, conf, early, adj=True): def _graduatingIvl(self, card, conf, early, adj=True):
if card.type == 2: if card.type == 2:
@ -565,7 +583,7 @@ where queue = 1 and type = 2
def _lrnForDeck(self, did): def _lrnForDeck(self, did):
return self.col.db.scalar( return self.col.db.scalar(
""" """
select sum(left) from select sum(left/1000) from
(select left from cards where did = ? and queue = 1 and due < ? limit ?)""", (select left from cards where did = ? and queue = 1 and due < ? limit ?)""",
did, intTime() + self.col.conf['collapseTime'], self.reportLimit) or 0 did, intTime() + self.col.conf['collapseTime'], self.reportLimit) or 0
@ -662,8 +680,9 @@ did = ? and queue = 2 and due <= ? limit ?""",
card.odue = card.due card.odue = card.due
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.left += self._leftToday(conf['delays'], card.left)*1000
card.queue = 1 card.queue = 1
self.lrnCount += card.left self.lrnCount += card.left/1000
# leech? # leech?
if not self._checkLeech(card, conf) and conf['delays']: if not self._checkLeech(card, conf) and conf['delays']:
heappush(self._lrnQueue, (card.due, card.id)) heappush(self._lrnQueue, (card.due, card.id))
@ -1024,7 +1043,7 @@ your short-term review workload will become."""))
# early removal # early removal
return self._graduatingIvl(card, conf, True, adj=False) * 86400 return self._graduatingIvl(card, conf, True, adj=False) * 86400
else: else:
left = card.left - 1 left = card.left%1000 - 1
if left <= 0: if left <= 0:
# graduate # graduate
return self._graduatingIvl(card, conf, False, adj=False) * 86400 return self._graduatingIvl(card, conf, False, adj=False) * 86400

View file

@ -135,6 +135,10 @@ def _upgrade(col, ver):
if changed: if changed:
col.media.db.commit() col.media.db.commit()
col.db.execute("update col set ver = 9") col.db.execute("update col set ver = 9")
if ver < 10:
col.db.execute("""
update cards set left = left + left*1000 where queue = 1""")
col.db.execute("update col set ver = 10")
def _upgradeClozeModel(col, m): def _upgradeClozeModel(col, m):
m['type'] = MODEL_CLOZE m['type'] = MODEL_CLOZE

View file

@ -682,7 +682,7 @@ and ord = ? limit 1""", m['id'], t['ord']):
for t in ("cards", "notes", "models", "media"): for t in ("cards", "notes", "models", "media"):
col.db.execute("drop table if exists %sDeleted" % t) col.db.execute("drop table if exists %sDeleted" % t)
# and failed cards # and failed cards
left = len(col.decks.confForDid(1)['new']['delays']) left = len(col.decks.confForDid(1)['new']['delays'])*1001
col.db.execute("update cards set odue = ?, left=? where type = 1", col.db.execute("update cards set odue = ?, left=? where type = 1",
col.sched.today+1, left) col.sched.today+1, left)
# and due cards # and due cards

View file

@ -112,7 +112,8 @@ def test_learn():
# fail it # fail it
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
# it should have three reps left to graduation # it should have three reps left to graduation
assert c.left == 3 assert c.left%1000 == 3
assert c.left/1000 == 3
# it should by due in 30 seconds # it should by due in 30 seconds
t = round(c.due - time.time()) t = round(c.due - time.time())
assert t >= 25 and t <= 40 assert t >= 25 and t <= 40
@ -120,7 +121,8 @@ def test_learn():
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
# it should by due in 3 minutes # it should by due in 3 minutes
assert round(c.due - time.time()) in (179, 180) assert round(c.due - time.time()) in (179, 180)
assert c.left == 2 assert c.left%1000 == 2
assert c.left/1000 == 2
# check log is accurate # check log is accurate
log = d.db.first("select * from revlog order by id desc") log = d.db.first("select * from revlog order by id desc")
assert log[3] == 2 assert log[3] == 2
@ -130,7 +132,8 @@ def test_learn():
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
# it should by due in 10 minutes # it should by due in 10 minutes
assert round(c.due - time.time()) in (599, 600) assert round(c.due - time.time()) in (599, 600)
assert c.left == 1 assert c.left%1000 == 1
assert c.left/1000 == 1
# the next pass should graduate the card # the next pass should graduate the card
assert c.queue == 1 assert c.queue == 1
assert c.type == 1 assert c.type == 1
@ -299,7 +302,7 @@ def test_overdue_lapse():
c.due = -1 c.due = -1
c.odue = -1 c.odue = -1
c.factor = 2500 c.factor = 2500
c.left = 2 c.left = 2002
c.ivl = 0 c.ivl = 0
c.flush() c.flush()
d.sched._clearOverdue = False d.sched._clearOverdue = False

View file

@ -48,7 +48,7 @@ def test_review():
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 == 1001
assert d.sched.counts() == (0, 1, 0) assert d.sched.counts() == (0, 1, 0)
assert c.queue == 1 assert c.queue == 1
# undo # undo
@ -58,7 +58,7 @@ def test_review():
assert d.sched.counts() == (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 != 1001
assert not d.undoName() assert not d.undoName()
# we should be able to undo multiple answers too # we should be able to undo multiple answers too
f['Front'] = u"two" f['Front'] = u"two"