From d16faacd0f37e14386b1980cca7e1483686a9164 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 30 Sep 2023 13:39:22 +1000 Subject: [PATCH] Allow cards with no learning history when not training --- rslib/src/scheduler/fsrs/memory_state.rs | 55 ++++++++++++++++++++++++ rslib/src/scheduler/fsrs/weights.rs | 15 ++++--- 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index 8deb78f17..8e5b64054 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -125,3 +125,58 @@ pub(crate) fn single_card_revlog_to_item( let items = single_card_revlog_to_items(entries, next_day_at, false); items.and_then(|mut i| i.pop()) } + +#[cfg(test)] +mod tests { + use fsrs::MemoryState; + + use super::super::weights::tests::fsrs_items; + use super::*; + use crate::revlog::RevlogReviewKind; + use crate::scheduler::fsrs::weights::tests::convert; + use crate::scheduler::fsrs::weights::tests::review; + use crate::scheduler::fsrs::weights::tests::revlog; + + #[test] + fn bypassed_learning_is_handled() { + // cards without any learning steps due to truncated history still have memory + // state calculated + assert_eq!( + convert( + &[ + RevlogEntry { + ease_factor: 2500, + ..revlog(RevlogReviewKind::Manual, 7) + }, + revlog(RevlogReviewKind::Review, 6), + ], + false, + ), + fsrs_items!([review(0)]) + ); + } + + #[test] + fn zero_history_is_handled() { + // when the history is empty, no items are produced + assert_eq!(convert(&[], false), None); + // but memory state should still be inferred, by using the card's current state + let mut card = Card { + ctype: CardType::Review, + interval: 100, + ease_factor: 1300, + ..Default::default() + }; + card.set_memory_state(&FSRS::new(Some(&[])).unwrap(), None); + assert_eq!( + card.memory_state, + Some( + MemoryState { + stability: 100.0, + difficulty: 9.692858 + } + .into() + ) + ); + } +} diff --git a/rslib/src/scheduler/fsrs/weights.rs b/rslib/src/scheduler/fsrs/weights.rs index 7464e40b3..c069c02ff 100644 --- a/rslib/src/scheduler/fsrs/weights.rs +++ b/rslib/src/scheduler/fsrs/weights.rs @@ -135,8 +135,8 @@ pub(crate) fn single_card_revlog_to_items( if idx > 0 { entries.drain(..idx); } - } else { - // we ignore cards that don't have any learning steps + } else if training { + // when training, we ignore cards that don't have any learning steps return None; } @@ -210,12 +210,12 @@ impl RevlogEntry { } #[cfg(test)] -mod tests { +pub(crate) mod tests { use super::*; const NEXT_DAY_AT: TimestampSecs = TimestampSecs(86400 * 100); - fn revlog(review_kind: RevlogReviewKind, days_ago: i64) -> RevlogEntry { + pub(crate) fn revlog(review_kind: RevlogReviewKind, days_ago: i64) -> RevlogEntry { RevlogEntry { review_kind, id: ((NEXT_DAY_AT.0 - days_ago * 86400) * 1000).into(), @@ -224,14 +224,15 @@ mod tests { } } - fn review(delta_t: u32) -> FSRSReview { + pub(crate) fn review(delta_t: u32) -> FSRSReview { FSRSReview { rating: 3, delta_t } } - fn convert(revlog: &[RevlogEntry], training: bool) -> Option> { + pub(crate) fn convert(revlog: &[RevlogEntry], training: bool) -> Option> { single_card_revlog_to_items(revlog.to_vec(), NEXT_DAY_AT, training) } + #[macro_export] macro_rules! fsrs_items { ($($reviews:expr),*) => { Some(vec![ @@ -244,6 +245,8 @@ mod tests { }; } + pub(crate) use fsrs_items; + #[test] fn delta_t_is_correct() -> Result<()> { assert_eq!(