diff --git a/rslib/src/scheduler/answering/mod.rs b/rslib/src/scheduler/answering/mod.rs index 7c25f4a6a..7e6511563 100644 --- a/rslib/src/scheduler/answering/mod.rs +++ b/rslib/src/scheduler/answering/mod.rs @@ -620,7 +620,7 @@ fn get_fuzz_factor(seed: Option) -> Option { } #[cfg(test)] -mod test { +pub(crate) mod test { use super::*; use crate::card::CardType; use crate::deckconfig::ReviewMix; @@ -741,7 +741,7 @@ mod test { Ok(()) } - fn v3_test_collection(cards: usize) -> Result<(Collection, Vec)> { + pub(crate) fn v3_test_collection(cards: usize) -> Result<(Collection, Vec)> { let mut col = Collection::new(); let nt = col.get_notetype_by_name("Basic")?.unwrap(); for _ in 0..cards { diff --git a/rslib/src/scheduler/fsrs/mod.rs b/rslib/src/scheduler/fsrs/mod.rs index aa324dddb..fe2168a7f 100644 --- a/rslib/src/scheduler/fsrs/mod.rs +++ b/rslib/src/scheduler/fsrs/mod.rs @@ -1,5 +1,6 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + mod error; pub mod memory_state; pub mod params; diff --git a/rslib/src/storage/card/mod.rs b/rslib/src/storage/card/mod.rs index 4cc7fd626..6b19b034b 100644 --- a/rslib/src/storage/card/mod.rs +++ b/rslib/src/storage/card/mod.rs @@ -736,7 +736,7 @@ impl super::SqliteStorage { } #[derive(Clone, Copy)] -enum ReviewOrderSubclause { +pub(crate) enum ReviewOrderSubclause { Day, Deck, Random, diff --git a/rslib/src/storage/sqlite.rs b/rslib/src/storage/sqlite.rs index 0775ac7b4..0f94e79df 100644 --- a/rslib/src/storage/sqlite.rs +++ b/rslib/src/storage/sqlite.rs @@ -346,51 +346,53 @@ fn add_extract_fsrs_relative_retrievability(db: &Connection) -> rusqlite::Result move |ctx| { assert_eq!(ctx.len(), 5, "called with unexpected number of arguments"); - let Ok(card_data) = ctx.get_raw(0).as_str() else { + let Ok(due) = ctx.get_raw(1).as_i64() else { return Ok(None); }; - if card_data.is_empty() { + let Ok(interval) = ctx.get_raw(3).as_i64() else { return Ok(None); - } - let card_data = &CardData::from_str(card_data); - let Ok(due) = ctx.get_raw(1).as_i64() else { + }; + let Ok(next_day_at) = ctx.get_raw(4).as_i64() else { return Ok(None); }; let days_elapsed = if due > 365_000 { // (re)learning - let Ok(next_day_at) = ctx.get_raw(4).as_i64() else { - return Ok(None); - }; next_day_at.saturating_sub(due) as u32 / 86_400 } else { let Ok(days_elapsed) = ctx.get_raw(2).as_i64() else { return Ok(None); }; - let Ok(interval) = ctx.get_raw(3).as_i64() else { - return Ok(None); - }; let review_day = due.saturating_sub(interval); days_elapsed.saturating_sub(review_day) as u32 }; - let Some(state) = card_data.memory_state() else { - return Ok(None); - }; - let Some(mut desired_retrievability) = card_data.fsrs_desired_retention else { - return Ok(None); - }; - // avoid div by zero - desired_retrievability = desired_retrievability.max(0.0001); + if let Ok(card_data) = ctx.get_raw(0).as_str() { + if !card_data.is_empty() { + let card_data = &CardData::from_str(card_data); + if let (Some(state), Some(mut desired_retrievability)) = + (card_data.memory_state(), card_data.fsrs_desired_retention) + { + // avoid div by zero + desired_retrievability = desired_retrievability.max(0.0001); - let current_retrievability = FSRS::new(None) - .unwrap() - .current_retrievability(state.into(), days_elapsed) - .max(0.0001); + let current_retrievability = FSRS::new(None) + .unwrap() + .current_retrievability(state.into(), days_elapsed) + .max(0.0001); + return Ok(Some( + // power should be the reciprocal of the value of DECAY in FSRS-rs, + // which is currently -0.5 + -(current_retrievability.powi(-2) - 1.) + / (desired_retrievability.powi(-2) - 1.), + )); + } + } + } + + // FSRS data missing; fall back to SM2 ordering Ok(Some( - // power should be the reciprocal of the value of DECAY in FSRS-rs, which is - // currently -0.5 - -(current_retrievability.powi(-2) - 1.) / (desired_retrievability.powi(-2) - 1.), + -((days_elapsed as f32) + 0.001) / (interval as f32).max(1.0), )) }, ) @@ -613,3 +615,36 @@ impl Display for SqlSortOrder { ) } } + +#[cfg(test)] +mod test { + use super::*; + use crate::scheduler::answering::test::v3_test_collection; + use crate::storage::card::ReviewOrderSubclause; + + #[test] + fn missing_memory_state_falls_back_to_sm2() -> Result<()> { + let (mut col, _cids) = v3_test_collection(1)?; + col.set_config_bool(BoolKey::Fsrs, true, true)?; + col.answer_easy(); + + let timing = col.timing_today()?; + let order = SqlSortOrder::Ascending; + let sql_func = ReviewOrderSubclause::RetrievabilityFsrs { timing, order } + .to_string() + .replace(" asc", ""); + let sql = format!("select {sql_func} from cards"); + + // value from fsrs + let mut pos: Option; + pos = col.storage.db_scalar(&sql).unwrap(); + assert_eq!(pos, Some(0.0)); + // erasing the memory state should not result in None output + col.storage.db.execute("update cards set data=''", [])?; + pos = col.storage.db_scalar(&sql).unwrap(); + assert!(pos.is_some()); + // but it won't match the fsrs value + assert!(pos.unwrap() < -0.0); + Ok(()) + } +}