-
-
Save ctrngk/fb29b39918b86c54067712c8b1d2e36f to your computer and use it in GitHub Desktop.
my current understanding of Anki's spacing algorithm
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// https://gist.github.com/riceissa/1ead1b9881ffbb48793565ce69d7dbdd | |
// "New cards" tab | |
const NEW_STEPS = [15, 25, 35] // in minutes | |
const GRADUATING_INTERVAL = 15 // in days | |
const EASY_INTERVAL = 4 // in days | |
const STARTING_EASE = 2.50 // in percent | |
// "Reviews" tab | |
const EASY_BONUS = 1.30 | |
const INTERVAL_MODIFIER = 1 | |
const MAXIMUM_INTERVAL = 36500 // in days | |
// "Lapses" tab | |
const LAPSES_STEPS = [20] // in minutes | |
const NEW_INTERVAL = 0.70 | |
const MINIMUM_INTERVAL = 2 // in days | |
class Card { | |
constructor() { | |
this.status = 'learning' // can be 'learning', 'learned', or 'relearning' | |
this.steps_index = 0 | |
this.ease_factor = STARTING_EASE | |
this.interval = null | |
this.history = [] | |
this.repr = this.repr.bind(this); | |
this.choice = this.choice.bind(this); | |
this.minutes_to_days = this.minutes_to_days.bind(this); | |
this.prompt = this.prompt.bind(this); | |
} | |
repr() { | |
return `Card[${this.status}; steps_idx=${this.steps_index}; ease=${this.ease_factor}; interval=${this.interval}]` | |
} | |
choice(button) { | |
//button is one of "wrong", "hard", "good", or "easy" | |
// returns a result in days | |
this.history.push(button) | |
if (this.status === 'learning') { | |
// for learning cards, there is no "hard" response possible | |
if (button === 'wrong') { | |
this.steps_index = 0 | |
return this.minutes_to_days(NEW_STEPS[this.steps_index]) | |
} | |
else if (button === 'good') { | |
this.steps_index += 1 | |
if (this.steps_index < NEW_STEPS.length) { | |
return this.minutes_to_days(NEW_STEPS[this.steps_index]) | |
} else { | |
// we have graduated! | |
this.status = 'learned' | |
this.interval = GRADUATING_INTERVAL | |
return this.interval | |
} | |
} | |
else if (button === 'easy') { | |
this.status = 'learned' | |
this.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") | |
} | |
} | |
else if (this.status === 'learned') { | |
if (button === "wrong") { | |
this.status = 'relearning' | |
this.steps_index = 0 | |
this.ease_factor = Math.max(1.30, this.ease_factor - 0.20) | |
// the anki manual says "the current interval is multiplied by the | |
// value of new interval", but I have no idea what the "new | |
// interval" is | |
return this.minutes_to_days(LAPSES_STEPS[0]) | |
} | |
else if (button === 'hard') { | |
this.ease_factor = Math.max(1.30, this.ease_factor - 0.15) | |
this.interval = this.interval * 1.2 * INTERVAL_MODIFIER | |
return Math.min(MAXIMUM_INTERVAL, this.interval) | |
} | |
else if (button === 'good') { | |
this.interval = (this.interval * this.ease_factor | |
* INTERVAL_MODIFIER) | |
return Math.min(MAXIMUM_INTERVAL, this.interval) | |
} | |
else if (button === 'easy') { | |
this.ease_factor += 0.15 | |
this.interval = (this.interval * this.ease_factor | |
* INTERVAL_MODIFIER * EASY_BONUS) | |
return Math.min(MAXIMUM_INTERVAL, this.interval) | |
} | |
else { | |
// raise ValueError("you can't press this button / we don't know how to deal with this case") | |
} | |
} | |
else if (this.status === 'relearning') { | |
if (button === "wrong") { | |
this.steps_index = 0 | |
return this.minutes_to_days(LAPSES_STEPS[0]) | |
} | |
else if (button === "good") { | |
this.steps_index += 1 | |
if (this.steps_index < LAPSES_STEPS.length) { | |
return this.minutes_to_days(LAPSES_STEPS[this.steps_index]) | |
} | |
else { | |
// we have re-graduated! | |
this.status = 'learned' | |
this.interval = Math.max(MINIMUM_INTERVAL, this.interval * NEW_INTERVAL) | |
return this.interval | |
} | |
} else { | |
// raise ValueError("you can't press this button / we don't know how to deal with this case") | |
} | |
} | |
} | |
minutes_to_days(minutes) { | |
return minutes / (60 * 24) | |
} | |
prompt() { | |
const promt_pp = (ivl, s) => { | |
if (ivl) { | |
if (ivl <= 1) | |
return `${s} ${ivl*1440}m` | |
else | |
return `${s} ${ivl.toFixed(2)}d` | |
} | |
} | |
let c = new Card() | |
let wrong_ivl = [...this.history, 'wrong'].map(x => c.choice(x)).pop() | |
c = new Card() | |
let hard_ivl = [...this.history, 'hard'].map(x => c.choice(x)).pop() | |
c = new Card() | |
let good_ivl = [...this.history, 'good'].map(x => c.choice(x)).pop() | |
c = new Card() | |
let easy_ivl = [...this.history, 'easy'].map(x => c.choice(x)).pop() | |
const s = [ | |
promt_pp(wrong_ivl, "wrong"), | |
promt_pp(hard_ivl, "hard"), | |
promt_pp(good_ivl, "good"), | |
promt_pp(easy_ivl, "easy") | |
].filter(x => x !== undefined).join(" | ") | |
return s | |
} | |
} | |
// a = new Card() | |
// console.log(a.prompt()) | |
// a.choice("good") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("good") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("good") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("good") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("good") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("good") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("good") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// | |
// | |
// console.log() | |
// a = new Card() | |
// console.log(a.prompt()) | |
// a.choice("good") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("good") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("wrong") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("good") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("good") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("good") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("wrong") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("wrong") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("wrong") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("good") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("wrong") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("good") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("wrong") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("good") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("wrong") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("good") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("wrong") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("good") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("wrong") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("good") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("wrong") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("good") | |
// console.log(a.repr()) | |
// console.log(a.prompt()) | |
// a.choice("wrong") | |
// console.log(a.repr()) | |
// ```bash | |
//node anki_algorithm.js | |
//``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
https://gist.github.com/riceissa/1ead1b9881ffbb48793565ce69d7dbdd | |
""" | |
# "New Cards" tab | |
NEW_STEPS = [15, 25, 35] # in minutes | |
GRADUATING_INTERVAL = 15 # in days | |
EASY_INTERVAL = 4 # in days | |
STARTING_EASE = 2.50 # in percent | |
# "Reviews" tab | |
EASY_BONUS = 1.30 | |
INTERVAL_MODIFIER = 1 | |
MAXIMUM_INTERVAL = 36500 # in days | |
# "Lapses" tab | |
LAPSES_STEPS = [20] # in minutes | |
NEW_INTERVAL = 0.70 | |
MINIMUM_INTERVAL = 2 # 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 | |
self.history = [] | |
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 choice(self, button: str): | |
'''button is one of "wrong", "hard", "good", or "easy" | |
returns a result in days''' | |
self.history.append(button) | |
if self.status == 'learning': | |
# for learning cards, there is no "hard" response possible | |
if button == "wrong": | |
self.steps_index = 0 | |
return self.minutes_to_days(NEW_STEPS[self.steps_index]) | |
elif button == "good": | |
self.steps_index += 1 | |
if self.steps_index < len(NEW_STEPS): | |
return self.minutes_to_days(NEW_STEPS[self.steps_index]) | |
else: | |
# we have graduated! | |
self.status = 'learned' | |
self.interval = GRADUATING_INTERVAL | |
return self.interval | |
elif button == "easy": | |
self.status = 'learned' | |
self.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") | |
return | |
elif self.status == 'learned': | |
if button == "wrong": | |
self.status = 'relearning' | |
self.steps_index = 0 | |
self.ease_factor = max(1.30, self.ease_factor - 0.20) | |
# the anki manual says "the current interval is multiplied by the | |
# value of new interval", but I have no idea what the "new | |
# interval" is | |
return self.minutes_to_days(LAPSES_STEPS[0]) | |
elif button == "hard": | |
self.ease_factor = max(1.30, self.ease_factor - 0.15) | |
self.interval = self.interval * 1.2 * INTERVAL_MODIFIER | |
return min(MAXIMUM_INTERVAL, self.interval) | |
elif button == "good": | |
self.interval = (self.interval * self.ease_factor | |
* INTERVAL_MODIFIER) | |
return min(MAXIMUM_INTERVAL, self.interval) | |
elif button == "easy": | |
self.ease_factor += 0.15 | |
self.interval = (self.interval * self.ease_factor | |
* INTERVAL_MODIFIER * EASY_BONUS) | |
return min(MAXIMUM_INTERVAL, self.interval) | |
else: | |
# raise ValueError("you can't press this button / we don't know how to deal with this case") | |
return | |
elif self.status == 'relearning': | |
if button == "wrong": | |
self.steps_index = 0 | |
return self.minutes_to_days(LAPSES_STEPS[0]) | |
elif button == "good": | |
self.steps_index += 1 | |
if self.steps_index < len(LAPSES_STEPS): | |
return self.minutes_to_days(LAPSES_STEPS[self.steps_index]) | |
else: | |
# we have re-graduated! | |
self.status = 'learned' | |
self.interval = max(MINIMUM_INTERVAL, self.interval * NEW_INTERVAL) | |
return self.interval | |
else: | |
# raise ValueError("you can't press this button / we don't know how to deal with this case") | |
return | |
def minutes_to_days(self, minutes): | |
return minutes / (60 * 24) | |
@property | |
def prompt(self): | |
c = Card() | |
wrong_ivl = [c.choice(b) for b in self.history + ["wrong"]][-1] | |
c = Card() | |
hard_ivl = [c.choice(b) for b in self.history + ["hard"]][-1] | |
c = Card() | |
good_ivl = [c.choice(b) for b in self.history + ["good"]][-1] | |
c = Card() | |
easy_ivl = [c.choice(b) for b in self.history + ["easy"]][-1] | |
def prompt_pp(ivl, s: str): | |
if ivl: | |
if ivl <= 1: | |
return f'{s} {ivl * 1440}m' | |
else: | |
return f'{s} {round(ivl, 2)}d' | |
s = " | ".join(filter(None, | |
[ | |
prompt_pp(wrong_ivl, "wrong"), | |
prompt_pp(hard_ivl, "hard"), | |
prompt_pp(good_ivl, "good"), | |
prompt_pp(easy_ivl, "easy") | |
])) | |
return s | |
# python | |
# >>> from anki_algorithm import * | |
# >>> a = Card() | |
# >>> a.prompt | |
# >>> a.choice("good") | |
# >>> a.choice("good") | |
# >>> a.prompt | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment