aboutsummaryrefslogtreecommitdiff
path: root/src/space_repetition.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/space_repetition.rs')
-rw-r--r--src/space_repetition.rs361
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 }
+ );
+ }
+}