Calculate elapsed days for intraday learning cards

https://forums.ankiweb.net/t/anki-23-12-beta/37771/109
This commit is contained in:
Damien Elmes 2023-12-13 10:18:29 +10:00
parent 400686d2ae
commit edd38ca067
8 changed files with 66 additions and 42 deletions

View file

@ -127,7 +127,7 @@ impl Card {
/// 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 !self.is_due_in_days() {
Some(0) Some((timing.next_day_at.0 as u32).saturating_sub(self.due.max(0) as u32) / 86_400)
} 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)

View file

@ -10,6 +10,7 @@ use crate::decks::FilteredDeck;
use crate::decks::FilteredSearchTerm; use crate::decks::FilteredSearchTerm;
use crate::error::FilteredDeckError; use crate::error::FilteredDeckError;
use crate::prelude::*; use crate::prelude::*;
use crate::scheduler::timing::SchedTimingToday;
use crate::search::writer::deck_search; use crate::search::writer::deck_search;
use crate::search::writer::normalize_search; use crate::search::writer::normalize_search;
use crate::search::SortMode; use crate::search::SortMode;
@ -28,7 +29,7 @@ pub(crate) struct DeckFilterContext<'a> {
pub target_deck: DeckId, pub target_deck: DeckId,
pub config: &'a FilteredDeck, pub config: &'a FilteredDeck,
pub usn: Usn, pub usn: Usn,
pub today: u32, pub timing: SchedTimingToday,
} }
impl Collection { impl Collection {
@ -123,7 +124,7 @@ impl Collection {
format!("({})", term.search) format!("({})", term.search)
} }
); );
let order = order_and_limit_for_search(term, ctx.today, TimestampSecs::now().0, fsrs); let order = order_and_limit_for_search(term, ctx.timing, fsrs);
for mut card in self.all_cards_for_search_in_order(&search, SortMode::Custom(order))? { for mut card in self.all_cards_for_search_in_order(&search, SortMode::Custom(order))? {
let original = card.clone(); let original = card.clone();
@ -186,11 +187,12 @@ impl Collection {
} }
let config = deck.filtered()?; let config = deck.filtered()?;
let timing = self.timing_today()?;
let ctx = DeckFilterContext { let ctx = DeckFilterContext {
target_deck: deck.id, target_deck: deck.id,
config, config,
usn, usn,
today: self.timing_today()?.days_elapsed, timing,
}; };
self.return_all_cards_in_filtered_deck(deck.id)?; self.return_all_cards_in_filtered_deck(deck.id)?;

View file

@ -37,7 +37,7 @@ impl QueueBuilder {
return Ok(()); return Ok(());
} }
col.storage.for_each_due_card_in_active_decks( col.storage.for_each_due_card_in_active_decks(
self.context.timing.days_elapsed, self.context.timing,
self.context.sort_options.review_order, self.context.sort_options.review_order,
kind, kind,
self.context.fsrs, self.context.fsrs,

View file

@ -377,8 +377,9 @@ 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
) )
.into(), .into(),
} }

View file

@ -380,10 +380,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 = self.col.timing_today()?.days_elapsed; let (elap, next_day_at) = {
let timing = self.col.timing_today()?;
(timing.days_elapsed, timing.next_day_at)
};
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}) {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}) {op} {r}"
) )
.unwrap() .unwrap()
} }

View file

@ -4,14 +4,15 @@
use crate::card::CardQueue; use crate::card::CardQueue;
use crate::decks::FilteredSearchOrder; use crate::decks::FilteredSearchOrder;
use crate::decks::FilteredSearchTerm; use crate::decks::FilteredSearchTerm;
use crate::scheduler::timing::SchedTimingToday;
pub(crate) fn order_and_limit_for_search( pub(crate) fn order_and_limit_for_search(
term: &FilteredSearchTerm, term: &FilteredSearchTerm,
today: u32, timing: SchedTimingToday,
current_timestamp: i64,
fsrs: bool, fsrs: bool,
) -> String { ) -> String {
let temp_string; let temp_string;
let today = timing.days_elapsed;
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()",
@ -21,13 +22,17 @@ pub(crate) fn order_and_limit_for_search(
FilteredSearchOrder::Added => "n.id, c.ord", FilteredSearchOrder::Added => "n.id, c.ord",
FilteredSearchOrder::ReverseAdded => "n.id desc", FilteredSearchOrder::ReverseAdded => "n.id desc",
FilteredSearchOrder::Due => { FilteredSearchOrder::Due => {
let current_timestamp = timing.now.0;
temp_string = format!( temp_string = format!(
"(case when c.due > 1000000000 then due else (due - {today}) * 86400 + {current_timestamp} end), c.ord"); "(case when c.due > 1000000000 then due else (due - {today}) * 86400 + {current_timestamp} end), c.ord");
&temp_string &temp_string
} }
FilteredSearchOrder::DuePriority => { FilteredSearchOrder::DuePriority => {
let next_day_at = timing.next_day_at.0;
temp_string = if fsrs { temp_string = if fsrs {
format!("extract_fsrs_relative_overdueness(c.data, due, {today}, ivl) desc") format!(
"extract_fsrs_relative_overdueness(c.data, due, {today}, ivl, {next_day_at}) desc"
)
} else { } else {
format!( format!(
" "

View file

@ -35,6 +35,7 @@ use crate::scheduler::queue::BuryMode;
use crate::scheduler::queue::DueCard; use crate::scheduler::queue::DueCard;
use crate::scheduler::queue::DueCardKind; use crate::scheduler::queue::DueCardKind;
use crate::scheduler::queue::NewCard; use crate::scheduler::queue::NewCard;
use crate::scheduler::timing::SchedTimingToday;
use crate::timestamp::TimestampMillis; use crate::timestamp::TimestampMillis;
use crate::timestamp::TimestampSecs; use crate::timestamp::TimestampSecs;
use crate::types::Usn; use crate::types::Usn;
@ -250,7 +251,7 @@ impl super::SqliteStorage {
/// when it returns false or no more cards found. /// when it returns false or no more cards found.
pub(crate) fn for_each_due_card_in_active_decks<F>( pub(crate) fn for_each_due_card_in_active_decks<F>(
&self, &self,
day_cutoff: u32, timing: SchedTimingToday,
order: ReviewCardOrder, order: ReviewCardOrder,
kind: DueCardKind, kind: DueCardKind,
fsrs: bool, fsrs: bool,
@ -259,7 +260,7 @@ impl super::SqliteStorage {
where where
F: FnMut(DueCard) -> Result<bool>, F: FnMut(DueCard) -> Result<bool>,
{ {
let order_clause = review_order_sql(order, day_cutoff, fsrs); let order_clause = review_order_sql(order, timing, fsrs);
let mut stmt = self.db.prepare_cached(&format!( let mut stmt = self.db.prepare_cached(&format!(
"{} order by {}", "{} order by {}",
include_str!("due_cards.sql"), include_str!("due_cards.sql"),
@ -269,7 +270,7 @@ impl super::SqliteStorage {
DueCardKind::Review => CardQueue::Review, DueCardKind::Review => CardQueue::Review,
DueCardKind::Learning => CardQueue::DayLearn, DueCardKind::Learning => CardQueue::DayLearn,
}; };
let mut rows = stmt.query(params![queue as i8, day_cutoff])?; let mut rows = stmt.query(params![queue as i8, timing.days_elapsed])?;
while let Some(row) = rows.next()? { while let Some(row) = rows.next()? {
if !func(DueCard { if !func(DueCard {
id: row.get(0)?, id: row.get(0)?,
@ -708,7 +709,7 @@ enum ReviewOrderSubclause {
today: u32, today: u32,
}, },
RelativeOverduenessFsrs { RelativeOverduenessFsrs {
today: u32, timing: SchedTimingToday,
}, },
} }
@ -729,9 +730,11 @@ impl fmt::Display for ReviewOrderSubclause {
temp_string = format!("ivl / cast({today}-due+0.001 as real)", today = today); temp_string = format!("ivl / cast({today}-due+0.001 as real)", today = today);
&temp_string &temp_string
} }
ReviewOrderSubclause::RelativeOverduenessFsrs { today } => { ReviewOrderSubclause::RelativeOverduenessFsrs { timing } => {
let today = timing.days_elapsed;
let next_day_at = timing.next_day_at.0;
temp_string = temp_string =
format!("extract_fsrs_relative_overdueness(data, due, {today}, ivl) desc"); format!("extract_fsrs_relative_overdueness(data, due, {today}, ivl, {next_day_at}) desc");
&temp_string &temp_string
} }
}; };
@ -739,7 +742,7 @@ impl fmt::Display for ReviewOrderSubclause {
} }
} }
fn review_order_sql(order: ReviewCardOrder, today: u32, fsrs: bool) -> String { fn review_order_sql(order: ReviewCardOrder, timing: SchedTimingToday, fsrs: bool) -> String {
let mut subclauses = match order { let mut subclauses = match order {
ReviewCardOrder::Day => vec![ReviewOrderSubclause::Day], ReviewCardOrder::Day => vec![ReviewOrderSubclause::Day],
ReviewCardOrder::DayThenDeck => vec![ReviewOrderSubclause::Day, ReviewOrderSubclause::Deck], ReviewCardOrder::DayThenDeck => vec![ReviewOrderSubclause::Day, ReviewOrderSubclause::Deck],
@ -760,9 +763,11 @@ fn review_order_sql(order: ReviewCardOrder, today: u32, fsrs: bool) -> String {
}], }],
ReviewCardOrder::RelativeOverdueness => { ReviewCardOrder::RelativeOverdueness => {
vec![if fsrs { vec![if fsrs {
ReviewOrderSubclause::RelativeOverduenessFsrs { today } ReviewOrderSubclause::RelativeOverduenessFsrs { timing }
} else { } else {
ReviewOrderSubclause::RelativeOverdueness { today } ReviewOrderSubclause::RelativeOverdueness {
today: timing.days_elapsed,
}
}] }]
} }
ReviewCardOrder::Random => vec![], ReviewCardOrder::Random => vec![],

View file

@ -266,15 +266,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) -> float | null /// timing.days_elapsed, timing.next_day_at) -> 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",
4, 5,
FunctionFlags::SQLITE_DETERMINISTIC, FunctionFlags::SQLITE_DETERMINISTIC,
move |ctx| { move |ctx| {
assert_eq!(ctx.len(), 4, "called with unexpected number of arguments"); assert_eq!(ctx.len(), 5, "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);
}; };
@ -286,8 +285,11 @@ fn add_extract_fsrs_retrievability(db: &Connection) -> rusqlite::Result<()> {
return Ok(None); return Ok(None);
}; };
let days_elapsed = if due > 365_000 { let days_elapsed = if due > 365_000 {
// (re)learning card, assume 0 days have elapsed // (re)learning card in seconds
0 let Ok(next_day_at) = ctx.get_raw(4).as_i64() else {
return Ok(None);
};
(next_day_at as u32).saturating_sub(due.max(0) as u32) / 86_400
} 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);
@ -307,15 +309,16 @@ fn add_extract_fsrs_retrievability(db: &Connection) -> rusqlite::Result<()> {
) )
} }
/// eg. extract_fsrs_retrievability(card.data, card.due, timing.days_elapsed, /// eg. extract_fsrs_relative_overdueness(card.data, card.due,
/// card.ivl) -> float | null. The higher the number, the more overdue. /// timing.days_elapsed, card.ivl, timing.next_day_at) -> float | null. The
/// higher the number, the more overdue.
fn add_extract_fsrs_relative_overdueness(db: &Connection) -> rusqlite::Result<()> { fn add_extract_fsrs_relative_overdueness(db: &Connection) -> rusqlite::Result<()> {
db.create_scalar_function( db.create_scalar_function(
"extract_fsrs_relative_overdueness", "extract_fsrs_relative_overdueness",
4, 5,
FunctionFlags::SQLITE_DETERMINISTIC, FunctionFlags::SQLITE_DETERMINISTIC,
move |ctx| { move |ctx| {
assert_eq!(ctx.len(), 4, "called with unexpected number of arguments"); assert_eq!(ctx.len(), 5, "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);
@ -327,16 +330,23 @@ fn add_extract_fsrs_relative_overdueness(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);
}; };
if due > 365_000 { let days_elapsed = if due > 365_000 {
// learning card // (re)learning
let Ok(next_day_at) = ctx.get_raw(4).as_i64() else {
return Ok(None); return Ok(None);
} };
(next_day_at as u32).saturating_sub(due.max(0) as u32) / 86_400
} else {
let Ok(days_elapsed) = ctx.get_raw(2).as_i64() else { let Ok(days_elapsed) = ctx.get_raw(2).as_i64() else {
return Ok(None); return Ok(None);
}; };
let Ok(interval) = ctx.get_raw(3).as_i64() else { let Ok(interval) = ctx.get_raw(3).as_i64() else {
return Ok(None); return Ok(None);
}; };
let review_day = due.saturating_sub(interval);
days_elapsed.saturating_sub(review_day) as u32
};
let Some(state) = card_data.memory_state() else { let Some(state) = card_data.memory_state() else {
return Ok(None); return Ok(None);
}; };
@ -346,8 +356,6 @@ fn add_extract_fsrs_relative_overdueness(db: &Connection) -> rusqlite::Result<()
// avoid div by zero // avoid div by zero
desired_retrievability = desired_retrievability.max(0.0001); desired_retrievability = desired_retrievability.max(0.0001);
let review_day = due.saturating_sub(interval);
let days_elapsed = days_elapsed.saturating_sub(review_day) as u32;
let current_retrievability = FSRS::new(None) let current_retrievability = FSRS::new(None)
.unwrap() .unwrap()
.current_retrievability(state.into(), days_elapsed) .current_retrievability(state.into(), days_elapsed)