diff --git a/anki/sched.py b/anki/sched.py index 18a463612..81541c025 100644 --- a/anki/sched.py +++ b/anki/sched.py @@ -18,6 +18,7 @@ from anki.hooks import runHook class Scheduler(object): name = "std" haveCustomStudy = True + _spreadRev = True def __init__(self, col): self.col = col @@ -873,6 +874,25 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)""" # interval capped? return min(interval, conf['maxIvl']) + def _fuzzedIvl(self, ivl): + min, max = self._fuzzIvlRange(ivl) + return random.randint(min, max) + + def _fuzzIvlRange(self, ivl): + if ivl < 2: + return [1, 1] + elif ivl == 2: + return [2, 3] + elif ivl < 7: + fuzz = int(ivl*0.25) + elif ivl < 30: + fuzz = max(2, int(ivl*0.15)) + else: + fuzz = max(4, int(ivl*0.05)) + # fuzz at least a day + fuzz = max(fuzz, 1) + return [ivl-fuzz, ivl+fuzz] + def _constrainedIvl(self, ivl, conf, prev): "Integer interval after interval factor and prev+1 constraints applied." new = ivl * conf.get('ivlFct', 1) @@ -890,6 +910,8 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)""" def _adjRevIvl(self, card, idealIvl): "Given IDEALIVL, return an IVL away from siblings." + if self._spreadRev: + idealIvl = self._fuzzedIvl(idealIvl) idealDue = self.today + idealIvl conf = self._revConf(card) # find sibling positions diff --git a/tests/test_sched.py b/tests/test_sched.py index 921a7fca0..53a1e1405 100644 --- a/tests/test_sched.py +++ b/tests/test_sched.py @@ -5,6 +5,10 @@ from tests.shared import getEmptyDeck from anki.utils import intTime from anki.hooks import addHook +def checkRevIvl(d, targetIvl): + min, max = d.sched._fuzzIvlRange(targetIvl) + return min <= targetIvl <= max + def test_basics(): d = getEmptyDeck() d.reset() @@ -148,7 +152,7 @@ def test_learn(): d.sched.answerCard(c, 3) assert c.type == 2 assert c.queue == 2 - assert c.ivl == 4 + assert checkRevIvl(d, 4) # revlog should have been updated each time assert d.db.scalar("select count() from revlog where type = 0") == 5 # now failed card handling @@ -302,8 +306,8 @@ def test_reviews(): d.sched.answerCard(c, 2) assert c.queue == 2 # the new interval should be (100 + 8/4) * 1.2 = 122 - assert c.ivl == 122 - assert c.due == d.sched.today + 122 + assert checkRevIvl(d, 122) + assert c.due == d.sched.today + c.ivl # factor should have been decremented assert c.factor == 2350 # check counters @@ -315,8 +319,8 @@ def test_reviews(): c.flush() d.sched.answerCard(c, 3) # the new interval should be (100 + 8/2) * 2.5 = 260 - assert c.ivl == 260 - assert c.due == d.sched.today + 260 + assert checkRevIvl(d, 260) + assert c.due == d.sched.today + c.ivl # factor should have been left alone assert c.factor == 2500 # ease 4 @@ -325,8 +329,8 @@ def test_reviews(): c.flush() d.sched.answerCard(c, 4) # the new interval should be (100 + 8) * 2.5 * 1.3 = 351 - assert c.ivl == 351 - assert c.due == d.sched.today + 351 + assert checkRevIvl(d, 351) + assert c.due == d.sched.today + c.ivl # factor should have been increased assert c.factor == 2650 # leech handling @@ -767,6 +771,7 @@ def test_cram_resched(): def test_adjIvl(): d = getEmptyDeck() + d.sched._spreadRev = False # add two more templates and set second active m = d.models.current(); mm = d.models t = mm.newTemplate("Reverse")