Fix panic when enabling FSRS with add-on-rescheduled cards

https://forums.ankiweb.net/t/error-upon-fsrs-activation-on-anki-23-10/36488
This commit is contained in:
Damien Elmes 2023-11-03 10:07:47 +10:00
parent 613a75773d
commit 987c1825a6
2 changed files with 48 additions and 28 deletions

View file

@ -12,7 +12,6 @@ use itertools::Itertools;
use crate::card::CardType; use crate::card::CardType;
use crate::prelude::*; use crate::prelude::*;
use crate::revlog::RevlogEntry; use crate::revlog::RevlogEntry;
use crate::revlog::RevlogReviewKind;
use crate::scheduler::fsrs::weights::single_card_revlog_to_items; use crate::scheduler::fsrs::weights::single_card_revlog_to_items;
use crate::scheduler::fsrs::weights::Weights; use crate::scheduler::fsrs::weights::Weights;
use crate::scheduler::states::fuzz::with_review_fuzz; use crate::scheduler::states::fuzz::with_review_fuzz;
@ -235,27 +234,39 @@ pub(crate) fn single_card_revlog_to_item(
next_day_at: TimestampSecs, next_day_at: TimestampSecs,
sm2_retention: f32, sm2_retention: f32,
) -> Option<FsrsItemWithStartingState> { ) -> Option<FsrsItemWithStartingState> {
let have_learning = entries struct FirstReview {
interval: f32,
ease_factor: f32,
}
let first_review = entries
.iter() .iter()
.any(|e| e.review_kind == RevlogReviewKind::Learning); .find(|e| e.button_chosen > 0)
if have_learning { .map(|e| FirstReview {
let items = single_card_revlog_to_items(entries, next_day_at, false); interval: e.interval.max(1) as f32,
Some(FsrsItemWithStartingState { ease_factor: if e.ease_factor == 0 {
item: items.unwrap().pop().unwrap(),
starting_state: None,
})
} else if let Some(first_review) = entries.iter().find(|e| e.button_chosen > 0) {
let ease_factor = if first_review.ease_factor == 0 {
2500 2500
} else { } else {
first_review.ease_factor e.ease_factor
}; } as f32
let interval = first_review.interval.max(1); / 1000.0,
let starting_state = });
fsrs.memory_state_from_sm2(ease_factor as f32 / 1000.0, interval as f32, sm2_retention); if let Some((mut items, found_learning)) =
let items = single_card_revlog_to_items(entries, next_day_at, false); single_card_revlog_to_items(entries, next_day_at, false)
items.and_then(|mut items| { {
let mut item = items.pop().unwrap(); let mut item = items.pop().unwrap();
if found_learning {
// we assume the revlog is complete
Some(FsrsItemWithStartingState {
item,
starting_state: None,
})
} else if let Some(first_review) = first_review {
// the revlog has been truncated, but not fully
let starting_state = fsrs.memory_state_from_sm2(
first_review.ease_factor,
first_review.interval,
sm2_retention,
);
item.reviews.remove(0); item.reviews.remove(0);
if item.reviews.is_empty() { if item.reviews.is_empty() {
None None
@ -265,7 +276,10 @@ pub(crate) fn single_card_revlog_to_item(
starting_state: Some(starting_state), starting_state: Some(starting_state),
}) })
} }
}) } else {
// only manual rescheduling; treat like empty
None
}
} else { } else {
None None
} }

View file

@ -101,7 +101,7 @@ fn fsrs_items_for_training(revlogs: Vec<RevlogEntry>, next_day_at: TimestampSecs
.filter_map(|(_cid, entries)| { .filter_map(|(_cid, entries)| {
single_card_revlog_to_items(entries.collect(), next_day_at, true) single_card_revlog_to_items(entries.collect(), next_day_at, true)
}) })
.flatten() .flat_map(|i| i.0)
.collect_vec(); .collect_vec();
revlogs.sort_by_cached_key(|r| r.reviews.len()); revlogs.sort_by_cached_key(|r| r.reviews.len());
revlogs revlogs
@ -111,16 +111,22 @@ fn fsrs_items_for_training(revlogs: Vec<RevlogEntry>, next_day_at: TimestampSecs
/// expects multiple items for a given card when training - for revlog /// expects multiple items for a given card when training - for revlog
/// `[1,2,3]`, we create FSRSItems corresponding to `[1,2]` and `[1,2,3]` /// `[1,2,3]`, we create FSRSItems corresponding to `[1,2]` and `[1,2,3]`
/// in training, and `[1]`, [1,2]` and `[1,2,3]` when calculating memory /// in training, and `[1]`, [1,2]` and `[1,2,3]` when calculating memory
/// state. /// state. Returns (items, found_learn_entry), the latter of which is used
/// to determine whether the revlogs have been truncated when not training.
pub(crate) fn single_card_revlog_to_items( pub(crate) fn single_card_revlog_to_items(
mut entries: Vec<RevlogEntry>, mut entries: Vec<RevlogEntry>,
next_day_at: TimestampSecs, next_day_at: TimestampSecs,
training: bool, training: bool,
) -> Option<Vec<FSRSItem>> { ) -> Option<(Vec<FSRSItem>, bool)> {
let mut last_learn_entry = None; let mut last_learn_entry = None;
let mut found_learn_entry = false;
for (index, entry) in entries.iter().enumerate().rev() { for (index, entry) in entries.iter().enumerate().rev() {
if entry.review_kind == RevlogReviewKind::Learning { if matches!(
(entry.review_kind, entry.button_chosen),
(RevlogReviewKind::Learning, 1..=4)
) {
last_learn_entry = Some(index); last_learn_entry = Some(index);
found_learn_entry = true;
} else if last_learn_entry.is_some() { } else if last_learn_entry.is_some() {
break; break;
} }
@ -199,7 +205,7 @@ pub(crate) fn single_card_revlog_to_items(
if items.is_empty() { if items.is_empty() {
None None
} else { } else {
Some(items) Some((items, found_learn_entry))
} }
} }
@ -229,7 +235,7 @@ pub(crate) mod tests {
} }
pub(crate) fn convert(revlog: &[RevlogEntry], training: bool) -> Option<Vec<FSRSItem>> { pub(crate) fn convert(revlog: &[RevlogEntry], training: bool) -> Option<Vec<FSRSItem>> {
single_card_revlog_to_items(revlog.to_vec(), NEXT_DAY_AT, training) single_card_revlog_to_items(revlog.to_vec(), NEXT_DAY_AT, training).map(|i| i.0)
} }
#[macro_export] #[macro_export]