From 0532c1f5b08c1b790d233ee3049c086a48c44bd5 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 27 Sep 2023 15:50:51 +1000 Subject: [PATCH] Use revlog to determine days_elapsed when studying/for card info Currently prop searches and the retrievability column will continue to derive the days from the card only, as it's difficulty to integrate revlog upgrade lookups into those code paths, especially in a performant way. One possible way we could solve this in the future is to store last_review_day in the card data, so we can know it even if the due date has been shifted. Check DB could fill it in for existing cards. --- rslib/src/browser_table.rs | 11 ++++++++--- rslib/src/scheduler/answering/mod.rs | 7 ++++++- rslib/src/stats/card.rs | 7 ++++++- rslib/src/storage/revlog/mod.rs | 10 ++++++++++ rslib/src/storage/revlog/time_of_last_review.sql | 6 ++++++ rslib/src/timestamp.rs | 4 ++++ 6 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 rslib/src/storage/revlog/time_of_last_review.sql diff --git a/rslib/src/browser_table.rs b/rslib/src/browser_table.rs index 791f92ea4..dd1bf0158 100644 --- a/rslib/src/browser_table.rs +++ b/rslib/src/browser_table.rs @@ -112,14 +112,19 @@ impl Card { if self.queue == CardQueue::Learn { Some(TimestampSecs(self.due as i64)) } else if self.is_due_in_days() { - Some(TimestampSecs::now().adding_secs( - ((self.original_or_current_due() - timing.days_elapsed as i32).saturating_mul(86400)) as i64, - )) + Some( + TimestampSecs::now().adding_secs( + ((self.original_or_current_due() - timing.days_elapsed as i32) + .saturating_mul(86400)) as i64, + ), + ) } else { None } } + /// This uses card.due and card.ivl to infer the elapsed time. If 'set due + /// date' or an add-on has changed the due date, this won't be accurate. pub(crate) fn days_since_last_review(&self, timing: &SchedTimingToday) -> Option { if !self.is_due_in_days() { Some(0) diff --git a/rslib/src/scheduler/answering/mod.rs b/rslib/src/scheduler/answering/mod.rs index 557270d61..02e5ad0fc 100644 --- a/rslib/src/scheduler/answering/mod.rs +++ b/rslib/src/scheduler/answering/mod.rs @@ -364,10 +364,15 @@ impl Collection { let item = single_card_revlog_to_item(revlog, timing.next_day_at); card.set_memory_state(&fsrs, item); } + let days_elapsed = self + .storage + .time_of_last_review(card.id)? + .map(|ts| ts.elapsed_days_since(timing.next_day_at)) + .unwrap_or_default() as u32; Some(fsrs.next_states( card.memory_state.map(Into::into), config.inner.desired_retention, - card.days_since_last_review(&timing).unwrap_or_default(), + days_elapsed, )) } else { None diff --git a/rslib/src/stats/card.rs b/rslib/src/stats/card.rs index 2a7afb428..22d15cd6e 100644 --- a/rslib/src/stats/card.rs +++ b/rslib/src/stats/card.rs @@ -27,9 +27,14 @@ impl Collection { let (average_secs, total_secs) = average_and_total_secs_strings(&revlog); let (due_date, due_position) = self.due_date_and_position(&card)?; let timing = self.timing_today()?; + let days_elapsed = self + .storage + .time_of_last_review(card.id)? + .map(|ts| ts.elapsed_days_since(timing.next_day_at)) + .unwrap_or_default() as u32; let fsrs_retrievability = card .memory_state - .zip(card.days_since_last_review(&timing)) + .zip(Some(days_elapsed)) .map(|(state, days)| { FSRS::new(None) .unwrap() diff --git a/rslib/src/storage/revlog/mod.rs b/rslib/src/storage/revlog/mod.rs index 7326c7ea1..3de6fa768 100644 --- a/rslib/src/storage/revlog/mod.rs +++ b/rslib/src/storage/revlog/mod.rs @@ -7,6 +7,7 @@ use rusqlite::params; use rusqlite::types::FromSql; use rusqlite::types::FromSqlError; use rusqlite::types::ValueRef; +use rusqlite::OptionalExtension; use rusqlite::Row; use super::SqliteStorage; @@ -93,6 +94,15 @@ impl SqliteStorage { .transpose() } + /// Determine the the last review time based on the revlog. + pub(crate) fn time_of_last_review(&self, card_id: CardId) -> Result> { + self.db + .prepare_cached(include_str!("time_of_last_review.sql"))? + .query_row([card_id], |row| row.get(0)) + .optional() + .map_err(Into::into) + } + /// Only intended to be used by the undo code, as Anki can not sync revlog /// deletions. pub(crate) fn remove_revlog_entry(&self, id: RevlogId) -> Result<()> { diff --git a/rslib/src/storage/revlog/time_of_last_review.sql b/rslib/src/storage/revlog/time_of_last_review.sql new file mode 100644 index 000000000..ce66308f5 --- /dev/null +++ b/rslib/src/storage/revlog/time_of_last_review.sql @@ -0,0 +1,6 @@ +SELECT id / 1000 +FROM revlog +WHERE cid = $1 + AND ease BETWEEN 1 AND 4 +ORDER BY id DESC +LIMIT 1 \ No newline at end of file diff --git a/rslib/src/timestamp.rs b/rslib/src/timestamp.rs index e0f919e47..589dfad89 100644 --- a/rslib/src/timestamp.rs +++ b/rslib/src/timestamp.rs @@ -28,6 +28,10 @@ impl TimestampSecs { (Self::now().0 - self.0).max(0) as u64 } + pub fn elapsed_days_since(self, other: TimestampSecs) -> u64 { + (other.0 - self.0).max(0) as u64 / 86_400 + } + pub fn as_millis(self) -> TimestampMillis { TimestampMillis(self.0 * 1000) }