Suppress manual revlog entry if the previous entry was also manual

Means we lose some detail in the history, but should reduce the
collection bloat caused by a user experimenting with reschedule multiple
times in a short period, when they don't restore from backup afterwards.

https://forums.ankiweb.net/t/possible-syncing-limitation-by-fsrs-manual-scheduling-data-accumulation/37610
This commit is contained in:
Damien Elmes 2023-11-27 11:23:00 +10:00
parent 8b6abd3f8f
commit d767e9ad3c

View File

@ -12,6 +12,7 @@ use itertools::Itertools;
use crate::card::CardType;
use crate::prelude::*;
use crate::revlog::RevlogEntry;
use crate::revlog::RevlogReviewKind;
use crate::scheduler::fsrs::weights::single_card_revlog_to_items;
use crate::scheduler::fsrs::weights::Weights;
use crate::scheduler::states::fuzz::with_review_fuzz;
@ -51,8 +52,8 @@ impl Collection {
SearchBuilder::all([search.into(), SearchNode::State(StateKind::New).negated()]);
let revlog = self.revlog_for_srs(search)?;
let reschedule = req.as_ref().map(|e| e.reschedule).unwrap_or_default();
let last_reviews = if reschedule {
Some(get_last_reviews(&revlog))
let last_revlog_info = if reschedule {
Some(get_last_revlog_info(&revlog))
} else {
None
};
@ -75,40 +76,46 @@ impl Collection {
card.set_memory_state(&fsrs, item, sm2_retention.unwrap());
card.desired_retention = desired_retention;
// if rescheduling
if let Some(reviews) = &last_reviews {
if let Some(reviews) = &last_revlog_info {
// and we have a last review time for the card
if let Some(last_review) = reviews.get(&card.id) {
let days_elapsed =
timing.next_day_at.elapsed_days_since(*last_review) as i32;
// and the card's not new
if let Some(state) = &card.memory_state {
// or in (re)learning
if card.ctype == CardType::Review {
// reschedule it
let original_interval = card.interval;
let interval = fsrs.next_interval(
Some(state.stability),
card.desired_retention.unwrap(),
0,
) as f32;
card.interval = with_review_fuzz(
card.get_fuzz_factor(),
interval,
1,
req.max_interval,
);
let due = if card.original_due != 0 {
&mut card.original_due
} else {
&mut card.due
};
*due = (timing.days_elapsed as i32) - days_elapsed
+ card.interval as i32;
self.log_manually_scheduled_review(
&card,
original_interval,
usn,
)?;
if let Some(last_info) = reviews.get(&card.id) {
if let Some(last_review) = &last_info.last_reviewed_at {
let days_elapsed =
timing.next_day_at.elapsed_days_since(*last_review) as i32;
// and the card's not new
if let Some(state) = &card.memory_state {
// or in (re)learning
if card.ctype == CardType::Review {
// reschedule it
let original_interval = card.interval;
let interval = fsrs.next_interval(
Some(state.stability),
card.desired_retention.unwrap(),
0,
)
as f32;
card.interval = with_review_fuzz(
card.get_fuzz_factor(),
interval,
1,
req.max_interval,
);
let due = if card.original_due != 0 {
&mut card.original_due
} else {
&mut card.due
};
*due = (timing.days_elapsed as i32) - days_elapsed
+ card.interval as i32;
// Add a manual revlog entry if the last entry wasn't manual
if !last_info.last_revlog_is_manual {
self.log_manually_scheduled_review(
&card,
original_interval,
usn,
)?;
}
}
}
}
}
@ -204,21 +211,39 @@ pub(crate) fn fsrs_items_for_memory_state(
.collect()
}
/// Return a map of cards to the last time they were reviewed.
fn get_last_reviews(revlogs: &[RevlogEntry]) -> HashMap<CardId, TimestampSecs> {
struct LastRevlogInfo {
/// Used to determine the actual elapsed time between the last time the user
/// reviewed the card and now, so that we can determine an accurate period
/// when the card has subsequently been rescheduled to a different day.
last_reviewed_at: Option<TimestampSecs>,
/// If true, the last action on this card was a reschedule, so we
/// can avoid writing an extra revlog entry on another reschedule.
last_revlog_is_manual: bool,
}
/// Return a map of cards to info about last review/reschedule.
fn get_last_revlog_info(revlogs: &[RevlogEntry]) -> HashMap<CardId, LastRevlogInfo> {
let mut out = HashMap::new();
revlogs
.iter()
.group_by(|r| r.cid)
.into_iter()
.for_each(|(card_id, group)| {
let mut last_ts = TimestampSecs::zero();
for entry in group.into_iter().filter(|r| r.button_chosen >= 1) {
last_ts = entry.id.as_secs();
}
if last_ts != TimestampSecs::zero() {
out.insert(card_id, last_ts);
let mut last_reviewed_at = None;
let mut last_revlog_is_manual = false;
for e in group.into_iter() {
if e.button_chosen >= 1 {
last_reviewed_at = Some(e.id.as_secs());
}
last_revlog_is_manual = e.review_kind == RevlogReviewKind::Manual;
}
out.insert(
card_id,
LastRevlogInfo {
last_reviewed_at,
last_revlog_is_manual,
},
);
});
out
}