diff options
author | Joris | 2021-11-14 23:25:55 +0100 |
---|---|---|
committer | Joris | 2021-11-19 11:42:20 +0100 |
commit | 9f94611a42d41cf94cdccb00b5d2eec0d5d02970 (patch) | |
tree | 9bab5bc342e22aa38b13a2dbd3525bbfe2beedb5 /src/space_repetition.rs | |
parent | 59c44b15010eea5490896a5b5d427b415ad6f56a (diff) |
Add initial working version
Diffstat (limited to 'src/space_repetition.rs')
-rw-r--r-- | src/space_repetition.rs | 361 |
1 files changed, 361 insertions, 0 deletions
diff --git a/src/space_repetition.rs b/src/space_repetition.rs new file mode 100644 index 0000000..25cae7f --- /dev/null +++ b/src/space_repetition.rs @@ -0,0 +1,361 @@ +// SM2-Anki +// https://gist.github.com/riceissa/1ead1b9881ffbb48793565ce69d7dbdd + +use crate::model::difficulty::{Difficulty, Difficulty::*}; +use serde::{Deserialize, Serialize}; + +// Learning +const LEARNING_INTERVALS: [f32; 2] = [ + 1.0 / 60.0 / 24.0, // 1 minute + 10.0 / 60.0 / 24.0, // 10 minutes +]; + +// Ease +const EASE_INIT: f32 = 2.5; +const EASE_MIN: f32 = 1.3; + +// Interval +const INTERVAL_INIT: f32 = 1.0; +const INTERVAL_INIT_EASY: f32 = 4.0; +const INTERVAL_MIN: f32 = 0.1; +const INTERVAL_MAX: f32 = 36500.0; + +// Learned +const EASE_AGAIN_SUB: f32 = 0.2; +const EASE_HARD_SUB: f32 = 0.15; +const EASE_EASY_ADD: f32 = 0.15; +const INTERVAL_AGAIN_MUL: f32 = 0.7; +const INTERVAL_HARD_MUL: f32 = 1.2; +const INTERVAL_EASY_MUL: f32 = 1.3; + +// Relearning +const RELEARNING_INTERVALS: [f32; 1] = [ + 10.0 / 60.0 / 24.0, // 10 minutes +]; + +#[derive(Debug, PartialEq, Deserialize, Serialize)] +pub enum State { + Learning { + step: usize, + }, + Learned { + ease: f32, // ratio + interval: f32, // in days + }, + Relearning { + step: usize, + ease: f32, + interval: f32, + }, +} + +pub fn init() -> State { + State::Learning { step: 0 } +} + +impl State { + pub fn get_interval_seconds(&self) -> u64 { + let days = match self { + State::Learning { step } => LEARNING_INTERVALS[*step], + State::Learned { interval, .. } => *interval, + State::Relearning { step, .. } => RELEARNING_INTERVALS[*step], + }; + (days * 24.0 * 60.0 * 60.0).round() as u64 + } + + pub fn difficulties(&self) -> Vec<Difficulty> { + match self { + State::Learning { .. } => [Again, Good, Easy].to_vec(), + State::Learned { .. } => [Again, Hard, Good, Easy].to_vec(), + State::Relearning { .. } => [Again, Good].to_vec(), + } + } +} + +pub fn update(state: State, difficulty: Difficulty) -> State { + match state { + State::Learning { step } => match difficulty { + Again => State::Learning { step: 0 }, + Good => { + let new_step = step + 1; + if new_step < LEARNING_INTERVALS.len() { + State::Learning { step: new_step } + } else { + State::Learned { + ease: EASE_INIT, + interval: INTERVAL_INIT, + } + } + } + Easy => State::Learned { + ease: EASE_INIT, + interval: INTERVAL_INIT_EASY, + }, + _ => panic!("Learning is incompatible with {:?}", difficulty), + }, + State::Learned { ease, interval } => match difficulty { + Again => State::Relearning { + step: 0, + ease: clamp_ease(ease - EASE_AGAIN_SUB), + interval: clamp_interval(interval * INTERVAL_AGAIN_MUL), + }, + Hard => State::Learned { + ease: clamp_ease(ease - EASE_HARD_SUB), + interval: clamp_interval(interval * INTERVAL_HARD_MUL), + }, + Good => State::Learned { + ease, + interval: clamp_interval(interval * ease), + }, + Easy => State::Learned { + ease: clamp_ease(ease + EASE_EASY_ADD), + interval: clamp_interval(interval * ease * INTERVAL_EASY_MUL), + }, + }, + State::Relearning { + step, + ease, + interval, + } => match difficulty { + Again => State::Relearning { + step: 0, + ease, + interval, + }, + Good => { + let new_step = step + 1; + if new_step < RELEARNING_INTERVALS.len() { + State::Relearning { + step: new_step, + ease, + interval, + } + } else { + State::Learned { ease, interval } + } + } + _ => panic!("Relearning is incompatible with {:?}.", difficulty), + }, + } +} + +fn clamp_ease(f: f32) -> f32 { + if f < EASE_MIN { + EASE_MIN + } else { + f + } +} + +fn clamp_interval(i: f32) -> f32 { + if i < INTERVAL_MIN { + INTERVAL_MIN + } else if i > INTERVAL_MAX { + INTERVAL_MAX + } else { + i + } +} + +#[cfg(test)] +mod tests { + use super::{State::*, *}; + + #[test] + fn learning_again() { + assert_eq!(update(Learning { step: 1 }, Again), Learning { step: 0 }); + } + + #[test] + fn learning_good() { + assert_eq!(update(Learning { step: 0 }, Good), Learning { step: 1 }); + + assert_eq!( + update( + Learning { + step: LEARNING_INTERVALS.len() - 1 + }, + Good + ), + Learned { + ease: EASE_INIT, + interval: INTERVAL_INIT + } + ); + } + + #[test] + fn learning_easy() { + assert_eq!( + update(Learning { step: 0 }, Easy), + Learned { + ease: EASE_INIT, + interval: INTERVAL_INIT_EASY + } + ); + } + + #[test] + fn learned_again() { + assert_eq!( + update( + Learned { + ease: EASE_MIN, + interval: INTERVAL_MIN + }, + Again + ), + Relearning { + step: 0, + ease: EASE_MIN, + interval: INTERVAL_MIN + } + ); + + assert_eq!( + update( + Learned { + ease: EASE_INIT, + interval: INTERVAL_INIT + }, + Again + ), + Relearning { + step: 0, + ease: EASE_INIT - EASE_AGAIN_SUB, + interval: INTERVAL_INIT * INTERVAL_AGAIN_MUL + } + ); + } + + #[test] + fn learned_hard() { + assert_eq!( + update( + Learned { + ease: EASE_MIN, + interval: INTERVAL_MAX + }, + Hard + ), + Learned { + ease: EASE_MIN, + interval: INTERVAL_MAX + } + ); + + assert_eq!( + update( + Learned { + ease: EASE_INIT, + interval: INTERVAL_INIT + }, + Hard + ), + Learned { + ease: EASE_INIT - EASE_HARD_SUB, + interval: INTERVAL_INIT * INTERVAL_HARD_MUL + } + ); + } + + #[test] + fn learned_good() { + assert_eq!( + update( + Learned { + ease: EASE_INIT, + interval: INTERVAL_MAX + }, + Good + ), + Learned { + ease: EASE_INIT, + interval: INTERVAL_MAX + } + ); + + assert_eq!( + update( + Learned { + ease: EASE_INIT, + interval: INTERVAL_INIT + }, + Good + ), + Learned { + ease: EASE_INIT, + interval: INTERVAL_INIT * EASE_INIT + } + ); + } + + #[test] + fn learned_easy() { + assert_eq!( + update( + Learned { + ease: EASE_INIT, + interval: INTERVAL_MAX + }, + Easy + ), + Learned { + ease: EASE_INIT + EASE_EASY_ADD, + interval: INTERVAL_MAX + } + ); + + assert_eq!( + update( + Learned { + ease: EASE_INIT, + interval: INTERVAL_INIT + }, + Easy + ), + Learned { + ease: EASE_INIT + EASE_EASY_ADD, + interval: INTERVAL_INIT * EASE_INIT * INTERVAL_EASY_MUL + } + ); + } + + #[test] + fn relearning_again() { + let ease = EASE_INIT + EASE_EASY_ADD; + let interval = INTERVAL_INIT * ease; + assert_eq!( + update( + Relearning { + step: 1, + ease, + interval, + }, + Again + ), + Relearning { + step: 0, + ease, + interval + } + ); + } + + #[test] + fn relearning_good() { + let ease = EASE_INIT + EASE_EASY_ADD; + let interval = INTERVAL_INIT * ease; + assert_eq!( + update( + Relearning { + step: RELEARNING_INTERVALS.len() - 1, + ease, + interval, + }, + Good + ), + Learned { ease, interval } + ); + } +} |