diff --git a/rslib/src/browser_table.rs b/rslib/src/browser_table.rs index 7d823f9e9..7f43bd2f1 100644 --- a/rslib/src/browser_table.rs +++ b/rslib/src/browser_table.rs @@ -482,7 +482,7 @@ impl RowContext { self.cards[0] .memory_state .as_ref() - .map(|s| format!("{:.0}%", (s.difficulty - 1.0) / 9.0 * 100.0)) + .map(|s| format!("{:.0}%", s.difficulty() * 100.0)) .unwrap_or_default() } diff --git a/rslib/src/card/mod.rs b/rslib/src/card/mod.rs index ee7bbc3a3..9285993d5 100644 --- a/rslib/src/card/mod.rs +++ b/rslib/src/card/mod.rs @@ -101,10 +101,26 @@ pub struct Card { #[derive(Debug, Clone, Copy, PartialEq)] pub struct FsrsMemoryState { + /// The expected memory stability, in days. pub stability: f32, + /// A number in the range 1.0-10.0. Use difficulty() for a normalized + /// number. pub difficulty: f32, } +impl FsrsMemoryState { + /// Returns the difficulty normalized to a 0.0-1.0 range. + pub(crate) fn difficulty(&self) -> f32 { + (self.difficulty - 1.0) / 9.0 + } + + /// Returns the difficulty normalized to a 0.1-1.1 range, + /// which is used in revlog entries. + pub(crate) fn difficulty_shifted(&self) -> f32 { + self.difficulty() + 0.1 + } +} + impl Default for Card { fn default() -> Self { Self { diff --git a/rslib/src/revlog/mod.rs b/rslib/src/revlog/mod.rs index ce4c23785..69ed83ee8 100644 --- a/rslib/src/revlog/mod.rs +++ b/rslib/src/revlog/mod.rs @@ -48,7 +48,8 @@ pub struct RevlogEntry { #[serde(rename = "lastIvl", deserialize_with = "deserialize_int_from_number")] pub last_interval: i32, /// Card's ease after answering, stored as 10x the %, eg 2500 represents - /// 250%. + /// 250%. When FSRS is active, difficulty is normalized to 100-1100 range, + /// so a 0 difficulty can be distinguished from SM-2 learning. #[serde(rename = "factor", deserialize_with = "deserialize_int_from_number")] pub ease_factor: u32, /// Amount of milliseconds taken to answer the card. diff --git a/rslib/src/scheduler/answering/learning.rs b/rslib/src/scheduler/answering/learning.rs index 4b674e131..3888ae651 100644 --- a/rslib/src/scheduler/answering/learning.rs +++ b/rslib/src/scheduler/answering/learning.rs @@ -26,7 +26,15 @@ impl CardStateUpdater { self.card.original_position = None; self.card.memory_state = None; - RevlogEntryPartial::new(current, next.into(), 0.0, self.secs_until_rollover()) + RevlogEntryPartial::new( + current, + next.into(), + self.card + .memory_state + .map(|d| d.difficulty_shifted()) + .unwrap_or_default(), + self.secs_until_rollover(), + ) } pub(super) fn apply_learning_state( @@ -55,7 +63,15 @@ impl CardStateUpdater { } } - RevlogEntryPartial::new(current, next.into(), 0.0, self.secs_until_rollover()) + RevlogEntryPartial::new( + current, + next.into(), + self.card + .memory_state + .map(|d| d.difficulty_shifted()) + .unwrap_or_default(), + self.secs_until_rollover(), + ) } /// Adds secs + fuzz to current time diff --git a/rslib/src/scheduler/answering/relearning.rs b/rslib/src/scheduler/answering/relearning.rs index c218e765c..ec74a5fec 100644 --- a/rslib/src/scheduler/answering/relearning.rs +++ b/rslib/src/scheduler/answering/relearning.rs @@ -42,7 +42,10 @@ impl CardStateUpdater { RevlogEntryPartial::new( current, next.into(), - next.review.ease_factor, + self.card + .memory_state + .map(|d| d.difficulty_shifted()) + .unwrap_or(next.review.ease_factor), self.secs_until_rollover(), ) } diff --git a/rslib/src/scheduler/answering/review.rs b/rslib/src/scheduler/answering/review.rs index 9f3a197fb..ef7aec616 100644 --- a/rslib/src/scheduler/answering/review.rs +++ b/rslib/src/scheduler/answering/review.rs @@ -29,7 +29,10 @@ impl CardStateUpdater { RevlogEntryPartial::new( current, next.into(), - next.ease_factor, + self.card + .memory_state + .map(|d| d.difficulty_shifted()) + .unwrap_or(next.ease_factor), self.secs_until_rollover(), ) } diff --git a/rslib/src/stats/graphs/eases.rs b/rslib/src/stats/graphs/eases.rs index 15571c54a..27b59343f 100644 --- a/rslib/src/stats/graphs/eases.rs +++ b/rslib/src/stats/graphs/eases.rs @@ -15,9 +15,7 @@ impl GraphsContext { if let Some(state) = card.memory_state { *difficulty .eases - .entry(round_to_nearest_five( - (state.difficulty - 1.0) / 9.0 * 100.0, - )) + .entry(round_to_nearest_five(state.difficulty() * 100.0)) .or_insert_with(Default::default) += 1; } else if matches!(card.ctype, CardType::Review | CardType::Relearn) { *eases diff --git a/ts/card-info/Revlog.svelte b/ts/card-info/Revlog.svelte index 3cc7161f0..8c8cf7c15 100644 --- a/ts/card-info/Revlog.svelte +++ b/ts/card-info/Revlog.svelte @@ -67,12 +67,27 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html rating: entry.buttonChosen, ratingClass: ratingClass(entry), interval: timeSpan(entry.interval), - ease: entry.ease ? `${entry.ease / 10}%` : "", + ease: formatEaseOrDifficulty(entry.ease), takenSecs: timeSpan(entry.takenSecs, true), }; } $: revlogRows = revlog.map(revlogRowFromEntry); + + function formatEaseOrDifficulty(ease: number): string { + if (ease === 0) { + return ""; + } + const asPct = ease / 10.0; + if (asPct <= 110) { + // FSRS + const unshifted = asPct - 10; + return `D:${unshifted.toFixed(0)}%`; + } else { + // SM-2 + return `${asPct.toFixed(0)}%`; + } + } {#if revlog.length > 0}