diff --git a/anki/sched.py b/anki/sched.py
index be6242b85..97c626aea 100644
--- a/anki/sched.py
+++ b/anki/sched.py
@@ -17,11 +17,11 @@ 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
# positive revlog intervals are in days (rev), negative in seconds (lrn)
+# odue/odid store original due/did when cards moved to filtered deck
class Scheduler:
name = "std"
haveCustomStudy = True
- _spreadRev = True
_burySiblingsOnAnswer = True
def __init__(self, col):
@@ -57,42 +57,52 @@ class Scheduler:
def answerCard(self, card, ease):
self.col.log()
assert 1 <= ease <= 4
+ assert 0 <= card.queue <= 3
self.col.markReview(card)
if self._burySiblingsOnAnswer:
self._burySiblings(card)
card.reps += 1
- # former is for logging new cards, latter also covers filt. decks
- card.wasNew = card.type == 0
- wasNewQ = card.queue == 0
- if wasNewQ:
- # came from the new queue, move to learning
- card.queue = 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)
- # dynamic?
- if card.odid and card.type == 2:
- if self._resched(card):
- # reviews get their ivl boosted on first sight
- card.ivl = self._dynIvlBoost(card)
- card.odue = self.today + card.ivl
- self._updateStats(card, 'new')
- if card.queue in (1, 3):
- self._answerLrnCard(card, ease)
- if not wasNewQ:
- self._updateStats(card, 'lrn')
- elif card.queue == 2:
- self._answerRevCard(card, ease)
- self._updateStats(card, 'rev')
+
+ # filtered cards handled differently
+ if card.odid:
+ self._answerFilteredCard(card, ease)
else:
- raise Exception("Invalid queue")
+ self._answerNormalCard(card, ease)
+
self._updateStats(card, 'time', card.timeTaken())
card.mod = intTime()
card.usn = self.col.usn()
card.flushSched()
+ def _answerNormalCard(self, card, ease):
+ card.wasNew = card.type == 0
+
+ if card.queue == 0:
+ # came from the new queue, move to learning
+ card.queue = 1
+ card.type = 1
+ # init reps to graduation
+ card.left = self._startingLeft(card)
+ # update daily limit
+ self._updateStats(card, 'new')
+
+ if card.queue in (1, 3):
+ self._answerLrnCard(card, ease)
+ #if not wasNewQ:
+ # self._updateStats(card, 'lrn')
+ elif card.queue == 2:
+ self._answerRevCard(card, ease)
+ # update daily limit
+ self._updateStats(card, 'rev')
+
+ def _answerFilteredCard(self, card, ease):
+ # a review card that wasn't due?
+ if card.queue == 0 and card.type == 2:
+ self._answerEarlyRevCard(card, ease)
+ return
+
+ return self._answerNormalCard(card, ease)
+
def counts(self, card=None):
counts = [self.newCount, self.lrnCount, self.revCount]
if card:
@@ -127,18 +137,19 @@ order by due""" % self._deckLimit(),
return card.queue
def answerButtons(self, card):
- if card.odue:
- # normal review in dyn deck?
- if card.odid and card.queue == 2:
- return 4
- conf = self._lrnConf(card)
- if card.type in (0,1) or len(conf['delays']) > 1:
- return 3
- return 2
- elif card.queue == 2:
- return 4
- else:
- return 3
+ conf = self._cardConf(card)
+
+ # fixme: resched=off case
+ # if card.odid:
+ # if not conf['resched']:
+ # if card.queue == 2:
+ # return 4
+ # conf = self._lrnConf(card)
+ # if card.type in (0,1) or len(conf['delays']) > 1:
+ # return 3
+ # return 2
+
+ return 4
def unburyCards(self):
"Unbury cards."
@@ -516,66 +527,90 @@ did = ? and queue = 3 and due <= ? limit ?""",
return self.col.getCard(self._lrnDayQueue.pop())
def _answerLrnCard(self, card, ease):
- # ease 1=no, 2=yes, 3=remove
conf = self._lrnConf(card)
- if card.odid and not card.wasNew:
- type = 3
- elif card.type == 2:
+ if card.type == 2:
type = 2
else:
type = 0
- leaving = False
# lrnCount was decremented once when card was fetched
lastLeft = card.left
# immediate graduate?
- if ease == 3:
+ if ease == 4:
self._rescheduleAsRev(card, conf, True)
- leaving = True
- # graduation time?
- elif ease == 2 and (card.left%1000)-1 <= 0:
- self._rescheduleAsRev(card, conf, False)
- leaving = True
+ self._logLrn(card, ease, conf, True, type, lastLeft)
+ # next step?
+ elif ease == 3:
+ # graduation time?
+ if (card.left%1000)-1 <= 0:
+ self._rescheduleAsRev(card, conf, False)
+ self._logLrn(card, ease, conf, True, type, lastLeft)
+ else:
+ self._moveToNextStep(card, conf)
+ self._logLrn(card, ease, conf, False, type, lastLeft)
+ elif ease == 2:
+ self._repeatStep(card, conf)
+ self._logLrn(card, ease, conf, False, type, lastLeft)
else:
- # one step towards graduation
- if ease == 2:
- # decrement real left count and recalculate left today
- left = (card.left % 1000) - 1
- card.left = self._leftToday(conf['delays'], left)*1000 + left
- # failed
- else:
- card.left = self._startingLeft(card)
- resched = self._resched(card)
- if 'mult' in conf and resched:
- # review that's lapsed
- card.ivl = max(1, conf['minInt'], card.ivl*conf['mult'])
- else:
- # new card; no ivl adjustment
- pass
- if resched and card.odid:
- card.odue = self.today + 1
+ # back to first step
+ self._moveToFirstStep(card, conf)
+ self._logLrn(card, ease, conf, False, type, lastLeft)
+
+ def _moveToFirstStep(self, card, conf):
+ card.left = self._startingLeft(card)
+ resched = self._resched(card)
+
+ card.lastIvl = card.ivl
+ if card.type == 2 and resched:
+ # review card that will move to relearning
+ card.ivl = self._lapseIvl(card, self._lrnConf(card))
+
+ if card.queue not in (1,3):
+ card.lapses += 1
+ card.factor = max(1300, card.factor-200)
+
+ # if no relearning steps, reschedule as review immediately
+ if not conf['delays']:
+ self._rescheduleAsRev(card, conf, False)
+ return
+
+ return self._rescheduleLrnCard(card, conf)
+
+ def _moveToNextStep(self, card, conf):
+ # decrement real left count and recalculate left today
+ left = (card.left % 1000) - 1
+ card.left = self._leftToday(conf['delays'], left)*1000 + left
+
+ self._rescheduleLrnCard(card, conf)
+
+ def _repeatStep(self, card, conf):
+ delay = self._delayForRepeatingGrade(conf, card.left)
+ self._rescheduleLrnCard(card, conf, delay=delay)
+
+ def _rescheduleLrnCard(self, card, conf, delay=None):
+ if delay is None:
delay = self._delayForGrade(conf, card.left)
- if card.due < time.time():
- # not collapsed; add some randomness
- delay *= random.uniform(1, 1.25)
- card.due = int(time.time() + delay)
- # due today?
- if card.due < self.dayCutoff:
- self.lrnCount += card.left // 1000
- # 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
- # it twice in a row
- card.queue = 1
- if self._lrnQueue and not self.revCount and not self.newCount:
- smallestDue = self._lrnQueue[0][0]
- 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)
+ if card.due < time.time():
+ # not collapsed; add some randomness
+ delay *= random.uniform(1, 1.25)
+ card.due = int(time.time() + delay)
+ # due today?
+ if card.due < self.dayCutoff:
+ self.lrnCount += card.left // 1000
+ # 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
+ # it twice in a row
+ card.queue = 1
+ if self._lrnQueue and not self.revCount and not self.newCount:
+ smallestDue = self._lrnQueue[0][0]
+ 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
+ return delay
def _delayForGrade(self, conf, left):
left = left % 1000
@@ -589,6 +624,13 @@ did = ? and queue = 3 and due <= ? limit ?""",
delay = 1
return delay*60
+ def _delayForRepeatingGrade(self, conf, left):
+ # halfway between last and next
+ delay1 = self._delayForGrade(conf, left)
+ delay2 = self._delayForGrade(conf, left-1)
+ avg = (delay1+max(delay1, delay2))//2
+ return avg
+
def _lrnConf(self, card):
if card.type == 2:
return self._lapseConf(card)
@@ -597,26 +639,31 @@ did = ? and queue = 3 and due <= ? limit ?""",
def _rescheduleAsRev(self, card, conf, early):
lapse = card.type == 2
- if lapse:
- if self._resched(card):
- card.due = max(self.today+1, card.odue)
- else:
- card.due = card.odue
- card.odue = 0
- else:
- self._rescheduleNew(card, conf, early)
- card.queue = 2
- card.type = 2
- # if we were dynamic, graduating means moving back to the old deck
resched = self._resched(card)
+
+ if resched:
+ if lapse:
+ self._rescheduleGraduatingLapse(card)
+ else:
+ self._rescheduleNew(card, conf, early)
+
+ # if we were dynamic, graduating means moving back to the old deck
if card.odid:
+ # and leaving learning if scheduling off
+ if not resched:
+ card.due = card.odue
+ if card.type == 2:
+ card.queue = 2
+ else:
+ card.queue = card.type = 0
+
card.did = card.odid
card.odue = 0
card.odid = 0
- # if rescheduling is off, it needs to be set back to a new card
- if not resched and not lapse:
- card.queue = card.type = 0
- card.due = self.col.nextID("pos")
+
+ def _rescheduleGraduatingLapse(self, card):
+ card.due = self.today+card.ivl
+ card.queue = 2
def _startingLeft(self, card):
if card.type == 2:
@@ -640,12 +687,8 @@ did = ? and queue = 3 and due <= ? limit ?""",
ok = i
return ok+1
- def _graduatingIvl(self, card, conf, early, adj=True):
+ def _graduatingIvl(self, card, conf, early, fuzz=True):
if card.type == 2:
- # lapsed card being relearnt
- if card.odid:
- if conf['resched']:
- return self._dynIvlBoost(card)
return card.ivl
if not early:
# graduate
@@ -653,16 +696,16 @@ did = ? and queue = 3 and due <= ? limit ?""",
else:
# early remove
ideal = conf['ints'][1]
- if adj:
- return self._adjRevIvl(card, ideal)
- else:
- return ideal
+ if fuzz:
+ ideal = self._fuzzedIvl(ideal)
+ 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']
+ card.type = card.queue = 2
def _logLrn(self, card, ease, conf, leaving, type, lastLeft):
lastIvl = -(self._delayForGrade(conf, lastLeft))
@@ -690,10 +733,11 @@ did = ? and queue = 3 and due <= ? limit ?""",
# review cards in relearning
self.col.db.execute("""
update cards set
-due = odue, queue = 2, mod = %d, usn = %d, odue = 0
+due = ivl+?, queue = 2, mod = %d, usn = %d,
+odue = 0 -- older anki versions set odue when cards in relearning
where queue in (1,3) and type = 2
%s
-""" % (intTime(), self.col.usn(), extra))
+""" % (intTime(), self.col.usn(), extra), self.today)
# new cards in learning
self.forgetCards(self.col.db.list(
"select id from cards where queue in (1,3) %s" % extra))
@@ -806,48 +850,36 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
self._rescheduleRev(card, ease)
self._logRev(card, ease, delay)
+ def _answerEarlyRevCard(self, card, ease):
+ delay = 0
+ if ease == 1:
+ delay = self._rescheduleLapse(card)
+ else:
+ self._rescheduleEarlyRev(card, ease)
+
+ self._logRev(card, ease, delay, type=3)
+
def _rescheduleLapse(self, card):
conf = self._lapseConf(card)
- card.lastIvl = card.ivl
- if self._resched(card):
- card.lapses += 1
- card.ivl = self._nextLapseIvl(card, conf)
- card.factor = max(1300, card.factor-200)
- card.due = self.today + card.ivl
- # if it's a filtered deck, update odue as well
- if card.odid:
- card.odue = card.due
+ delay = self._moveToFirstStep(card, conf)
+
# if suspended as a leech, nothing to do
- delay = 0
if self._checkLeech(card, conf) and card.queue == -1:
- return delay
- # if no relearning steps, nothing to do
- if not conf['delays']:
- return delay
- # record rev due date for later
- if not card.odue:
- card.odue = card.due
- delay = self._delayForGrade(conf, 0)
- card.due = int(delay + time.time())
- card.left = self._startingLeft(card)
- # queue 1
- if card.due < self.dayCutoff:
- self.lrnCount += card.left // 1000
- card.queue = 1
- heappush(self._lrnQueue, (card.due, card.id))
- else:
- # day learn queue
- ahead = ((card.due - self.dayCutoff) // 86400) + 1
- card.due = self.today + ahead
- card.queue = 3
+ return 0
+
return delay
- def _nextLapseIvl(self, card, conf):
- return max(conf['minInt'], int(card.ivl*conf['mult']))
+ def _lapseIvl(self, card, conf):
+ due = card.odue or card.due
+ elapsed = card.ivl - (due - self.today)
+ ivl = min(elapsed, card.ivl)
+ ivl = max(1, conf['minInt'], ivl*conf['mult'])
+ return ivl
def _rescheduleRev(self, card, ease):
# update interval
card.lastIvl = card.ivl
+
if self._resched(card):
self._updateRevIvl(card, ease)
# then the rest
@@ -855,18 +887,41 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
card.due = self.today + card.ivl
else:
card.due = card.odue
+
+ # card leaves filtered deck
if card.odid:
card.did = card.odid
card.odid = 0
card.odue = 0
- def _logRev(self, card, ease, delay):
+ def _rescheduleEarlyRev(self, card, ease):
+ # update interval
+ card.lastIvl = card.ivl
+
+ if self._resched(card):
+ self._updateEarlyRevIvl(card, ease)
+ # then the rest
+ card.factor = max(1300, card.factor+[-150, 0, 150][ease-2])
+ card.due = self.today + card.ivl
+ else:
+ card.due = card.odue
+
+ # move from 0->2
+ card.queue = 2
+
+ # card leaves filtered deck
+ if card.odid:
+ card.did = card.odid
+ card.odid = 0
+ card.odue = 0
+
+ def _logRev(self, card, ease, delay, type=1):
def log():
self.col.db.execute(
"insert into revlog values (?,?,?,?,?,?,?,?,?)",
int(time.time()*1000), card.id, self.col.usn(), ease,
-delay or card.ivl, card.lastIvl, card.factor, card.timeTaken(),
- 1)
+ type)
try:
log()
except:
@@ -926,13 +981,49 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
def _updateRevIvl(self, card, ease):
idealIvl = self._nextRevIvl(card, ease)
- card.ivl = min(max(self._adjRevIvl(card, idealIvl), card.ivl+1),
+ card.ivl = min(max(self._fuzzedIvl(idealIvl), card.ivl+1),
self._revConf(card)['maxIvl'])
- def _adjRevIvl(self, card, idealIvl):
- if self._spreadRev:
- idealIvl = self._fuzzedIvl(idealIvl)
- return idealIvl
+ def _updateEarlyRevIvl(self, card, ease):
+ idealIvl = self._earlyReviewIvl(card, ease)
+ card.ivl = min(max(self._fuzzedIvl(idealIvl), card.ivl+1),
+ self._revConf(card)['maxIvl'])
+
+ # next interval for card when answered early+correctly
+ def _earlyReviewIvl(self, card, ease):
+ assert card.odid and card.type == 2
+ assert card.factor
+ assert ease > 1
+
+ elapsed = card.ivl - (card.odue - self.today)
+
+ conf = self._revConf(card)
+
+ easyBonus = 0
+ # early 3/4 reviews shouldn't decrease previous interval
+ minNewIvl = 1
+
+ if ease == 2:
+ factor = 1.2
+ # hard cards shouldn't have their interval decreased by more than 50%
+ minNewIvl = 0.5
+ elif ease == 3:
+ factor = card.factor / 1000
+ else: # ease == 4:
+ factor = card.factor / 1000 * conf['ease4']
+ # add an extra day, so early reviews have an easy interval nominally
+ # different from the good answer
+ easyBonus = 1
+
+ ivl = max(elapsed * factor, 1)
+
+ # cap interval decreases
+ ivl = max(card.ivl*minNewIvl+easyBonus, ivl)
+
+ # don't require prev+1 for early
+ ivl = self._constrainedIvl(ivl, conf, 0)
+
+ return min(conf['maxIvl'], ivl)
# Dynamic deck handling
##########################################################################
@@ -971,15 +1062,37 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
if not lim:
lim = "did = %s" % did
self.col.log(self.col.db.list("select id from cards where %s" % lim))
- # move out of cram queue
+
self.col.db.execute("""
-update cards set did = odid, queue = (case when type = 1 then 0
-else type end), type = (case when type = 1 then 0 else type end),
+update cards set did = odid, queue = (case when queue = 0 then type else queue end),
due = odue, odue = 0, odid = 0, usn = ? where %s""" % lim,
self.col.usn())
def remFromDyn(self, cids):
- self.emptyDyn(None, "id in %s and odid" % ids2str(cids))
+ for did, grpcids in self._cidsByDid(cids):
+ self.emptyDyn(did, "id in %s" % ids2str(grpcids))
+
+ def _cidsByDid(self, cids):
+ groups = []
+ currentCids = []
+ currentDid = None
+ for id, did in self.col.db.execute("""
+select id, did from cards where id in %s and odid
+group by did
+""" % ids2str(cids)):
+ # next group?
+ if did != currentDid:
+ if currentCids:
+ groups.append((currentDid, currentCids))
+ currentCids = []
+ currentDid = did
+
+ currentCids.append(id)
+
+ if currentCids:
+ groups.append((currentDid, currentCids))
+
+ return groups
def _dynOrder(self, o, l):
if o == DYN_OLDEST:
@@ -1013,26 +1126,22 @@ due = odue, odue = 0, odid = 0, usn = ? where %s""" % lim,
for c, id in enumerate(ids):
# start at -100000 so that reviews are all due
data.append((did, -100000+c, u, id))
- # due reviews stay in the review queue. careful: can't use
- # "odid or did", as sqlite converts to boolean
- queue = """
-(case when type=2 and (case when odue then odue <= %d else due <= %d end)
- then 2 else 0 end)"""
- queue %= (self.today, self.today)
- self.col.db.executemany("""
-update cards set
-odid = (case when odid then odid else did end),
-odue = (case when odue then odue else due end),
-did = ?, queue = %s, due = ?, usn = ? where id = ?""" % queue, data)
- def _dynIvlBoost(self, card):
- assert card.odid and card.type == 2
- assert card.factor
- elapsed = card.ivl - (card.odue - self.today)
- factor = ((card.factor/1000)+1.2)/2
- ivl = int(max(card.ivl, elapsed * factor, 1))
- conf = self._revConf(card)
- return min(conf['maxIvl'], ivl)
+ # fixme: put due reviews in new queue in the future
+ queue = """
+(case
+when queue=2 and due<=%d then queue
+when queue in (1,3) then queue
+else 0 end)"""
+ queue %= self.today
+
+ query = """
+update cards set
+odid = did, odue = due,
+did = ?, queue = %s, due = ?, usn = ? where id = ?
+""" % queue
+
+ self.col.db.executemany(query, data)
# Leeches
##########################################################################
@@ -1076,14 +1185,13 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?""" % queue, data)
return conf['new']
# dynamic deck; override some attributes, use original deck for others
oconf = self.col.decks.confForDid(card.odid)
- delays = conf['delays'] or oconf['new']['delays']
return dict(
# original deck
ints=oconf['new']['ints'],
initialFactor=oconf['new']['initialFactor'],
bury=oconf['new'].get("bury", True),
+ delays=oconf['new']['delays'],
# overrides
- delays=delays,
separate=conf['separate'],
order=NEW_CARDS_DUE,
perDay=self.reportLimit
@@ -1096,15 +1204,14 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?""" % queue, data)
return conf['lapse']
# dynamic deck; override some attributes, use original deck for others
oconf = self.col.decks.confForDid(card.odid)
- delays = conf['delays'] or oconf['lapse']['delays']
return dict(
# original deck
minInt=oconf['lapse']['minInt'],
leechFails=oconf['lapse']['leechFails'],
leechAction=oconf['lapse']['leechAction'],
mult=oconf['lapse']['mult'],
+ delays=oconf['lapse']['delays'],
# overrides
- delays=delays,
resched=conf['resched'],
)
@@ -1224,14 +1331,19 @@ To study outside of the normal schedule, click the Custom Study button below."""
def nextIvl(self, card, ease):
"Return the next interval for CARD, in seconds."
- if card.queue in (0,1,3):
+ if card.queue == 0 and card.type == 2 and ease > 1:
+ if self._resched(card):
+ return self._earlyReviewIvl(card, ease)*86400
+ else:
+ return 0
+ elif card.queue in (0,1,3):
return self._nextLrnIvl(card, ease)
elif ease == 1:
# lapsed
conf = self._lapseConf(card)
if conf['delays']:
return conf['delays'][0]*60
- return self._nextLapseIvl(card, conf)*86400
+ return self._lapseIvl(card, conf)*86400
else:
# review
return self._nextRevIvl(card, ease)*86400
@@ -1244,18 +1356,20 @@ To study outside of the normal schedule, click the Custom Study button below."""
if ease == 1:
# fail
return self._delayForGrade(conf, len(conf['delays']))
- elif ease == 3:
+ elif ease == 2:
+ return self._delayForRepeatingGrade(conf, card.left)
+ elif ease == 4:
# early removal
if not self._resched(card):
return 0
- return self._graduatingIvl(card, conf, True, adj=False) * 86400
- else:
+ return self._graduatingIvl(card, conf, True, fuzz=False) * 86400
+ else: # ease == 3
left = card.left%1000 - 1
if left <= 0:
# graduate
if not self._resched(card):
return 0
- return self._graduatingIvl(card, conf, False, adj=False) * 86400
+ return self._graduatingIvl(card, conf, False, fuzz=False) * 86400
else:
return self._delayForGrade(conf, left)
diff --git a/aqt/customstudy.py b/aqt/customstudy.py
index d16007513..97ed08085 100644
--- a/aqt/customstudy.py
+++ b/aqt/customstudy.py
@@ -133,19 +133,15 @@ class CustomStudy(QDialog):
dyn = self.mw.col.decks.get(did)
# and then set various options
if i == RADIO_FORGOT:
- dyn['delays'] = [1]
dyn['terms'][0] = ['rated:%d:1' % spin, DYN_MAX_SIZE, DYN_RANDOM]
dyn['resched'] = False
elif i == RADIO_AHEAD:
- dyn['delays'] = None
dyn['terms'][0] = ['prop:due<=%d' % spin, DYN_MAX_SIZE, DYN_DUE]
dyn['resched'] = True
elif i == RADIO_PREVIEW:
- dyn['delays'] = None
dyn['terms'][0] = ['is:new added:%s'%spin, DYN_MAX_SIZE, DYN_OLDEST]
dyn['resched'] = False
elif i == RADIO_CRAM:
- dyn['delays'] = None
type = f.cardType.currentRow()
if type == TYPE_NEW:
terms = "is:new "
diff --git a/aqt/dyndeckconf.py b/aqt/dyndeckconf.py
index cb03638c7..1fc04639d 100644
--- a/aqt/dyndeckconf.py
+++ b/aqt/dyndeckconf.py
@@ -43,12 +43,6 @@ class DeckConf(QDialog):
d = self.deck
search, limit, order = d['terms'][0]
f.search.setText(search)
- if d['delays']:
- f.steps.setText(self.listToUser(d['delays']))
- f.stepsOn.setChecked(True)
- else:
- f.steps.setText("1 10")
- f.stepsOn.setChecked(False)
f.resched.setChecked(d['resched'])
f.order.setCurrentIndex(order)
f.limit.setValue(limit)
@@ -57,12 +51,6 @@ class DeckConf(QDialog):
f = self.form
d = self.deck
d['delays'] = None
- if f.stepsOn.isChecked():
- steps = self.userToList(f.steps)
- if steps:
- d['delays'] = steps
- else:
- d['delays'] = None
d['terms'][0] = [f.search.text(),
f.limit.value(),
f.order.currentIndex()]
diff --git a/designer/dyndconf.ui b/designer/dyndconf.ui
index 39ca36f76..c0f8d935c 100644
--- a/designer/dyndconf.ui
+++ b/designer/dyndconf.ui
@@ -82,23 +82,6 @@
- -
-
-
- Custom steps (in minutes)
-
-
-
- -
-
-
- false
-
-
- 1 10
-
-
-
@@ -132,8 +115,6 @@
limit
order
resched
- stepsOn
- steps
buttonBox
@@ -170,21 +151,5 @@
-
- stepsOn
- toggled(bool)
- steps
- setEnabled(bool)
-
-
- 126
- 207
-
-
- 272
- 205
-
-
-
diff --git a/tests/test_exporting.py b/tests/test_exporting.py
index 261e27dcc..6fcb73710 100644
--- a/tests/test_exporting.py
+++ b/tests/test_exporting.py
@@ -89,8 +89,8 @@ def test_export_anki_due():
deck.crt -= 86400*10
deck.sched.reset()
c = deck.sched.getCard()
- deck.sched.answerCard(c, 2)
- deck.sched.answerCard(c, 2)
+ deck.sched.answerCard(c, 3)
+ deck.sched.answerCard(c, 3)
# should have ivl of 1, due on day 11
assert c.ivl == 1
assert c.due == 11
diff --git a/tests/test_sched.py b/tests/test_sched.py
index 669fa8a03..978e14d42 100644
--- a/tests/test_sched.py
+++ b/tests/test_sched.py
@@ -8,7 +8,6 @@ from tests.shared import getEmptyCol
from anki.utils import intTime
from anki.hooks import addHook
-
def test_clock():
d = getEmptyCol()
if (d.sched.dayCutoff - intTime()) < 10*60:
@@ -132,18 +131,18 @@ def test_learn():
t = round(c.due - time.time())
assert t >= 25 and t <= 40
# pass it once
- d.sched.answerCard(c, 2)
+ d.sched.answerCard(c, 3)
# it should by due in 3 minutes
assert round(c.due - time.time()) in (179, 180)
assert c.left%1000 == 2
assert c.left//1000 == 2
# check log is accurate
log = d.db.first("select * from revlog order by id desc")
- assert log[3] == 2
+ assert log[3] == 3
assert log[4] == -180
assert log[5] == -30
# pass again
- d.sched.answerCard(c, 2)
+ d.sched.answerCard(c, 3)
# it should by due in 10 minutes
assert round(c.due - time.time()) in (599, 600)
assert c.left%1000 == 1
@@ -151,7 +150,7 @@ def test_learn():
# the next pass should graduate the card
assert c.queue == 1
assert c.type == 1
- d.sched.answerCard(c, 2)
+ d.sched.answerCard(c, 3)
assert c.queue == 2
assert c.type == 2
# should be due tomorrow, with an interval of 1
@@ -160,29 +159,48 @@ def test_learn():
# or normal removal
c.type = 0
c.queue = 1
- d.sched.answerCard(c, 3)
+ d.sched.answerCard(c, 4)
assert c.type == 2
assert c.queue == 2
assert checkRevIvl(d, c, 4)
# revlog should have been updated each time
assert d.db.scalar("select count() from revlog where type = 0") == 5
- # now failed card handling
- c.type = 2
- c.queue = 1
- c.odue = 123
- d.sched.answerCard(c, 3)
- assert c.due == 123
- assert c.type == 2
- assert c.queue == 2
- # we should be able to remove manually, too
- c.type = 2
- c.queue = 1
- c.odue = 321
+
+def test_relearn():
+ d = getEmptyCol()
+ f = d.newNote()
+ f['Front'] = "one"
+ d.addNote(f)
+ c = f.cards()[0]
+ c.ivl = 100
+ c.due = d.sched.today
+ c.type = c.queue = 2
c.flush()
- d.sched.removeLrn()
+
+ # fail the card
+ d.reset()
+ c = d.sched.getCard()
+ d.sched.answerCard(c, 1)
+ assert c.queue == 1
+ assert c.type == 2
+ assert c.ivl == 1
+
+ # immediately graduate it
+ d.sched.answerCard(c, 4)
+ assert c.queue == c.type == 2
+ assert c.ivl == 1
+ assert c.due == d.sched.today + c.ivl
+
+ # if forced out of learning, it should have the correct due date
+ c.ivl = 100
+ c.due = d.sched.today
+ c.flush()
+ c = d.sched.getCard()
+ d.sched.answerCard(c, 1)
+ assert c.due > intTime()
+ d.sched.removeLrn([c.id])
c.load()
- assert c.queue == 2
- assert c.due == 321
+ assert c.due == d.sched.today + c.ivl
def test_learn_collapsed():
d = getEmptyCol()
@@ -200,7 +218,7 @@ def test_learn_collapsed():
c = d.sched.getCard()
assert c.q().endswith("1")
# pass it so it's due in 10 minutes
- d.sched.answerCard(c, 2)
+ d.sched.answerCard(c, 3)
# get the other card
c = d.sched.getCard()
assert c.q().endswith("2")
@@ -220,16 +238,16 @@ def test_learn_day():
c = d.sched.getCard()
d.sched._cardConf(c)['new']['delays'] = [1, 10, 1440, 2880]
# pass it
- d.sched.answerCard(c, 2)
+ d.sched.answerCard(c, 3)
# 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
+ assert ni(c, 3) == 86400
# answering it will place it in queue 3
- d.sched.answerCard(c, 2)
+ d.sched.answerCard(c, 3)
assert c.due == d.sched.today+1
assert c.queue == 3
assert not d.sched.getCard()
@@ -240,21 +258,21 @@ def test_learn_day():
assert d.sched.counts() == (0, 1, 0)
c = d.sched.getCard()
# nextIvl should work
- assert ni(c, 2) == 86400*2
+ assert ni(c, 3) == 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)
+ d.sched.answerCard(c, 3)
# 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 ni(c, 3) == 86400
+ d.sched.answerCard(c, 3)
assert c.queue == c.type == 2
# if the lapse step is tomorrow, failing it should handle the counts
# correctly
@@ -287,33 +305,11 @@ def test_reviews():
c.flush()
# save it for later use as well
cardcopy = copy.copy(c)
- # failing it should put it in the learn queue with the default options
- ##################################################
- # different delay to new
- d.reset()
- d.sched._cardConf(c)['lapse']['delays'] = [2, 20]
- d.sched.answerCard(c, 1)
- assert c.queue == 1
- # it should be due tomorrow, with an interval of 1
- assert c.odue == d.sched.today + 1
- assert c.ivl == 1
- # but because it's in the learn queue, its current due time should be in
- # the future
- assert c.due >= time.time()
- assert (c.due - time.time()) > 119
- # factor should have been decremented
- assert c.factor == 2300
- # check counters
- assert c.lapses == 2
- assert c.reps == 4
- # check ests.
- ni = d.sched.nextIvl
- assert ni(c, 1) == 120
- assert ni(c, 2) == 20*60
- # try again with an ease of 2 instead
+ # try with an ease of 2
##################################################
c = copy.copy(cardcopy)
c.flush()
+ d.reset()
d.sched.answerCard(c, 2)
assert c.queue == 2
# the new interval should be (100 + 8/4) * 1.2 = 122
@@ -448,30 +444,33 @@ def test_nextIvl():
##################################################
ni = d.sched.nextIvl
assert ni(c, 1) == 30
- assert ni(c, 2) == 180
- assert ni(c, 3) == 4*86400
+ assert ni(c, 2) == (30+180)//2
+ assert ni(c, 3) == 180
+ assert ni(c, 4) == 4*86400
d.sched.answerCard(c, 1)
# cards in learning
##################################################
assert ni(c, 1) == 30
- assert ni(c, 2) == 180
- assert ni(c, 3) == 4*86400
- d.sched.answerCard(c, 2)
+ assert ni(c, 2) == (30+180)//2
+ assert ni(c, 3) == 180
+ assert ni(c, 4) == 4*86400
+ d.sched.answerCard(c, 3)
assert ni(c, 1) == 30
- assert ni(c, 2) == 600
- assert ni(c, 3) == 4*86400
- d.sched.answerCard(c, 2)
+ assert ni(c, 2) == (180+600)//2
+ assert ni(c, 3) == 600
+ assert ni(c, 4) == 4*86400
+ d.sched.answerCard(c, 3)
# normal graduation is tomorrow
- assert ni(c, 2) == 1*86400
- assert ni(c, 3) == 4*86400
+ assert ni(c, 3) == 1*86400
+ assert ni(c, 4) == 4*86400
# lapsed cards
##################################################
c.type = 2
c.ivl = 100
c.factor = STARTING_FACTOR
assert ni(c, 1) == 60
- assert ni(c, 2) == 100*86400
assert ni(c, 3) == 100*86400
+ assert ni(c, 4) == 100*86400
# review cards
##################################################
c.queue = 2
@@ -547,7 +546,7 @@ def test_suspend():
assert c.due == 1
assert c.did == 1
-def test_cram():
+def test_filt_reviewing_early_normal():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
@@ -563,7 +562,6 @@ def test_cram():
c.flush()
d.reset()
assert d.sched.counts() == (0,0,0)
- cardcopy = copy.copy(c)
# create a dynamic deck and refresh it
did = d.decks.newDyn("Cram")
d.sched.rebuildDyn(did)
@@ -574,110 +572,62 @@ def test_cram():
assert d.sched.counts() == (1,0,0)
# grab it and check estimates
c = d.sched.getCard()
- assert d.sched.answerButtons(c) == 2
+ assert d.sched.answerButtons(c) == 4
assert d.sched.nextIvl(c, 1) == 600
- assert d.sched.nextIvl(c, 2) == 138*60*60*24
- cram = d.decks.get(did)
- cram['delays'] = [1, 10]
- assert d.sched.answerButtons(c) == 3
- assert d.sched.nextIvl(c, 1) == 60
- assert d.sched.nextIvl(c, 2) == 600
- assert d.sched.nextIvl(c, 3) == 138*60*60*24
- d.sched.answerCard(c, 2)
- # elapsed time was 75 days
- # factor = 2.5+1.2/2 = 1.85
- # int(75*1.85) = 138
- assert c.ivl == 138
- assert c.odue == 138
- assert c.queue == 1
+ assert d.sched.nextIvl(c, 2) == int(75*1.2)*86400
+ assert d.sched.nextIvl(c, 3) == int(75*2.5)*86400
+ assert d.sched.nextIvl(c, 4) == int(75*2.5*1.3)*86400
+
+ # answer 'good'
+ d.sched.answerCard(c, 3)
+ checkRevIvl(d, c, 90)
+ assert c.due == d.sched.today + c.ivl
+ assert not c.odue
+ # should not be in learning
+ assert c.queue == 2
# should be logged as a cram rep
assert d.db.scalar(
"select type from revlog order by id desc limit 1") == 3
- # check ivls again
- assert d.sched.nextIvl(c, 1) == 60
- assert d.sched.nextIvl(c, 2) == 138*60*60*24
- assert d.sched.nextIvl(c, 3) == 138*60*60*24
- # when it graduates, due is updated
- c = d.sched.getCard()
- d.sched.answerCard(c, 2)
- assert c.ivl == 138
- assert c.due == 138
- assert c.queue == 2
- # and it will have moved back to the previous deck
- assert c.did == 1
- # cram the deck again
- d.sched.rebuildDyn(did)
- d.reset()
- c = d.sched.getCard()
- # check ivls again - passing should be idempotent
- assert d.sched.nextIvl(c, 1) == 60
- assert d.sched.nextIvl(c, 2) == 600
- assert d.sched.nextIvl(c, 3) == 138*60*60*24
- d.sched.answerCard(c, 2)
- assert c.ivl == 138
- assert c.odue == 138
- # fail
- d.sched.answerCard(c, 1)
- assert d.sched.nextIvl(c, 1) == 60
- assert d.sched.nextIvl(c, 2) == 600
- assert d.sched.nextIvl(c, 3) == 86400
- # delete the deck, returning the card mid-study
- d.decks.rem(d.decks.selected())
- assert len(d.sched.deckDueList()) == 1
- c.load()
- assert c.ivl == 1
- assert c.due == d.sched.today+1
- # make it due
- d.reset()
- assert d.sched.counts() == (0,0,0)
- c.due = -5
- c.ivl = 100
- c.flush()
- d.reset()
- assert d.sched.counts() == (0,0,1)
- # cram again
- did = d.decks.newDyn("Cram")
- d.sched.rebuildDyn(did)
- d.reset()
- assert d.sched.counts() == (0,0,1)
- c.load()
- assert d.sched.answerButtons(c) == 4
- # add a sibling so we can test minSpace, etc
- c.col = None
- c2 = copy.deepcopy(c)
- c2.col = c.col = d
- c2.id = 123
- c2.ord = 1
- c2.due = 325
- c2.col = c.col
- c2.flush()
- # should be able to answer it
- c = d.sched.getCard()
- d.sched.answerCard(c, 4)
- # it should have been moved back to the original deck
- assert c.did == 1
-def test_cram_rem():
+ # due in 75 days, so it's been waiting 25 days
+ c.ivl = 100
+ c.due = d.sched.today + 75
+ c.flush()
+ d.sched.rebuildDyn(did)
+ d.reset()
+ c = d.sched.getCard()
+
+ assert d.sched.nextIvl(c, 2) == 50*86400
+ assert d.sched.nextIvl(c, 3) == 100*86400
+ assert d.sched.nextIvl(c, 4) == 101*86400
+
+def test_filt_keep_lrn_state():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
d.addNote(f)
- oldDue = f.cards()[0].due
+
+ # fail the card outside filtered deck
+ c = d.sched.getCard()
+ d.sched.answerCard(c, 1)
+
+ assert c.type == c.queue == 1
+
+ # create a dynamic deck and refresh it
did = d.decks.newDyn("Cram")
d.sched.rebuildDyn(did)
d.reset()
- c = d.sched.getCard()
- d.sched.answerCard(c, 2)
- # answering the card will put it in the learning queue
+
+ # card should still be in learning state
+ c.load()
assert c.type == c.queue == 1
- assert c.due != oldDue
- # if we terminate cramming prematurely it should be set back to new
+
+ # emptying the deck preserves learning state
d.sched.emptyDyn(did)
c.load()
- assert c.type == c.queue == 0
- assert c.due == oldDue
+ assert c.type == c.queue == 1
-def test_cram_resched():
+def test_filt_reschedoff():
# add card
d = getEmptyCol()
f = d.newNote()
@@ -693,14 +643,16 @@ def test_cram_resched():
c = d.sched.getCard()
ni = d.sched.nextIvl
assert ni(c, 1) == 60
- assert ni(c, 2) == 600
- assert ni(c, 3) == 0
- assert d.sched.nextIvlStr(c, 3) == "(end)"
- d.sched.answerCard(c, 3)
+ assert ni(c, 2) == (60+600)//2
+ assert ni(c, 3) == 600
+ assert ni(c, 4) == 0
+ assert d.sched.nextIvlStr(c, 4) == "(end)"
+ d.sched.answerCard(c, 4)
assert c.queue == c.type == 0
# undue reviews should also be unaffected
c.ivl = 100
- c.type = c.queue = 2
+ c.type = 2
+ c.queue = 0
c.due = d.sched.today + 25
c.factor = STARTING_FACTOR
c.flush()
@@ -732,7 +684,7 @@ def test_cram_resched():
d.reset()
c = d.sched.getCard()
d.sched.answerCard(c, 1)
- d.sched.answerCard(c, 3)
+ d.sched.answerCard(c, 4)
d.sched.emptyDyn(did)
c.load()
assert c.ivl == 100
@@ -769,7 +721,7 @@ def test_cram_resched():
d.reset()
c = d.sched.getCard()
d.sched.answerCard(c, 1)
- d.sched.answerCard(c, 3)
+ d.sched.answerCard(c, 4)
c.load()
assert c.ivl == 100
assert c.due == -25
@@ -840,31 +792,31 @@ def test_repCounts():
assert d.sched.counts() == (0, 2, 0)
d.sched.answerCard(d.sched.getCard(), 1)
assert d.sched.counts() == (0, 2, 0)
- d.sched.answerCard(d.sched.getCard(), 2)
+ d.sched.answerCard(d.sched.getCard(), 3)
assert d.sched.counts() == (0, 1, 0)
d.sched.answerCard(d.sched.getCard(), 1)
assert d.sched.counts() == (0, 2, 0)
- d.sched.answerCard(d.sched.getCard(), 2)
+ d.sched.answerCard(d.sched.getCard(), 3)
assert d.sched.counts() == (0, 1, 0)
- d.sched.answerCard(d.sched.getCard(), 2)
+ d.sched.answerCard(d.sched.getCard(), 3)
assert d.sched.counts() == (0, 0, 0)
f = d.newNote()
f['Front'] = "two"
d.addNote(f)
d.reset()
# initial pass should be correct too
- d.sched.answerCard(d.sched.getCard(), 2)
+ d.sched.answerCard(d.sched.getCard(), 3)
assert d.sched.counts() == (0, 1, 0)
d.sched.answerCard(d.sched.getCard(), 1)
assert d.sched.counts() == (0, 2, 0)
- d.sched.answerCard(d.sched.getCard(), 3)
+ d.sched.answerCard(d.sched.getCard(), 4)
assert d.sched.counts() == (0, 0, 0)
# immediate graduate should work
f = d.newNote()
f['Front'] = "three"
d.addNote(f)
d.reset()
- d.sched.answerCard(d.sched.getCard(), 3)
+ d.sched.answerCard(d.sched.getCard(), 4)
assert d.sched.counts() == (0, 0, 0)
# and failing a review should too
f = d.newNote()
@@ -917,7 +869,7 @@ def test_collapse():
c = d.sched.getCard()
d.sched.answerCard(c, 1)
c = d.sched.getCard()
- d.sched.answerCard(c, 3)
+ d.sched.answerCard(c, 4)
assert not d.sched.getCard()
def test_deckDue():
@@ -1001,7 +953,7 @@ def test_deckFlow():
for i in "one", "three", "two":
c = d.sched.getCard()
assert c.note()['Front'] == i
- d.sched.answerCard(c, 2)
+ d.sched.answerCard(c, 3)
def test_reorder():
d = getEmptyCol()
@@ -1111,5 +1063,7 @@ def test_failmult():
c = d.sched.getCard()
d.sched.answerCard(c, 1)
assert c.ivl == 50
+ # failing again, the actual elapsed interval is 0,
+ # so the card is reset to new
d.sched.answerCard(c, 1)
- assert c.ivl == 25
+ assert c.ivl == 1
diff --git a/tests/test_sync.py b/tests/test_sync.py
index 585e68ca9..a61c582e9 100644
--- a/tests/test_sync.py
+++ b/tests/test_sync.py
@@ -343,6 +343,7 @@ def test_filtered_delete():
nid = deck1.db.scalar("select id from notes")
note = deck1.getNote(nid)
card = note.cards()[0]
+ card.queue = 2
card.type = 2
card.ivl = 10
card.factor = STARTING_FACTOR
diff --git a/tests/test_undo.py b/tests/test_undo.py
index a9b06b34c..457112987 100644
--- a/tests/test_undo.py
+++ b/tests/test_undo.py
@@ -47,7 +47,7 @@ def test_review():
assert d.sched.counts() == (1, 0, 0)
c = d.sched.getCard()
assert c.queue == 0
- d.sched.answerCard(c, 2)
+ d.sched.answerCard(c, 3)
assert c.left == 1001
assert d.sched.counts() == (0, 1, 0)
assert c.queue == 1
@@ -67,9 +67,9 @@ def test_review():
d.reset()
assert d.sched.counts() == (2, 0, 0)
c = d.sched.getCard()
- d.sched.answerCard(c, 2)
+ d.sched.answerCard(c, 3)
c = d.sched.getCard()
- d.sched.answerCard(c, 2)
+ d.sched.answerCard(c, 3)
assert d.sched.counts() == (0, 2, 0)
d.undo()
d.reset()
@@ -79,7 +79,7 @@ def test_review():
assert d.sched.counts() == (2, 0, 0)
# performing a normal op will clear the review queue
c = d.sched.getCard()
- d.sched.answerCard(c, 2)
+ d.sched.answerCard(c, 3)
assert d.undoName() == "Review"
d.save("foo")
assert d.undoName() == "foo"