mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
Fix/use current_retrievability_seconds in SQL to keep consistent with card info (#4231)
* Feat/use current_retrievability_seconds in SQL * replace `days_since_last_review` with `seconds_since_last_review` * Update is_due_in_days logic to include original or current due date check
This commit is contained in:
parent
3dc6b6b3ca
commit
4dc00556c1
7 changed files with 78 additions and 57 deletions
|
@ -105,7 +105,8 @@ impl Card {
|
||||||
|
|
||||||
/// Returns true if the card has a due date in terms of days.
|
/// Returns true if the card has a due date in terms of days.
|
||||||
fn is_due_in_days(&self) -> bool {
|
fn is_due_in_days(&self) -> bool {
|
||||||
matches!(self.queue, CardQueue::DayLearn | CardQueue::Review)
|
self.original_or_current_due() <= 365_000 // keep consistent with SQL
|
||||||
|
|| matches!(self.queue, CardQueue::DayLearn | CardQueue::Review)
|
||||||
|| (self.ctype == CardType::Review && self.is_undue_queue())
|
|| (self.ctype == CardType::Review && self.is_undue_queue())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,19 +128,17 @@ 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 seconds_since_last_review(&self, timing: &SchedTimingToday) -> Option<u32> {
|
||||||
if let Some(last_review_time) = self.last_review_time {
|
if let Some(last_review_time) = self.last_review_time {
|
||||||
Some(timing.next_day_at.elapsed_days_since(last_review_time) as u32)
|
Some(timing.now.elapsed_secs_since(last_review_time) as u32)
|
||||||
} else if !self.is_due_in_days() {
|
} else if !self.is_due_in_days() {
|
||||||
Some(
|
let last_review_time =
|
||||||
(timing.next_day_at.0 as u32).saturating_sub(self.original_or_current_due() as u32)
|
TimestampSecs(self.original_or_current_due() as i64 - self.interval as i64);
|
||||||
/ 86_400,
|
Some(timing.now.elapsed_secs_since(last_review_time) as u32)
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
self.due_time(timing).map(|due| {
|
self.due_time(timing).map(|due| {
|
||||||
(due.adding_secs(-86_400 * self.interval as i64)
|
(due.adding_secs(-86_400 * self.interval as i64)
|
||||||
.elapsed_secs()
|
.elapsed_secs()) as u32
|
||||||
/ 86_400) as u32
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -543,12 +542,12 @@ impl RowContext {
|
||||||
self.cards[0]
|
self.cards[0]
|
||||||
.memory_state
|
.memory_state
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.zip(self.cards[0].days_since_last_review(&self.timing))
|
.zip(self.cards[0].seconds_since_last_review(&self.timing))
|
||||||
.zip(Some(self.cards[0].decay.unwrap_or(FSRS5_DEFAULT_DECAY)))
|
.zip(Some(self.cards[0].decay.unwrap_or(FSRS5_DEFAULT_DECAY)))
|
||||||
.map(|((state, days_elapsed), decay)| {
|
.map(|((state, seconds), decay)| {
|
||||||
let r = FSRS::new(None).unwrap().current_retrievability(
|
let r = FSRS::new(None).unwrap().current_retrievability_seconds(
|
||||||
(*state).into(),
|
(*state).into(),
|
||||||
days_elapsed,
|
seconds,
|
||||||
decay,
|
decay,
|
||||||
);
|
);
|
||||||
format!("{:.0}%", r * 100.)
|
format!("{:.0}%", r * 100.)
|
||||||
|
|
|
@ -378,9 +378,10 @@ fn card_order_from_sort_column(column: Column, timing: SchedTimingToday) -> Cow<
|
||||||
Column::Stability => "extract_fsrs_variable(c.data, 's') asc".into(),
|
Column::Stability => "extract_fsrs_variable(c.data, 's') asc".into(),
|
||||||
Column::Difficulty => "extract_fsrs_variable(c.data, 'd') asc".into(),
|
Column::Difficulty => "extract_fsrs_variable(c.data, 'd') asc".into(),
|
||||||
Column::Retrievability => format!(
|
Column::Retrievability => format!(
|
||||||
"extract_fsrs_retrievability(c.data, case when c.odue !=0 then c.odue else c.due end, c.ivl, {}, {}) asc",
|
"extract_fsrs_retrievability(c.data, case when c.odue !=0 then c.odue else c.due end, c.ivl, {}, {}, {}) asc",
|
||||||
timing.days_elapsed,
|
timing.days_elapsed,
|
||||||
timing.next_day_at.0
|
timing.next_day_at.0,
|
||||||
|
timing.now.0,
|
||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -418,13 +418,13 @@ impl SqlWriter<'_> {
|
||||||
write!(self.sql, "extract_fsrs_variable(c.data, 'd') {op} {d}").unwrap()
|
write!(self.sql, "extract_fsrs_variable(c.data, 'd') {op} {d}").unwrap()
|
||||||
}
|
}
|
||||||
PropertyKind::Retrievability(r) => {
|
PropertyKind::Retrievability(r) => {
|
||||||
let (elap, next_day_at) = {
|
let (elap, next_day_at, now) = {
|
||||||
let timing = self.col.timing_today()?;
|
let timing = self.col.timing_today()?;
|
||||||
(timing.days_elapsed, timing.next_day_at)
|
(timing.days_elapsed, timing.next_day_at, timing.now)
|
||||||
};
|
};
|
||||||
write!(
|
write!(
|
||||||
self.sql,
|
self.sql,
|
||||||
"extract_fsrs_retrievability(c.data, case when c.odue !=0 then c.odue else c.due end, c.ivl, {elap}, {next_day_at}) {op} {r}"
|
"extract_fsrs_retrievability(c.data, case when c.odue !=0 then c.odue else c.due end, c.ivl, {elap}, {next_day_at}, {now}) {op} {r}"
|
||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,10 +30,10 @@ impl GraphsContext {
|
||||||
.or_insert((0.0, 0));
|
.or_insert((0.0, 0));
|
||||||
entry.1 += 1;
|
entry.1 += 1;
|
||||||
if let Some(state) = card.memory_state {
|
if let Some(state) = card.memory_state {
|
||||||
let elapsed_days = card.days_since_last_review(&timing).unwrap_or_default();
|
let elapsed_seconds = card.seconds_since_last_review(&timing).unwrap_or_default();
|
||||||
let r = fsrs.current_retrievability(
|
let r = fsrs.current_retrievability_seconds(
|
||||||
state.into(),
|
state.into(),
|
||||||
elapsed_days,
|
elapsed_seconds,
|
||||||
card.decay.unwrap_or(FSRS5_DEFAULT_DECAY),
|
card.decay.unwrap_or(FSRS5_DEFAULT_DECAY),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,8 @@ pub(crate) fn order_and_limit_for_search(
|
||||||
) -> String {
|
) -> String {
|
||||||
let temp_string;
|
let temp_string;
|
||||||
let today = timing.days_elapsed;
|
let today = timing.days_elapsed;
|
||||||
|
let next_day_at = timing.next_day_at.0;
|
||||||
|
let now = timing.now.0;
|
||||||
let order = match term.order() {
|
let order = match term.order() {
|
||||||
FilteredSearchOrder::OldestReviewedFirst => "(select max(id) from revlog where cid=c.id)",
|
FilteredSearchOrder::OldestReviewedFirst => "(select max(id) from revlog where cid=c.id)",
|
||||||
FilteredSearchOrder::Random => "random()",
|
FilteredSearchOrder::Random => "random()",
|
||||||
|
@ -29,15 +31,13 @@ pub(crate) fn order_and_limit_for_search(
|
||||||
&temp_string
|
&temp_string
|
||||||
}
|
}
|
||||||
FilteredSearchOrder::RetrievabilityAscending => {
|
FilteredSearchOrder::RetrievabilityAscending => {
|
||||||
let next_day_at = timing.next_day_at.0;
|
|
||||||
temp_string =
|
temp_string =
|
||||||
build_retrievability_query(fsrs, today, next_day_at, SqlSortOrder::Ascending);
|
build_retrievability_query(fsrs, today, next_day_at, now, SqlSortOrder::Ascending);
|
||||||
&temp_string
|
&temp_string
|
||||||
}
|
}
|
||||||
FilteredSearchOrder::RetrievabilityDescending => {
|
FilteredSearchOrder::RetrievabilityDescending => {
|
||||||
let next_day_at = timing.next_day_at.0;
|
|
||||||
temp_string =
|
temp_string =
|
||||||
build_retrievability_query(fsrs, today, next_day_at, SqlSortOrder::Descending);
|
build_retrievability_query(fsrs, today, next_day_at, now, SqlSortOrder::Descending);
|
||||||
&temp_string
|
&temp_string
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -49,11 +49,12 @@ fn build_retrievability_query(
|
||||||
fsrs: bool,
|
fsrs: bool,
|
||||||
today: u32,
|
today: u32,
|
||||||
next_day_at: i64,
|
next_day_at: i64,
|
||||||
|
now: i64,
|
||||||
order: SqlSortOrder,
|
order: SqlSortOrder,
|
||||||
) -> String {
|
) -> String {
|
||||||
if fsrs {
|
if fsrs {
|
||||||
format!(
|
format!(
|
||||||
"extract_fsrs_relative_retrievability(c.data, case when c.odue !=0 then c.odue else c.due end, {today}, ivl, {next_day_at}) {order}"
|
"extract_fsrs_relative_retrievability(c.data, case when c.odue !=0 then c.odue else c.due end, {today}, ivl, {next_day_at}, {now}) {order}"
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
format!(
|
format!(
|
||||||
|
|
|
@ -808,8 +808,9 @@ impl fmt::Display for ReviewOrderSubclause {
|
||||||
ReviewOrderSubclause::RetrievabilityFsrs { timing, order } => {
|
ReviewOrderSubclause::RetrievabilityFsrs { timing, order } => {
|
||||||
let today = timing.days_elapsed;
|
let today = timing.days_elapsed;
|
||||||
let next_day_at = timing.next_day_at.0;
|
let next_day_at = timing.next_day_at.0;
|
||||||
|
let now = timing.now.0;
|
||||||
temp_string =
|
temp_string =
|
||||||
format!("extract_fsrs_relative_retrievability(data, case when odue !=0 then odue else due end, {today}, ivl, {next_day_at}) {order}");
|
format!("extract_fsrs_relative_retrievability(data, case when odue !=0 then odue else due end, {today}, ivl, {next_day_at}, {now}) {order}");
|
||||||
&temp_string
|
&temp_string
|
||||||
}
|
}
|
||||||
ReviewOrderSubclause::Added => "nid asc, ord asc",
|
ReviewOrderSubclause::Added => "nid asc, ord asc",
|
||||||
|
|
|
@ -310,14 +310,14 @@ fn add_extract_fsrs_variable(db: &Connection) -> rusqlite::Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// eg. extract_fsrs_retrievability(card.data, card.due, card.ivl,
|
/// eg. extract_fsrs_retrievability(card.data, card.due, card.ivl,
|
||||||
/// timing.days_elapsed, timing.next_day_at) -> float | null
|
/// timing.days_elapsed, timing.next_day_at, timing.now) -> float | null
|
||||||
fn add_extract_fsrs_retrievability(db: &Connection) -> rusqlite::Result<()> {
|
fn add_extract_fsrs_retrievability(db: &Connection) -> rusqlite::Result<()> {
|
||||||
db.create_scalar_function(
|
db.create_scalar_function(
|
||||||
"extract_fsrs_retrievability",
|
"extract_fsrs_retrievability",
|
||||||
5,
|
6,
|
||||||
FunctionFlags::SQLITE_DETERMINISTIC,
|
FunctionFlags::SQLITE_DETERMINISTIC,
|
||||||
move |ctx| {
|
move |ctx| {
|
||||||
assert_eq!(ctx.len(), 5, "called with unexpected number of arguments");
|
assert_eq!(ctx.len(), 6, "called with unexpected number of arguments");
|
||||||
let Ok(card_data) = ctx.get_raw(0).as_str() else {
|
let Ok(card_data) = ctx.get_raw(0).as_str() else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
|
@ -328,18 +328,18 @@ 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 let Some(last_review_time) = card_data.last_review_time {
|
let Ok(now) = ctx.get_raw(5).as_i64() else {
|
||||||
// Use last_review_time to calculate days_elapsed
|
return Ok(None);
|
||||||
let Ok(next_day_at) = ctx.get_raw(4).as_i64() else {
|
};
|
||||||
return Ok(None);
|
let seconds_elapsed = if let Some(last_review_time) = card_data.last_review_time {
|
||||||
};
|
now.saturating_sub(last_review_time.0) as u32
|
||||||
(next_day_at as u32).saturating_sub(last_review_time.0 as u32) / 86_400
|
|
||||||
} else if due > 365_000 {
|
} 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(ivl) = ctx.get_raw(2).as_i64() else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
(next_day_at as u32).saturating_sub(due as u32) / 86_400
|
let last_review_time = due.saturating_sub(ivl);
|
||||||
|
now.saturating_sub(last_review_time) as u32
|
||||||
} else {
|
} else {
|
||||||
let Ok(ivl) = ctx.get_raw(2).as_i64() else {
|
let Ok(ivl) = ctx.get_raw(2).as_i64() else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
@ -348,29 +348,32 @@ fn add_extract_fsrs_retrievability(db: &Connection) -> rusqlite::Result<()> {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
let review_day = due.saturating_sub(ivl);
|
let review_day = due.saturating_sub(ivl);
|
||||||
(days_elapsed as u32).saturating_sub(review_day as u32)
|
days_elapsed.saturating_sub(review_day) as u32 * 86_400
|
||||||
};
|
};
|
||||||
let decay = card_data.decay.unwrap_or(FSRS5_DEFAULT_DECAY);
|
let decay = card_data.decay.unwrap_or(FSRS5_DEFAULT_DECAY);
|
||||||
Ok(card_data.memory_state().map(|state| {
|
let retrievability = card_data.memory_state().map(|state| {
|
||||||
FSRS::new(None)
|
FSRS::new(None).unwrap().current_retrievability_seconds(
|
||||||
.unwrap()
|
state.into(),
|
||||||
.current_retrievability(state.into(), days_elapsed, decay)
|
seconds_elapsed,
|
||||||
}))
|
decay,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
Ok(retrievability)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// eg. extract_fsrs_relative_retrievability(card.data, card.due,
|
/// eg. extract_fsrs_relative_retrievability(card.data, card.due,
|
||||||
/// timing.days_elapsed, card.ivl, timing.next_day_at) -> float | null. The
|
/// timing.days_elapsed, card.ivl, timing.next_day_at, timing.now) -> float |
|
||||||
/// higher the number, the higher the card's retrievability relative to the
|
/// null. The higher the number, the higher the card's retrievability relative
|
||||||
/// configured desired retention.
|
/// to the configured desired retention.
|
||||||
fn add_extract_fsrs_relative_retrievability(db: &Connection) -> rusqlite::Result<()> {
|
fn add_extract_fsrs_relative_retrievability(db: &Connection) -> rusqlite::Result<()> {
|
||||||
db.create_scalar_function(
|
db.create_scalar_function(
|
||||||
"extract_fsrs_relative_retrievability",
|
"extract_fsrs_relative_retrievability",
|
||||||
5,
|
6,
|
||||||
FunctionFlags::SQLITE_DETERMINISTIC,
|
FunctionFlags::SQLITE_DETERMINISTIC,
|
||||||
move |ctx| {
|
move |ctx| {
|
||||||
assert_eq!(ctx.len(), 5, "called with unexpected number of arguments");
|
assert_eq!(ctx.len(), 6, "called with unexpected number of arguments");
|
||||||
|
|
||||||
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);
|
||||||
|
@ -381,6 +384,9 @@ fn add_extract_fsrs_relative_retrievability(db: &Connection) -> rusqlite::Result
|
||||||
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);
|
||||||
};
|
};
|
||||||
|
let Ok(now) = ctx.get_raw(5).as_i64() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
let days_elapsed = if due > 365_000 {
|
let days_elapsed = if due > 365_000 {
|
||||||
// (re)learning
|
// (re)learning
|
||||||
(next_day_at as u32).saturating_sub(due as u32) / 86_400
|
(next_day_at as u32).saturating_sub(due as u32) / 86_400
|
||||||
|
@ -402,17 +408,30 @@ 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) =
|
let seconds_elapsed =
|
||||||
card_data.last_review_time
|
if let Some(last_review_time) = card_data.last_review_time {
|
||||||
{
|
now.saturating_sub(last_review_time.0) as u32
|
||||||
TimestampSecs(next_day_at).elapsed_days_since(last_review_time) as u32
|
} else if due > 365_000 {
|
||||||
} else {
|
// (re)learning card in seconds
|
||||||
days_elapsed
|
let Ok(ivl) = ctx.get_raw(2).as_i64() else {
|
||||||
};
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let last_review_time = due.saturating_sub(ivl);
|
||||||
|
now.saturating_sub(last_review_time) as u32
|
||||||
|
} else {
|
||||||
|
let Ok(ivl) = ctx.get_raw(2).as_i64() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let Ok(days_elapsed) = ctx.get_raw(3).as_i64() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let review_day = due.saturating_sub(ivl);
|
||||||
|
days_elapsed.saturating_sub(review_day) as u32 * 86_400
|
||||||
|
};
|
||||||
|
|
||||||
let current_retrievability = FSRS::new(None)
|
let current_retrievability = FSRS::new(None)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.current_retrievability(state.into(), days_elapsed, decay)
|
.current_retrievability_seconds(state.into(), seconds_elapsed, decay)
|
||||||
.max(0.0001);
|
.max(0.0001);
|
||||||
|
|
||||||
return Ok(Some(
|
return Ok(Some(
|
||||||
|
|
Loading…
Reference in a new issue