diff --git a/anki/cards.py b/anki/cards.py index 01047330e..96e2b2109 100644 --- a/anki/cards.py +++ b/anki/cards.py @@ -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'] diff --git a/anki/collection.py b/anki/collection.py index 7d9d79215..964bcd663 100644 --- a/anki/collection.py +++ b/anki/collection.py @@ -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 ########################################################################## diff --git a/anki/cram.py b/anki/cram.py deleted file mode 100644 index 1155894e1..000000000 --- a/anki/cram.py +++ /dev/null @@ -1,116 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright: Damien Elmes -# 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) diff --git a/anki/find.py b/anki/find.py index 0cd3e08d3..321d700db 100644 --- a/anki/find.py +++ b/anki/find.py @@ -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 diff --git a/anki/sched.py b/anki/sched.py index d82afa193..1fa3c68cc 100644 --- a/anki/sched.py +++ b/anki/sched.py @@ -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 ########################################################################## diff --git a/tests/test_sched.py b/tests/test_sched.py index 5d82704bc..397372c42 100644 --- a/tests/test_sched.py +++ b/tests/test_sched.py @@ -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()