diff --git a/rslib/src/browser_table.rs b/rslib/src/browser_table.rs index f61f82773..830f1b54a 100644 --- a/rslib/src/browser_table.rs +++ b/rslib/src/browser_table.rs @@ -127,7 +127,7 @@ impl Card { /// 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) + Some((timing.next_day_at.0 as u32).saturating_sub(self.due.max(0) as u32) / 86_400) } else { self.due_time(timing).map(|due| { due.adding_secs(-86_400 * self.interval as i64) diff --git a/rslib/src/scheduler/filtered/mod.rs b/rslib/src/scheduler/filtered/mod.rs index 56c2f45f5..f1f3cc07d 100644 --- a/rslib/src/scheduler/filtered/mod.rs +++ b/rslib/src/scheduler/filtered/mod.rs @@ -10,6 +10,7 @@ use crate::decks::FilteredDeck; use crate::decks::FilteredSearchTerm; use crate::error::FilteredDeckError; use crate::prelude::*; +use crate::scheduler::timing::SchedTimingToday; use crate::search::writer::deck_search; use crate::search::writer::normalize_search; use crate::search::SortMode; @@ -28,7 +29,7 @@ pub(crate) struct DeckFilterContext<'a> { pub target_deck: DeckId, pub config: &'a FilteredDeck, pub usn: Usn, - pub today: u32, + pub timing: SchedTimingToday, } impl Collection { @@ -123,7 +124,7 @@ impl Collection { 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))? { let original = card.clone(); @@ -186,11 +187,12 @@ impl Collection { } let config = deck.filtered()?; + let timing = self.timing_today()?; let ctx = DeckFilterContext { target_deck: deck.id, config, usn, - today: self.timing_today()?.days_elapsed, + timing, }; self.return_all_cards_in_filtered_deck(deck.id)?; diff --git a/rslib/src/scheduler/queue/builder/gathering.rs b/rslib/src/scheduler/queue/builder/gathering.rs index 349951098..f413c49ab 100644 --- a/rslib/src/scheduler/queue/builder/gathering.rs +++ b/rslib/src/scheduler/queue/builder/gathering.rs @@ -37,7 +37,7 @@ impl QueueBuilder { return Ok(()); } col.storage.for_each_due_card_in_active_decks( - self.context.timing.days_elapsed, + self.context.timing, self.context.sort_options.review_order, kind, self.context.fsrs, diff --git a/rslib/src/search/mod.rs b/rslib/src/search/mod.rs index ba8178e68..905585437 100644 --- a/rslib/src/search/mod.rs +++ b/rslib/src/search/mod.rs @@ -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::Difficulty => "extract_fsrs_variable(c.data, 'd') asc".into(), Column::Retrievability => format!( - "extract_fsrs_retrievability(c.data, case when c.odue !=0 then c.odue else c.due end, c.ivl, {}) asc", - timing.days_elapsed + "extract_fsrs_retrievability(c.data, case when c.odue !=0 then c.odue else c.due end, c.ivl, {}, {}) asc", + timing.days_elapsed, + timing.next_day_at.0 ) .into(), } diff --git a/rslib/src/search/sqlwriter.rs b/rslib/src/search/sqlwriter.rs index 95f1ff3b7..456144f89 100644 --- a/rslib/src/search/sqlwriter.rs +++ b/rslib/src/search/sqlwriter.rs @@ -380,10 +380,13 @@ impl SqlWriter<'_> { write!(self.sql, "extract_fsrs_variable(c.data, 'd') {op} {d}").unwrap() } 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!( 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() } diff --git a/rslib/src/storage/card/filtered.rs b/rslib/src/storage/card/filtered.rs index 812a0a2a6..16024e983 100644 --- a/rslib/src/storage/card/filtered.rs +++ b/rslib/src/storage/card/filtered.rs @@ -4,14 +4,15 @@ use crate::card::CardQueue; use crate::decks::FilteredSearchOrder; use crate::decks::FilteredSearchTerm; +use crate::scheduler::timing::SchedTimingToday; pub(crate) fn order_and_limit_for_search( term: &FilteredSearchTerm, - today: u32, - current_timestamp: i64, + timing: SchedTimingToday, fsrs: bool, ) -> String { let temp_string; + let today = timing.days_elapsed; let order = match term.order() { FilteredSearchOrder::OldestReviewedFirst => "(select max(id) from revlog where cid=c.id)", FilteredSearchOrder::Random => "random()", @@ -21,13 +22,17 @@ pub(crate) fn order_and_limit_for_search( FilteredSearchOrder::Added => "n.id, c.ord", FilteredSearchOrder::ReverseAdded => "n.id desc", FilteredSearchOrder::Due => { + let current_timestamp = timing.now.0; temp_string = format!( "(case when c.due > 1000000000 then due else (due - {today}) * 86400 + {current_timestamp} end), c.ord"); &temp_string } FilteredSearchOrder::DuePriority => { + let next_day_at = timing.next_day_at.0; 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 { format!( " diff --git a/rslib/src/storage/card/mod.rs b/rslib/src/storage/card/mod.rs index f69024c35..27ec7b818 100644 --- a/rslib/src/storage/card/mod.rs +++ b/rslib/src/storage/card/mod.rs @@ -35,6 +35,7 @@ use crate::scheduler::queue::BuryMode; use crate::scheduler::queue::DueCard; use crate::scheduler::queue::DueCardKind; use crate::scheduler::queue::NewCard; +use crate::scheduler::timing::SchedTimingToday; use crate::timestamp::TimestampMillis; use crate::timestamp::TimestampSecs; use crate::types::Usn; @@ -250,7 +251,7 @@ impl super::SqliteStorage { /// when it returns false or no more cards found. pub(crate) fn for_each_due_card_in_active_decks( &self, - day_cutoff: u32, + timing: SchedTimingToday, order: ReviewCardOrder, kind: DueCardKind, fsrs: bool, @@ -259,7 +260,7 @@ impl super::SqliteStorage { where F: FnMut(DueCard) -> Result, { - 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!( "{} order by {}", include_str!("due_cards.sql"), @@ -269,7 +270,7 @@ impl super::SqliteStorage { DueCardKind::Review => CardQueue::Review, 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()? { if !func(DueCard { id: row.get(0)?, @@ -708,7 +709,7 @@ enum ReviewOrderSubclause { today: u32, }, 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 } - ReviewOrderSubclause::RelativeOverduenessFsrs { today } => { + ReviewOrderSubclause::RelativeOverduenessFsrs { timing } => { + let today = timing.days_elapsed; + let next_day_at = timing.next_day_at.0; 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 } }; @@ -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 { ReviewCardOrder::Day => vec![ReviewOrderSubclause::Day], ReviewCardOrder::DayThenDeck => vec![ReviewOrderSubclause::Day, ReviewOrderSubclause::Deck], @@ -760,9 +763,11 @@ fn review_order_sql(order: ReviewCardOrder, today: u32, fsrs: bool) -> String { }], ReviewCardOrder::RelativeOverdueness => { vec![if fsrs { - ReviewOrderSubclause::RelativeOverduenessFsrs { today } + ReviewOrderSubclause::RelativeOverduenessFsrs { timing } } else { - ReviewOrderSubclause::RelativeOverdueness { today } + ReviewOrderSubclause::RelativeOverdueness { + today: timing.days_elapsed, + } }] } ReviewCardOrder::Random => vec![], diff --git a/rslib/src/storage/sqlite.rs b/rslib/src/storage/sqlite.rs index 60190ab85..f7d70d00b 100644 --- a/rslib/src/storage/sqlite.rs +++ b/rslib/src/storage/sqlite.rs @@ -266,15 +266,14 @@ fn add_extract_fsrs_variable(db: &Connection) -> rusqlite::Result<()> { } /// 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<()> { db.create_scalar_function( "extract_fsrs_retrievability", - 4, + 5, FunctionFlags::SQLITE_DETERMINISTIC, 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 { return Ok(None); }; @@ -286,8 +285,11 @@ fn add_extract_fsrs_retrievability(db: &Connection) -> rusqlite::Result<()> { return Ok(None); }; let days_elapsed = if due > 365_000 { - // (re)learning card, assume 0 days have elapsed - 0 + // (re)learning card in seconds + 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 { let Ok(ivl) = ctx.get_raw(2).as_i64() else { 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, -/// card.ivl) -> float | null. The higher the number, the more overdue. +/// eg. extract_fsrs_relative_overdueness(card.data, card.due, +/// 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<()> { db.create_scalar_function( "extract_fsrs_relative_overdueness", - 4, + 5, FunctionFlags::SQLITE_DETERMINISTIC, 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 { return Ok(None); @@ -327,15 +330,22 @@ fn add_extract_fsrs_relative_overdueness(db: &Connection) -> rusqlite::Result<() let Ok(due) = ctx.get_raw(1).as_i64() else { return Ok(None); }; - if due > 365_000 { - // learning card - return Ok(None); - } - let Ok(days_elapsed) = ctx.get_raw(2).as_i64() else { - return Ok(None); - }; - let Ok(interval) = ctx.get_raw(3).as_i64() else { - return Ok(None); + let days_elapsed = if due > 365_000 { + // (re)learning + 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 { + let Ok(days_elapsed) = ctx.get_raw(2).as_i64() else { + return Ok(None); + }; + let Ok(interval) = ctx.get_raw(3).as_i64() else { + 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 { return Ok(None); @@ -346,8 +356,6 @@ fn add_extract_fsrs_relative_overdueness(db: &Connection) -> rusqlite::Result<() // avoid div by zero 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) .unwrap() .current_retrievability(state.into(), days_elapsed)