mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
Add last_review_time
to card data for performance and accuracy (#4124)
* Add `last_review_time` to card data * cargo clippy * Calculate days elapsed since last review time in add_extract_fsrs_relative_retrievability * expose last_review_time to Card in Python * Fix last_review_time assignment in Card class to use last_review_time_secs * format * Update last_review_time assignment to exclude filtered preview state in Card class
This commit is contained in:
parent
3adcf05ca6
commit
037dfa1bc1
12 changed files with 75 additions and 20 deletions
|
@ -51,6 +51,7 @@ message Card {
|
||||||
optional FsrsMemoryState memory_state = 20;
|
optional FsrsMemoryState memory_state = 20;
|
||||||
optional float desired_retention = 21;
|
optional float desired_retention = 21;
|
||||||
optional float decay = 22;
|
optional float decay = 22;
|
||||||
|
optional int64 last_review_time_secs = 23;
|
||||||
string custom_data = 19;
|
string custom_data = 19;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,7 @@ class Card(DeprecatedNamesMixin):
|
||||||
memory_state: FSRSMemoryState | None
|
memory_state: FSRSMemoryState | None
|
||||||
desired_retention: float | None
|
desired_retention: float | None
|
||||||
decay: float | None
|
decay: float | None
|
||||||
|
last_review_time: int | None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -103,6 +104,11 @@ class Card(DeprecatedNamesMixin):
|
||||||
card.desired_retention if card.HasField("desired_retention") else None
|
card.desired_retention if card.HasField("desired_retention") else None
|
||||||
)
|
)
|
||||||
self.decay = card.decay if card.HasField("decay") else None
|
self.decay = card.decay if card.HasField("decay") else None
|
||||||
|
self.last_review_time = (
|
||||||
|
card.last_review_time_secs
|
||||||
|
if card.HasField("last_review_time_secs")
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
def _to_backend_card(self) -> cards_pb2.Card:
|
def _to_backend_card(self) -> cards_pb2.Card:
|
||||||
# mtime & usn are set by backend
|
# mtime & usn are set by backend
|
||||||
|
|
|
@ -128,7 +128,9 @@ impl Card {
|
||||||
/// This uses card.due and card.ivl to infer the elapsed time. If 'set due
|
/// 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.
|
/// 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<u32> {
|
pub(crate) fn days_since_last_review(&self, timing: &SchedTimingToday) -> Option<u32> {
|
||||||
if !self.is_due_in_days() {
|
if let Some(last_review_time) = self.last_review_time {
|
||||||
|
Some(timing.next_day_at.elapsed_days_since(last_review_time) as u32)
|
||||||
|
} else if !self.is_due_in_days() {
|
||||||
Some(
|
Some(
|
||||||
(timing.next_day_at.0 as u32).saturating_sub(self.original_or_current_due() as u32)
|
(timing.next_day_at.0 as u32).saturating_sub(self.original_or_current_due() as u32)
|
||||||
/ 86_400,
|
/ 86_400,
|
||||||
|
|
|
@ -96,6 +96,7 @@ pub struct Card {
|
||||||
pub(crate) memory_state: Option<FsrsMemoryState>,
|
pub(crate) memory_state: Option<FsrsMemoryState>,
|
||||||
pub(crate) desired_retention: Option<f32>,
|
pub(crate) desired_retention: Option<f32>,
|
||||||
pub(crate) decay: Option<f32>,
|
pub(crate) decay: Option<f32>,
|
||||||
|
pub(crate) last_review_time: Option<TimestampSecs>,
|
||||||
/// JSON object or empty; exposed through the reviewer for persisting custom
|
/// JSON object or empty; exposed through the reviewer for persisting custom
|
||||||
/// state
|
/// state
|
||||||
pub(crate) custom_data: String,
|
pub(crate) custom_data: String,
|
||||||
|
@ -147,6 +148,7 @@ impl Default for Card {
|
||||||
memory_state: None,
|
memory_state: None,
|
||||||
desired_retention: None,
|
desired_retention: None,
|
||||||
decay: None,
|
decay: None,
|
||||||
|
last_review_time: None,
|
||||||
custom_data: String::new(),
|
custom_data: String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,6 +107,7 @@ impl TryFrom<anki_proto::cards::Card> for Card {
|
||||||
memory_state: c.memory_state.map(Into::into),
|
memory_state: c.memory_state.map(Into::into),
|
||||||
desired_retention: c.desired_retention,
|
desired_retention: c.desired_retention,
|
||||||
decay: c.decay,
|
decay: c.decay,
|
||||||
|
last_review_time: c.last_review_time_secs.map(TimestampSecs),
|
||||||
custom_data: c.custom_data,
|
custom_data: c.custom_data,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -136,6 +137,7 @@ impl From<Card> for anki_proto::cards::Card {
|
||||||
memory_state: c.memory_state.map(Into::into),
|
memory_state: c.memory_state.map(Into::into),
|
||||||
desired_retention: c.desired_retention,
|
desired_retention: c.desired_retention,
|
||||||
decay: c.decay,
|
decay: c.decay,
|
||||||
|
last_review_time_secs: c.last_review_time.map(|t| t.0),
|
||||||
custom_data: c.custom_data,
|
custom_data: c.custom_data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -335,6 +335,12 @@ impl Collection {
|
||||||
self.maybe_bury_siblings(&original, &updater.config)?;
|
self.maybe_bury_siblings(&original, &updater.config)?;
|
||||||
let timing = updater.timing;
|
let timing = updater.timing;
|
||||||
let mut card = updater.into_card();
|
let mut card = updater.into_card();
|
||||||
|
if !matches!(
|
||||||
|
answer.current_state,
|
||||||
|
CardState::Filtered(FilteredState::Preview(_))
|
||||||
|
) {
|
||||||
|
card.last_review_time = Some(answer.answered_at.as_secs());
|
||||||
|
}
|
||||||
if let Some(data) = answer.custom_data.take() {
|
if let Some(data) = answer.custom_data.take() {
|
||||||
card.custom_data = data;
|
card.custom_data = data;
|
||||||
card.validate_custom_data()?;
|
card.validate_custom_data()?;
|
||||||
|
@ -451,11 +457,14 @@ impl Collection {
|
||||||
)?;
|
)?;
|
||||||
card.set_memory_state(&fsrs, item, config.inner.historical_retention)?;
|
card.set_memory_state(&fsrs, item, config.inner.historical_retention)?;
|
||||||
}
|
}
|
||||||
let days_elapsed = self
|
let days_elapsed = if let Some(last_review_time) = card.last_review_time {
|
||||||
.storage
|
timing.next_day_at.elapsed_days_since(last_review_time) as u32
|
||||||
|
} else {
|
||||||
|
self.storage
|
||||||
.time_of_last_review(card.id)?
|
.time_of_last_review(card.id)?
|
||||||
.map(|ts| timing.next_day_at.elapsed_days_since(ts))
|
.map(|ts| timing.next_day_at.elapsed_days_since(ts))
|
||||||
.unwrap_or_default() as u32;
|
.unwrap_or_default() as u32
|
||||||
|
};
|
||||||
Some(fsrs.next_states(
|
Some(fsrs.next_states(
|
||||||
card.memory_state.map(Into::into),
|
card.memory_state.map(Into::into),
|
||||||
config.inner.desired_retention,
|
config.inner.desired_retention,
|
||||||
|
|
|
@ -36,6 +36,11 @@ impl Card {
|
||||||
let new_due = (today + days_from_today) as i32;
|
let new_due = (today + days_from_today) as i32;
|
||||||
let fsrs_enabled = self.memory_state.is_some();
|
let fsrs_enabled = self.memory_state.is_some();
|
||||||
let new_interval = if fsrs_enabled {
|
let new_interval = if fsrs_enabled {
|
||||||
|
if let Some(last_review_time) = self.last_review_time {
|
||||||
|
let elapsed_days =
|
||||||
|
TimestampSecs(next_day_start).elapsed_days_since(last_review_time);
|
||||||
|
elapsed_days as u32 + days_from_today
|
||||||
|
} else {
|
||||||
let due = self.original_or_current_due();
|
let due = self.original_or_current_due();
|
||||||
let due_diff = if is_unix_epoch_timestamp(due) {
|
let due_diff = if is_unix_epoch_timestamp(due) {
|
||||||
let offset = (due as i64 - next_day_start) / 86_400;
|
let offset = (due as i64 - next_day_start) / 86_400;
|
||||||
|
@ -45,6 +50,7 @@ impl Card {
|
||||||
new_due - due
|
new_due - due
|
||||||
};
|
};
|
||||||
self.interval.saturating_add_signed(due_diff)
|
self.interval.saturating_add_signed(due_diff)
|
||||||
|
}
|
||||||
} else if force_reset || !matches!(self.ctype, CardType::Review | CardType::Relearn) {
|
} else if force_reset || !matches!(self.ctype, CardType::Review | CardType::Relearn) {
|
||||||
days_from_today.max(1)
|
days_from_today.max(1)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -30,11 +30,14 @@ impl Collection {
|
||||||
|
|
||||||
let (average_secs, total_secs) = average_and_total_secs_strings(&revlog);
|
let (average_secs, total_secs) = average_and_total_secs_strings(&revlog);
|
||||||
let timing = self.timing_today()?;
|
let timing = self.timing_today()?;
|
||||||
let seconds_elapsed = self
|
let seconds_elapsed = if let Some(last_review_time) = card.last_review_time {
|
||||||
.storage
|
timing.now.elapsed_secs_since(last_review_time) as u32
|
||||||
|
} else {
|
||||||
|
self.storage
|
||||||
.time_of_last_review(card.id)?
|
.time_of_last_review(card.id)?
|
||||||
.map(|ts| timing.now.elapsed_secs_since(ts))
|
.map(|ts| timing.now.elapsed_secs_since(ts))
|
||||||
.unwrap_or_default() as u32;
|
.unwrap_or_default() as u32
|
||||||
|
};
|
||||||
let fsrs_retrievability = card
|
let fsrs_retrievability = card
|
||||||
.memory_state
|
.memory_state
|
||||||
.zip(Some(seconds_elapsed))
|
.zip(Some(seconds_elapsed))
|
||||||
|
|
|
@ -47,6 +47,12 @@ pub(crate) struct CardData {
|
||||||
deserialize_with = "default_on_invalid"
|
deserialize_with = "default_on_invalid"
|
||||||
)]
|
)]
|
||||||
pub(crate) decay: Option<f32>,
|
pub(crate) decay: Option<f32>,
|
||||||
|
#[serde(
|
||||||
|
rename = "lrt",
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
deserialize_with = "default_on_invalid"
|
||||||
|
)]
|
||||||
|
pub(crate) last_review_time: Option<TimestampSecs>,
|
||||||
|
|
||||||
/// A string representation of a JSON object storing optional data
|
/// A string representation of a JSON object storing optional data
|
||||||
/// associated with the card, so v3 custom scheduling code can persist
|
/// associated with the card, so v3 custom scheduling code can persist
|
||||||
|
@ -63,6 +69,7 @@ impl CardData {
|
||||||
fsrs_difficulty: card.memory_state.as_ref().map(|m| m.difficulty),
|
fsrs_difficulty: card.memory_state.as_ref().map(|m| m.difficulty),
|
||||||
fsrs_desired_retention: card.desired_retention,
|
fsrs_desired_retention: card.desired_retention,
|
||||||
decay: card.decay,
|
decay: card.decay,
|
||||||
|
last_review_time: card.last_review_time,
|
||||||
custom_data: card.custom_data.clone(),
|
custom_data: card.custom_data.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -169,6 +176,7 @@ mod test {
|
||||||
fsrs_difficulty: Some(1.234567),
|
fsrs_difficulty: Some(1.234567),
|
||||||
fsrs_desired_retention: Some(0.987654),
|
fsrs_desired_retention: Some(0.987654),
|
||||||
decay: Some(0.123456),
|
decay: Some(0.123456),
|
||||||
|
last_review_time: None,
|
||||||
custom_data: "".to_string(),
|
custom_data: "".to_string(),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
|
@ -97,6 +97,7 @@ fn row_to_card(row: &Row) -> result::Result<Card, rusqlite::Error> {
|
||||||
memory_state: data.memory_state(),
|
memory_state: data.memory_state(),
|
||||||
desired_retention: data.fsrs_desired_retention,
|
desired_retention: data.fsrs_desired_retention,
|
||||||
decay: data.decay,
|
decay: data.decay,
|
||||||
|
last_review_time: data.last_review_time,
|
||||||
custom_data: data.custom_data,
|
custom_data: data.custom_data,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -328,7 +328,13 @@ fn add_extract_fsrs_retrievability(db: &Connection) -> rusqlite::Result<()> {
|
||||||
let Ok(due) = ctx.get_raw(1).as_i64() else {
|
let Ok(due) = ctx.get_raw(1).as_i64() else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
let days_elapsed = if due > 365_000 {
|
let days_elapsed = if let Some(last_review_time) = card_data.last_review_time {
|
||||||
|
// Use last_review_time to calculate days_elapsed
|
||||||
|
let Ok(next_day_at) = ctx.get_raw(4).as_i64() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
(next_day_at as u32).saturating_sub(last_review_time.0 as u32) / 86_400
|
||||||
|
} else if due > 365_000 {
|
||||||
// (re)learning card in seconds
|
// (re)learning card in seconds
|
||||||
let Ok(next_day_at) = ctx.get_raw(4).as_i64() else {
|
let Ok(next_day_at) = ctx.get_raw(4).as_i64() else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
@ -396,6 +402,14 @@ fn add_extract_fsrs_relative_retrievability(db: &Connection) -> rusqlite::Result
|
||||||
desired_retrievability = desired_retrievability.max(0.0001);
|
desired_retrievability = desired_retrievability.max(0.0001);
|
||||||
let decay = card_data.decay.unwrap_or(FSRS5_DEFAULT_DECAY);
|
let decay = card_data.decay.unwrap_or(FSRS5_DEFAULT_DECAY);
|
||||||
|
|
||||||
|
let days_elapsed = if let Some(last_review_time) =
|
||||||
|
card_data.last_review_time
|
||||||
|
{
|
||||||
|
TimestampSecs(next_day_at).elapsed_days_since(last_review_time) as u32
|
||||||
|
} else {
|
||||||
|
days_elapsed
|
||||||
|
};
|
||||||
|
|
||||||
let current_retrievability = FSRS::new(None)
|
let current_retrievability = FSRS::new(None)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.current_retrievability(state.into(), days_elapsed, decay)
|
.current_retrievability(state.into(), days_elapsed, decay)
|
||||||
|
|
|
@ -333,6 +333,7 @@ impl From<CardEntry> for Card {
|
||||||
memory_state: data.memory_state(),
|
memory_state: data.memory_state(),
|
||||||
desired_retention: data.fsrs_desired_retention,
|
desired_retention: data.fsrs_desired_retention,
|
||||||
decay: data.decay,
|
decay: data.decay,
|
||||||
|
last_review_time: data.last_review_time,
|
||||||
custom_data: data.custom_data,
|
custom_data: data.custom_data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue