mirror of
https://github.com/ankitects/anki.git
synced 2025-09-23 08:22:24 -04:00
start of cram refactor
This commit is contained in:
parent
a2312f9a1f
commit
01404fafaa
6 changed files with 125 additions and 242 deletions
|
@ -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']
|
||||
|
|
|
@ -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
|
||||
##########################################################################
|
||||
|
||||
|
|
116
anki/cram.py
116
anki/cram.py
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
##########################################################################
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue