// 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 (in days) 10.0 / 60.0 / 24.0, // 10 minutes (in days) ]; // 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 (in days) ]; #[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 { 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 } ); } }