Skip to content

Instantly share code, notes, and snippets.

@ctrngk
Forked from riceissa/anki_algorithm.py
Last active October 1, 2020 02:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ctrngk/fb29b39918b86c54067712c8b1d2e36f to your computer and use it in GitHub Desktop.
Save ctrngk/fb29b39918b86c54067712c8b1d2e36f to your computer and use it in GitHub Desktop.
my current understanding of Anki's spacing algorithm
// 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
//```
"""
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