""" This is my understanding of the Anki scheduling algorithm, which I mostly got from watching https://www.youtube.com/watch?v=lz60qTP2Gx0 and https://www.youtube.com/watch?v=1XaJjbCSXT0 and from reading https://faqs.ankiweb.net/what-spaced-repetition-algorithm.html There is also https://github.com/dae/anki/blob/master/anki/sched.py but I find it really hard to understand. Things I don't bother to implement here: the random fudge factor (that Anki uses to decorrelate cards that were added on the same day and have the same responses throughout their history), leech tracking, checking if a card from the same notes has been reviewed already that day, delay in response (i.e. I assume all cards are reviewed exactly on the day they are due). Update (2023-12-15): Please note that the Anki review algorithm has possibly changed in many ways since the time when I wrote this program (although I believe that Anki still uses SM2 by default, so the basic concepts should still be the same as what is shown below). I have sadly not had the time or energy to keep up with the latest changes. In particular, Anki now supports FSRS instead of the SM2 algorithm (which is the algorithm below); FSRS is not covered at all below. """ # "New Cards" tab NEW_STEPS = [1, 10] # in minutes GRADUATING_INTERVAL = 1 # in days EASY_INTERVAL = 4 # in days STARTING_EASE = 250 # in percent # "Reviews" tab EASY_BONUS = 130 # in percent INTERVAL_MODIFIER = 100 # in percent MAXIMUM_INTERVAL = 36500 # in days # "Lapses" tab LAPSES_STEPS = [10] # in minutes NEW_INTERVAL = 70 # in percent MINIMUM_INTERVAL = 1 # in days class Card: def __init__(self): self.status = 'learning' # can be 'learning', 'learned', or 'relearning' self.steps_index = 0 self.ease_factor = STARTING_EASE self.interval = None def __repr__(self): return "Card[%s; steps_idx=%s; ease=%s; interval=%s]" % (self.status, self.steps_index, self.ease_factor, str(self.interval)) def schedule(card, response): '''response is one of "again", "hard", "good", or "easy" returns a result in days''' if card.status == 'learning': # for learning cards, there is no "hard" response possible if response == "again": card.steps_index = 0 return minutes_to_days(NEW_STEPS[card.steps_index]) elif response == "good": card.steps_index += 1 if card.steps_index < len(NEW_STEPS): return minutes_to_days(NEW_STEPS[card.steps_index]) else: # we have graduated! card.status = 'learned' card.interval = GRADUATING_INTERVAL return card.interval elif response == "easy": card.status = 'learned' card.interval = EASY_INTERVAL return EASY_INTERVAL else: raise ValueError("you can't press this button / we don't know how to deal with this case") elif card.status == 'learned': if response == "again": card.status = 'relearning' card.steps_index = 0 card.ease_factor = max(130, card.ease_factor - 20) card.interval = max(MINIMUM_INTERVAL, card.interval * NEW_INTERVAL/100) return minutes_to_days(LAPSES_STEPS[0]) elif response == "hard": card.ease_factor = max(130, card.ease_factor - 15) card.interval = card.interval * 1.2 * INTERVAL_MODIFIER/100 return min(MAXIMUM_INTERVAL, card.interval) elif response == "good": card.interval = (card.interval * card.ease_factor/100 * INTERVAL_MODIFIER/100) return min(MAXIMUM_INTERVAL, card.interval) elif response == "easy": card.ease_factor += 15 card.interval = (card.interval * card.ease_factor/100 * INTERVAL_MODIFIER/100 * EASY_BONUS/100) return min(MAXIMUM_INTERVAL, card.interval) else: raise ValueError("you can't press this button / we don't know how to deal with this case") elif card.status == 'relearning': if response == "again": card.steps_index = 0 return minutes_to_days(LAPSES_STEPS[0]) elif response == "good": card.steps_index += 1 if card.steps_index < len(LAPSES_STEPS): return minutes_to_days(LAPSES_STEPS[card.steps_index]) else: # we have re-graduated! card.status = 'learned' # we don't modify the interval here because that was already done when # going from 'learned' to 'relearning' return card.interval else: raise ValueError("you can't press this button / we don't know how to deal with this case") def minutes_to_days(minutes): return minutes / (60 * 24) def human_friendly_time(days): if not days: return days if days < 1: return str(round(days * 24 * 60, 2)) + " minutes" elif days < 30: return str(round(days, 2)) + " days" elif days < 365: return str(round(days / (365.25 / 12), 2)) + " months" else: return str(round(days / 365.25, 2)) + " years" card1 = Card() # responses = ["good", "good", "good", "again", "good", "good", "good"] responses = ["good"] * 10 for r in responses: print(str(card1) + " [%s]" % r, end="→ ") t = schedule(card1, r) print(human_friendly_time(t), card1)