Use first >= 1d interval for starting memory state (#3959)

When a filtered revlog history begins with one or more < 1d intervals
(for example, because it starts in the middle of a sequence of
relearning steps), use the first >= 1d interval to calculate the initial
memory state.

Bug report thread:

https://forums.ankiweb.net/t/fsrs-stability-when-first-non-ignored-revlog-has-a-relearning-interval/59894
This commit is contained in:
Matt Brubeck 2025-05-04 22:49:06 -07:00 committed by GitHub
parent 57ecfbe562
commit bcb28f0a85
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -313,9 +313,12 @@ pub(crate) fn reviews_for_fsrs(
if entry.review_kind == RevlogReviewKind::Filtered && entry.ease_factor == 0 {
continue;
}
// For incomplete review histories, initial memory state is based on the first
// user-graded review after the cutoff date with interval >= 1d.
let within_cutoff = entry.id.0 > ignore_revlogs_before.0;
let user_graded = matches!(entry.button_chosen, 1..=4);
if user_graded && within_cutoff {
let interday = entry.interval >= 1 || entry.interval <= -86400;
if user_graded && within_cutoff && interday {
first_user_grade_idx = Some(index);
}
@ -477,6 +480,7 @@ pub(crate) mod tests {
review_kind,
id: days_ago_ms(days_ago).into(),
button_chosen: 3,
interval: 1,
..Default::default()
}
}
@ -711,6 +715,28 @@ pub(crate) mod tests {
assert_eq!(convert_ignore_before(revlogs, true, days_ago_ms(9)), None);
}
#[test]
fn skip_initial_relearning_steps() {
let revlogs = &[
revlog(RevlogReviewKind::Review, 10),
RevlogEntry {
button_chosen: 1, // Again
interval: -600,
..revlog(RevlogReviewKind::Review, 8)
},
revlog(RevlogReviewKind::Relearning, 8),
revlog(RevlogReviewKind::Review, 6),
];
// | = Ignore before
// A = Again
// X = Relearning
// R | A X R
assert_eq!(
convert_ignore_before(revlogs, false, days_ago_ms(9)),
fsrs_items!([review(0)], [review(0), review(2)])
);
}
#[test]
fn ignore_before_date_between_learning_steps_when_reviewing() {
let revlogs = &[