diff --git a/Cargo.lock b/Cargo.lock index 8ea4fbb1c..f0673c568 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1862,9 +1862,9 @@ dependencies = [ [[package]] name = "fsrs" -version = "1.2.4" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6c3e9d1ab337e63735c4ceff913b9818eeac054d8dc17ca1b259195a7fbfb16" +checksum = "2434366942bf285f3c0691e68d731e56f4e1fc1d8ec7b6a0e9411e94eda6ffbd" dependencies = [ "burn", "itertools 0.12.1", diff --git a/Cargo.toml b/Cargo.toml index 06d7375f0..78552be52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ git = "https://github.com/ankitects/linkcheck.git" rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca" [workspace.dependencies.fsrs] -version = "1.2.4" +version = "=1.3.1" # git = "https://github.com/open-spaced-repetition/fsrs-rs.git" # rev = "58ca25ed2bc4bb1dc376208bbcaed7f5a501b941" # path = "../open-spaced-repetition/fsrs-rs" diff --git a/cargo/licenses.json b/cargo/licenses.json index 212b858ea..eb75a72e7 100644 --- a/cargo/licenses.json +++ b/cargo/licenses.json @@ -1225,7 +1225,7 @@ }, { "name": "fsrs", - "version": "1.2.4", + "version": "1.3.1", "authors": "Open Spaced Repetition", "repository": "https://github.com/open-spaced-repetition/fsrs-rs", "license": "BSD-3-Clause", diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index 2bc6a279b..ce20d8925 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -105,8 +105,7 @@ impl Collection { Some(state.stability), card.desired_retention.unwrap(), 0, - ) - as f32; + ); card.interval = with_review_fuzz( card.get_fuzz_factor(true), interval, diff --git a/rslib/src/scheduler/states/learning.rs b/rslib/src/scheduler/states/learning.rs index 435a3bb04..83a2f7b32 100644 --- a/rslib/src/scheduler/states/learning.rs +++ b/rslib/src/scheduler/states/learning.rs @@ -48,18 +48,33 @@ impl LearnState { .into() } else { let (minimum, maximum) = ctx.min_and_max_review_intervals(1); - let interval = if let Some(states) = &ctx.fsrs_next_states { - states.again.interval + let (interval, short_term) = if let Some(states) = &ctx.fsrs_next_states { + (states.again.interval, states.again.interval < 0.5) } else { - ctx.graduating_interval_good + (ctx.graduating_interval_good as f32, false) }; - ReviewState { - scheduled_days: ctx.with_review_fuzz(interval as f32, minimum, maximum), - ease_factor: ctx.initial_ease_factor, - memory_state, - ..Default::default() + + if short_term { + LearnState { + remaining_steps: ctx.steps.remaining_for_failed(), + scheduled_secs: (interval * 86_400.0) as u32, + elapsed_secs: 0, + memory_state, + } + .into() + } else { + ReviewState { + scheduled_days: ctx.with_review_fuzz( + interval.round().max(1.0), + minimum, + maximum, + ), + ease_factor: ctx.initial_ease_factor, + memory_state, + ..Default::default() + } + .into() } - .into() } } @@ -75,18 +90,33 @@ impl LearnState { .into() } else { let (minimum, maximum) = ctx.min_and_max_review_intervals(1); - let interval = if let Some(states) = &ctx.fsrs_next_states { - states.hard.interval + let (interval, short_term) = if let Some(states) = &ctx.fsrs_next_states { + (states.hard.interval, states.hard.interval < 0.5) } else { - ctx.graduating_interval_good + (ctx.graduating_interval_good as f32, false) }; - ReviewState { - scheduled_days: ctx.with_review_fuzz(interval as f32, minimum, maximum), - ease_factor: ctx.initial_ease_factor, - memory_state, - ..Default::default() + + if short_term { + LearnState { + scheduled_secs: (interval * 86_400.0) as u32, + elapsed_secs: 0, + memory_state, + ..self + } + .into() + } else { + ReviewState { + scheduled_days: ctx.with_review_fuzz( + interval.round().max(1.0), + minimum, + maximum, + ), + ease_factor: ctx.initial_ease_factor, + memory_state, + ..Default::default() + } + .into() } - .into() } } @@ -102,27 +132,42 @@ impl LearnState { .into() } else { let (minimum, maximum) = ctx.min_and_max_review_intervals(1); - let interval = if let Some(states) = &ctx.fsrs_next_states { - states.good.interval + let (interval, short_term) = if let Some(states) = &ctx.fsrs_next_states { + (states.good.interval, states.good.interval < 0.5) } else { - ctx.graduating_interval_good + (ctx.graduating_interval_good as f32, false) }; - ReviewState { - scheduled_days: ctx.with_review_fuzz(interval as f32, minimum, maximum), - ease_factor: ctx.initial_ease_factor, - memory_state, - ..Default::default() + + if short_term { + LearnState { + scheduled_secs: (interval * 86_400.0) as u32, + elapsed_secs: 0, + memory_state, + ..self + } + .into() + } else { + ReviewState { + scheduled_days: ctx.with_review_fuzz( + interval.round().max(1.0), + minimum, + maximum, + ), + ease_factor: ctx.initial_ease_factor, + memory_state, + ..Default::default() + } + .into() } - .into() } } fn answer_easy(self, ctx: &StateContext) -> ReviewState { let (mut minimum, maximum) = ctx.min_and_max_review_intervals(1); let interval = if let Some(states) = &ctx.fsrs_next_states { - let good = ctx.with_review_fuzz(states.good.interval as f32, minimum, maximum); + let good = ctx.with_review_fuzz(states.good.interval, minimum, maximum); minimum = good + 1; - states.easy.interval + states.easy.interval.round().max(1.0) as u32 } else { ctx.graduating_interval_easy }; diff --git a/rslib/src/scheduler/states/relearning.rs b/rslib/src/scheduler/states/relearning.rs index e3d628045..79c021b10 100644 --- a/rslib/src/scheduler/states/relearning.rs +++ b/rslib/src/scheduler/states/relearning.rs @@ -45,7 +45,7 @@ impl RelearnState { memory_state, }, review: ReviewState { - scheduled_days, + scheduled_days: scheduled_days.round().max(1.0) as u32, elapsed_days: 0, memory_state, ..self.review @@ -55,11 +55,24 @@ impl RelearnState { } else if let Some(states) = &ctx.fsrs_next_states { let (minimum, maximum) = ctx.min_and_max_review_intervals(1); let interval = states.again.interval; - ReviewState { - scheduled_days: ctx.with_review_fuzz(interval as f32, minimum, maximum), + let again_review = ReviewState { + scheduled_days: ctx.with_review_fuzz(interval.round().max(1.0), minimum, maximum), ..self.review + }; + let again_relearn = RelearnState { + learning: LearnState { + remaining_steps: ctx.relearn_steps.remaining_for_failed(), + scheduled_secs: (interval * 86_400.0) as u32, + elapsed_secs: 0, + memory_state, + }, + review: again_review, + }; + if interval > 0.5 { + again_review.into() + } else { + again_relearn.into() } - .into() } else { self.review.into() } @@ -87,11 +100,23 @@ impl RelearnState { } else if let Some(states) = &ctx.fsrs_next_states { let (minimum, maximum) = ctx.min_and_max_review_intervals(1); let interval = states.hard.interval; - ReviewState { - scheduled_days: ctx.with_review_fuzz(interval as f32, minimum, maximum), + let hard_review = ReviewState { + scheduled_days: ctx.with_review_fuzz(interval.round().max(1.0), minimum, maximum), ..self.review + }; + let hard_relearn = RelearnState { + learning: LearnState { + scheduled_secs: (interval * 86_400.0) as u32, + memory_state, + ..self.learning + }, + review: hard_review, + }; + if interval > 0.5 { + hard_review.into() + } else { + hard_relearn.into() } - .into() } else { self.review.into() } @@ -122,11 +147,26 @@ impl RelearnState { } else if let Some(states) = &ctx.fsrs_next_states { let (minimum, maximum) = ctx.min_and_max_review_intervals(1); let interval = states.good.interval; - ReviewState { - scheduled_days: ctx.with_review_fuzz(interval as f32, minimum, maximum), + let good_review = ReviewState { + scheduled_days: ctx.with_review_fuzz(interval.round().max(1.0), minimum, maximum), ..self.review + }; + let good_relearn = RelearnState { + learning: LearnState { + scheduled_secs: (interval * 86_400.0) as u32, + remaining_steps: ctx + .relearn_steps + .remaining_for_good(self.learning.remaining_steps), + memory_state, + ..self.learning + }, + review: good_review, + }; + if interval > 0.5 { + good_review.into() + } else { + good_relearn.into() } - .into() } else { self.review.into() } @@ -135,10 +175,10 @@ impl RelearnState { fn answer_easy(self, ctx: &StateContext) -> ReviewState { let scheduled_days = if let Some(states) = &ctx.fsrs_next_states { let (mut minimum, maximum) = ctx.min_and_max_review_intervals(1); - let good = ctx.with_review_fuzz(states.good.interval as f32, minimum, maximum); + let good = ctx.with_review_fuzz(states.good.interval, minimum, maximum); minimum = good + 1; let interval = states.easy.interval; - ctx.with_review_fuzz(interval as f32, minimum, maximum) + ctx.with_review_fuzz(interval.round().max(1.0), minimum, maximum) } else { self.review.scheduled_days + 1 }; diff --git a/rslib/src/scheduler/states/review.rs b/rslib/src/scheduler/states/review.rs index 67a6380ed..9382d9264 100644 --- a/rslib/src/scheduler/states/review.rs +++ b/rslib/src/scheduler/states/review.rs @@ -75,7 +75,7 @@ impl ReviewState { pub(crate) fn failing_review_interval( self, ctx: &StateContext, - ) -> (u32, Option) { + ) -> (f32, Option) { if let Some(states) = &ctx.fsrs_next_states { // In FSRS, fuzz is applied when the card leaves the relearning // stage @@ -87,7 +87,7 @@ impl ReviewState { minimum, maximum, ); - (interval, None) + (interval as f32, None) } } @@ -96,13 +96,22 @@ impl ReviewState { let leeched = leech_threshold_met(lapses, ctx.leech_threshold); let (scheduled_days, memory_state) = self.failing_review_interval(ctx); let again_review = ReviewState { - scheduled_days, + scheduled_days: scheduled_days.round().max(1.0) as u32, elapsed_days: 0, ease_factor: (self.ease_factor + EASE_FACTOR_AGAIN_DELTA).max(MINIMUM_EASE_FACTOR), lapses, leeched, memory_state, }; + let again_relearn = RelearnState { + learning: LearnState { + remaining_steps: ctx.relearn_steps.remaining_for_failed(), + scheduled_secs: (scheduled_days * 86_400.0) as u32, + elapsed_secs: 0, + memory_state, + }, + review: again_review, + }; if let Some(again_delay) = ctx.relearn_steps.again_delay_secs_learn() { RelearnState { @@ -115,6 +124,8 @@ impl ReviewState { review: again_review, } .into() + } else if scheduled_days < 0.5 { + again_relearn.into() } else { again_review.into() } @@ -177,20 +188,20 @@ impl ReviewState { }; let hard = constrain_passing_interval( ctx, - states.hard.interval as f32, - greater_than_last(states.hard.interval).max(1), + states.hard.interval, + greater_than_last(states.hard.interval.round() as u32).max(1), true, ); let good = constrain_passing_interval( ctx, - states.good.interval as f32, - greater_than_last(states.good.interval).max(hard + 1), + states.good.interval, + greater_than_last(states.good.interval.round() as u32).max(hard + 1), true, ); let easy = constrain_passing_interval( ctx, - states.easy.interval as f32, - greater_than_last(states.easy.interval).max(good + 1), + states.easy.interval, + greater_than_last(states.easy.interval.round() as u32).max(good + 1), true, ); (hard, good, easy)