mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
Record FSRS difficulty in the review log
Will allow user to see a record of difficulty changes, and allows us to identify reviews that have been done with FSRS vs SM-2, since the valid range is different.
This commit is contained in:
parent
0301ae1d8a
commit
75de1f9709
8 changed files with 62 additions and 10 deletions
|
@ -482,7 +482,7 @@ impl RowContext {
|
||||||
self.cards[0]
|
self.cards[0]
|
||||||
.memory_state
|
.memory_state
|
||||||
.as_ref()
|
.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()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -101,10 +101,26 @@ pub struct Card {
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct FsrsMemoryState {
|
pub struct FsrsMemoryState {
|
||||||
|
/// The expected memory stability, in days.
|
||||||
pub stability: f32,
|
pub stability: f32,
|
||||||
|
/// A number in the range 1.0-10.0. Use difficulty() for a normalized
|
||||||
|
/// number.
|
||||||
pub difficulty: f32,
|
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 {
|
impl Default for Card {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
|
@ -48,7 +48,8 @@ pub struct RevlogEntry {
|
||||||
#[serde(rename = "lastIvl", deserialize_with = "deserialize_int_from_number")]
|
#[serde(rename = "lastIvl", deserialize_with = "deserialize_int_from_number")]
|
||||||
pub last_interval: i32,
|
pub last_interval: i32,
|
||||||
/// Card's ease after answering, stored as 10x the %, eg 2500 represents
|
/// 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")]
|
#[serde(rename = "factor", deserialize_with = "deserialize_int_from_number")]
|
||||||
pub ease_factor: u32,
|
pub ease_factor: u32,
|
||||||
/// Amount of milliseconds taken to answer the card.
|
/// Amount of milliseconds taken to answer the card.
|
||||||
|
|
|
@ -26,7 +26,15 @@ impl CardStateUpdater {
|
||||||
self.card.original_position = None;
|
self.card.original_position = None;
|
||||||
self.card.memory_state = 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(
|
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
|
/// Adds secs + fuzz to current time
|
||||||
|
|
|
@ -42,7 +42,10 @@ impl CardStateUpdater {
|
||||||
RevlogEntryPartial::new(
|
RevlogEntryPartial::new(
|
||||||
current,
|
current,
|
||||||
next.into(),
|
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(),
|
self.secs_until_rollover(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,10 @@ impl CardStateUpdater {
|
||||||
RevlogEntryPartial::new(
|
RevlogEntryPartial::new(
|
||||||
current,
|
current,
|
||||||
next.into(),
|
next.into(),
|
||||||
next.ease_factor,
|
self.card
|
||||||
|
.memory_state
|
||||||
|
.map(|d| d.difficulty_shifted())
|
||||||
|
.unwrap_or(next.ease_factor),
|
||||||
self.secs_until_rollover(),
|
self.secs_until_rollover(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,9 +15,7 @@ impl GraphsContext {
|
||||||
if let Some(state) = card.memory_state {
|
if let Some(state) = card.memory_state {
|
||||||
*difficulty
|
*difficulty
|
||||||
.eases
|
.eases
|
||||||
.entry(round_to_nearest_five(
|
.entry(round_to_nearest_five(state.difficulty() * 100.0))
|
||||||
(state.difficulty - 1.0) / 9.0 * 100.0,
|
|
||||||
))
|
|
||||||
.or_insert_with(Default::default) += 1;
|
.or_insert_with(Default::default) += 1;
|
||||||
} else if matches!(card.ctype, CardType::Review | CardType::Relearn) {
|
} else if matches!(card.ctype, CardType::Review | CardType::Relearn) {
|
||||||
*eases
|
*eases
|
||||||
|
|
|
@ -67,12 +67,27 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
rating: entry.buttonChosen,
|
rating: entry.buttonChosen,
|
||||||
ratingClass: ratingClass(entry),
|
ratingClass: ratingClass(entry),
|
||||||
interval: timeSpan(entry.interval),
|
interval: timeSpan(entry.interval),
|
||||||
ease: entry.ease ? `${entry.ease / 10}%` : "",
|
ease: formatEaseOrDifficulty(entry.ease),
|
||||||
takenSecs: timeSpan(entry.takenSecs, true),
|
takenSecs: timeSpan(entry.takenSecs, true),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
$: revlogRows = revlog.map(revlogRowFromEntry);
|
$: 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)}%`;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if revlog.length > 0}
|
{#if revlog.length > 0}
|
||||||
|
|
Loading…
Reference in a new issue