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) }