start of cram refactor

This commit is contained in:
Damien Elmes 2012-03-08 16:47:22 +09:00
parent a2312f9a1f
commit 01404fafaa
6 changed files with 125 additions and 242 deletions

View file

@ -98,10 +98,10 @@ insert or replace into cards values
self.col.db.execute(
"""update cards set
mod=?, usn=?, type=?, queue=?, due=?, ivl=?, factor=?, reps=?,
lapses=?, left=?, odue=? where id = ?""",
lapses=?, left=?, odue=?, did=? where id = ?""",
self.mod, self.usn, self.type, self.queue, self.due, self.ivl,
self.factor, self.reps, self.lapses,
self.left, self.odue, self.id)
self.left, self.odue, self.did, self.id)
def q(self, reload=False):
return self.css() + self._getQA(reload)['q']

View file

@ -58,8 +58,7 @@ class _Collection(object):
self.sessionStartReps = 0
self.sessionStartTime = 0
self.lastSessionStart = 0
self._stdSched = Scheduler(self)
self.sched = self._stdSched
self.sched = Scheduler(self)
# check for improper shutdown
self.cleanup()
@ -461,8 +460,8 @@ where c.nid == f.id
# Finding cards
##########################################################################
def findCards(self, query, full=False):
return anki.find.Finder(self).findCards(query, full)
def findCards(self, query, full=False, order=None):
return anki.find.Finder(self).findCards(query, full, order)
def findReplace(self, nids, src, dst, regex=None, field=None, fold=True):
return anki.find.findReplace(self, nids, src, dst, regex, field, fold)
@ -507,20 +506,6 @@ where c.nid == f.id
return True
return False
# Schedulers and cramming
##########################################################################
def stdSched(self):
"True if scheduler changed."
if self.sched.name != "std":
self.cleanup()
self.sched = self._stdSched
return True
def cramDecks(self, order="mod desc", min=0, max=None):
self.stdSched()
self.sched = anki.cram.CramScheduler(self, order, min, max)
# Undo
##########################################################################

View file

@ -1,116 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from anki.utils import ids2str, intTime
from anki.sched import Scheduler
# fixme: set log type for cram
class CramScheduler(Scheduler):
name = "cram"
def __init__(self, col, order, min=0, max=None):
Scheduler.__init__(self, col)
# should be the opposite order of what you want
self.order = order
# days to limit cram to, where tomorrow=0. Max is inclusive.
self.min = min
self.max = max
self.reset()
def counts(self):
return (self.newCount, self.lrnCount, 0)
def reset(self):
self._updateCutoff()
self._resetLrnCount()
self._resetLrn()
self._resetNew()
self._resetRev()
def answerCard(self, card, ease):
if card.queue == 2:
card.queue = 1
card.edue = card.due
if card.queue == 1:
self._answerLrnCard(card, ease)
else:
raise Exception("Invalid queue")
if ease == 1:
conf = self._lrnConf(card)
if conf['reset']:
# reset interval
card.ivl = max(1, int(card.ivl * conf['mult']))
# mark card as due today so that it doesn't get rescheduled
card.due = card.edue = self.today
card.mod = intTime()
card.flushSched()
def countIdx(self, card):
if card.queue == 2:
return 0
else:
return 1
def answerButtons(self, card):
return 3
# Fetching
##########################################################################
def _resetNew(self):
"All review cards that are not due yet."
if self.max is not None:
maxlim = "and due <= %d" % (self.today+1+self.max)
else:
maxlim = ""
self.newQueue = self.col.db.list("""
select id from cards where did in %s and queue = 2 and due >= %d
%s order by %s limit %d""" % (self._deckLimit(),
self.today+1+self.min,
maxlim,
self.order,
self.reportLimit))
self.newCount = len(self.newQueue)
def _resetRev(self):
self.revQueue = []
self.revCount = 0
def _timeForNewCard(self):
return True
def _getNewCard(self):
if self.newQueue:
id = self.newQueue.pop()
self.newCount -= 1
return id
# Answering
##########################################################################
def _rescheduleAsRev(self, card, conf, early):
Scheduler._rescheduleAsRev(self, card, conf, early)
ivl = self._graduatingIvl(card, conf, early)
card.due = self.today + ivl
# temporarily suspend it
self.col.setDirty()
card.queue = -3
def _graduatingIvl(self, card, conf, early):
if conf['resched']:
# shift card by the time it was delayed
return card.ivl - card.edue - self.today
else:
return card.ivl
def _lrnConf(self, card):
return self._cardConf(card)['cram']
# Next time reports
##########################################################################
def nextIvl(self, card, ease):
"Return the next interval for CARD, in seconds."
return self._nextLrnIvl(card, ease)

View file

@ -37,8 +37,9 @@ class Finder(object):
def __init__(self, col):
self.col = col
def findCards(self, query, full=False):
def findCards(self, query, full=False, order=None):
"Return a list of card ids for QUERY."
self.order = order
self.query = query
self.full = full
self._findLimits()
@ -57,7 +58,7 @@ and c.nid=n.id
select c.id from cards c, notes n where %s
and c.nid=n.id %s""" % (q, order)
res = self.col.db.list(query, **args)
if self.col.conf['sortBackwards']:
if not self.order and self.col.conf['sortBackwards']:
res.reverse()
return res
@ -68,6 +69,9 @@ and c.nid=n.id %s""" % (q, order)
return q, self.lims['args']
def _order(self):
# user provided override?
if self.order:
return self.order
type = self.col.conf['sortType']
if not type:
return

View file

@ -15,6 +15,11 @@ from anki.hooks import runHook
# other queue types: -1=suspended, -2=buried
# positive intervals are in days (rev), negative intervals in seconds (lrn)
# fixme:
# - should log cram reps as cramming
# - later we should set conf=None for the cram deck to catch where we're
# pulling from the original conf instead of the cram conf
class Scheduler(object):
name = "std"
def __init__(self, col):
@ -23,6 +28,7 @@ class Scheduler(object):
self.reportLimit = 1000
# fixme: replace reps with deck based counts
self.reps = 0
self._cramming = False
self._updateCutoff()
def getCard(self):
@ -34,6 +40,8 @@ class Scheduler(object):
return card
def reset(self):
deck = self.col.decks.current()
self._cramming = deck.get('cram')
self._updateCutoff()
self._resetLrn()
self._resetRev()
@ -44,12 +52,23 @@ class Scheduler(object):
self.col.markReview(card)
self.reps += 1
card.reps += 1
wasNew = (card.queue == 0) and card.type != 2
wasNew = card.queue == 0
if wasNew:
# put it in the learn queue
# came from the new queue, move to learning
card.queue = 1
card.type = 1
# if it was a new card, it's now a learning card
if card.type == 0:
card.type = 1
# init reps to graduation
card.left = self._startingLeft(card)
# cramming?
if self._cramming and card.type == 2:
# reviews get their ivl boosted on first sight
elapsed = card.ivl - card.odue - self.today
assert card.factor
factor = ((card.factor/1000.0)+1.2)/2.0
card.ivl = int(max(card.ivl, elapsed * factor, 1))+1
card.odue = self.today + card.ivl
self._updateStats(card, 'new')
if card.queue == 1:
self._answerLrnCard(card, ease)
@ -316,6 +335,8 @@ select id, due from cards where did = ? and queue = 0 limit ?""", did, lim)
"True if it's time to display a new card when distributing."
if not self.newCount:
return False
if self._cramming:
return True
if self.col.conf['newSpread'] == NEW_CARDS_LAST:
return False
elif self.col.conf['newSpread'] == NEW_CARDS_FIRST:
@ -348,6 +369,8 @@ select count() from
def _deckNewLimitSingle(self, g):
"Limit for deck without parent limits."
if self._cramming:
return self.reportLimit
c = self.col.decks.confForDid(g['id'])
return max(0, c['new']['perDay'] - g['newToday'][1])
@ -445,12 +468,16 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff)
def _rescheduleAsRev(self, card, conf, early):
if card.type == 2:
# failed; put back entry due
card.due = card.odue
else:
self._rescheduleNew(card, conf, early)
card.queue = 2
card.type = 2
# if we were cramming, graduating means moving back to the old deck
if self._cramming:
card.did = card.odid
card.odue = 0
card.odid = 0
def _startingLeft(self, card):
return len(self._cardConf(card)['new']['delays'])
@ -471,6 +498,7 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff)
return ideal
def _rescheduleNew(self, card, conf, early):
"Reschedule a new card that's graduated for the first time."
card.ivl = self._graduatingIvl(card, conf, early)
card.due = self.today+card.ivl
card.factor = conf['initialFactor']
@ -691,6 +719,57 @@ did = ? and queue = 2 and due <= ? %s limit ?""" % order,
break
return idealIvl + fudge
# Creation of cramming decks
##########################################################################
# type is one of all, due, rev
# order is one of due, added, random, relative, lapses
def cram(self, search, type="all", order="due",
limit=100, sched=[1, 10]):
# gather card ids and sort
order = self._cramOrder(order)
limit = " limit %d" % limit
ids = self.col.findCards(search, order=order+limit)
if not order:
random.shuffle(ids)
# get a cram deck
did = self._cramDeck()
# move the cards over
self._moveToCram(did, ids)
# and change to our new deck
self.col.decks.select(did)
def _cramOrder(self, order):
if order == "due":
return "order by c.due"
elif order == "added":
return "order by n.id"
elif order == "random":
return ""
elif order == "relative":
pass
elif order == "lapses":
return "order by lapses desc"
elif order == "failed":
pass
def _cramDeck(self):
did = self.col.decks.id(_("Cram"))
deck = self.col.decks.get(did)
# mark it as a cram deck
deck['cram'] = True
self.col.decks.save(deck)
return did
def _moveToCram(self, did, ids):
data = []
t = intTime(); u = self.col.usn()
for c, id in enumerate(ids):
data.append((did, c, t, u, id))
self.col.db.executemany("""
update cards set odid = did, odue = due, did = ?, queue = 0, due = ?,
mod = ?, usn = ? where id = ?""", data)
# Leeches
##########################################################################

View file

@ -431,8 +431,6 @@ def test_suspend():
assert c.due == 1
def test_cram():
print "disabled for now"
return
d = getEmptyDeck()
f = d.newNote()
f['Front'] = u"one"
@ -443,112 +441,45 @@ def test_cram():
# due in 25 days, so it's been waiting 75 days
c.due = d.sched.today + 25
c.mod = 1
c.factor = 2500
c.startTimer()
c.flush()
d.reset()
assert d.sched.counts() == (0,0,0)
cardcopy = copy.copy(c)
d.conf['decks'] = [1]
d.cramDecks()
# first, test with initial intervals preserved
conf = d.sched._lrnConf(c)
conf['reset'] = False
conf['resched'] = False
assert d.sched.counts() == (1, 0, 0)
d.sched.cram("")
d.reset()
# should appear as new in the deck list
assert sorted(d.sched.deckDueList())[0][3] == 1
# and should appear in the counts
assert d.sched.counts() == (1,0,0)
# grab it and make one step
c = d.sched.getCard()
d.sched._cardConf(c)['cram']['delays'] = [0.5, 3, 10]
assert d.sched.counts() == (0, 0, 0)
d.sched.answerCard(c, 2)
# elapsed time was 75 days
# factor = 2.5+1.2/2 = 1.85
# int(75*1.85)+1 = 139
assert c.ivl == 139
assert c.odue == 139
assert c.queue == 1
# when it graduates, due is updated
c = d.sched.getCard()
d.sched.answerCard(c, 2)
assert c.ivl == 139
assert c.due == 139
assert c.queue == 2
# and it will have moved back to the previous deck
assert c.did == 1
# card will have moved b
#assert sorted(d.sched.deckDueList())[0][3] == 1
return
# check that estimates work
assert d.sched.nextIvl(c, 1) == 30
assert d.sched.nextIvl(c, 2) == 180
assert d.sched.nextIvl(c, 3) == 86400*100
# failing it should not reset ivl
assert c.ivl == 100
d.sched.answerCard(c, 1)
assert c.ivl == 100
# and should have incremented lrn count
assert d.sched.counts()[1] == 1
# reset ivl for exit test, and pass card
d.sched.answerCard(c, 2)
delta = c.due - time.time()
assert delta > 175 and delta <= 180
# another two answers should reschedule it
assert c.queue == 1
d.sched.answerCard(c, 2)
d.sched.answerCard(c, 2)
assert c.queue == -3
assert c.ivl == 100
assert c.due == d.sched.today + c.ivl
# and if the queue is reset, it shouldn't appear in the new queue again
d.reset()
assert d.sched.counts() == (0, 0, 0)
# now try again with ivl rescheduling
c = copy.copy(cardcopy)
c.flush()
d.cramDecks()
conf = d.sched._lrnConf(c)
conf['reset'] = False
conf['resched'] = True
# failures shouldn't matter
d.sched.answerCard(c, 1)
# graduating the card will keep the same interval, but shift the card
# forward the number of days it had been waiting (75)
assert d.sched.nextIvl(c, 3) == 75*86400
d.sched.answerCard(c, 3)
assert c.ivl == 100
assert c.due == d.sched.today + 75
# try with ivl reset
c = copy.copy(cardcopy)
c.flush()
d.cramDecks()
conf = d.sched._lrnConf(c)
conf['reset'] = True
conf['resched'] = True
d.sched.answerCard(c, 1)
assert d.sched.nextIvl(c, 3) == 1*86400
d.sched.answerCard(c, 3)
assert c.ivl == 1
assert c.due == d.sched.today + 1
# users should be able to cram entire deck too
d.conf['decks'] = []
d.cramDecks()
assert d.sched.counts()[0] > 0
def test_cramLimits():
d = getEmptyDeck()
# create three cards, due tomorrow, the next, etc
for i in range(3):
f = d.newNote()
f['Front'] = str(i)
d.addNote(f)
c = f.cards()[0]
c.type = c.queue = 2
c.due = d.sched.today + 1 + i
c.flush()
# the default cram should return all three
d.conf['decks'] = [1]
d.cramDecks()
assert d.sched.counts()[0] == 3
# if we start from the day after tomorrow, it should be 2
d.cramDecks(min=1)
assert d.sched.counts()[0] == 2
# or after 2 days
d.cramDecks(min=2)
assert d.sched.counts()[0] == 1
# we may get nothing
d.cramDecks(min=3)
assert d.sched.counts()[0] == 0
# tomorrow(0) + dayAfter(1) = 2
d.cramDecks(max=1)
assert d.sched.counts()[0] == 2
# if max is tomorrow, we get only one
d.cramDecks(max=0)
assert d.sched.counts()[0] == 1
# both should work
d.cramDecks(min=0, max=0)
assert d.sched.counts()[0] == 1
d.cramDecks(min=1, max=1)
assert d.sched.counts()[0] == 1
d.cramDecks(min=0, max=1)
assert d.sched.counts()[0] == 2
def test_adjIvl():
d = getEmptyDeck()