From b646f09c686819db6db9b42228bdaf041ac8eacb Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 8 Nov 2024 22:53:13 +1000 Subject: [PATCH] Add descending retrievability (#3559) * "relative overdueness" -> "retrievability ascending" * Add 'retrievability descending' --- ftl/core/deck-config.ftl | 4 ++- ftl/core/decks.ftl | 6 +++- proto/anki/deck_config.proto | 3 +- proto/anki/decks.proto | 3 +- rslib/src/decks/filtered.rs | 7 +++- rslib/src/scheduler/queue/builder/mod.rs | 2 +- rslib/src/storage/card/filtered.rs | 45 +++++++++++++++-------- rslib/src/storage/card/mod.rs | 46 ++++++++++++++++-------- rslib/src/storage/sqlite.rs | 33 +++++++++++++---- ts/routes/deck-options/choices.ts | 8 +++-- 10 files changed, 115 insertions(+), 42 deletions(-) diff --git a/ftl/core/deck-config.ftl b/ftl/core/deck-config.ftl index fe47ef824..0b1ef775f 100644 --- a/ftl/core/deck-config.ftl +++ b/ftl/core/deck-config.ftl @@ -209,7 +209,9 @@ deck-config-sort-order-ascending-ease = Ascending ease deck-config-sort-order-descending-ease = Descending ease deck-config-sort-order-ascending-difficulty = Easy cards first deck-config-sort-order-descending-difficulty = Difficult cards first -deck-config-sort-order-relative-overdueness = Relative overdueness +deck-config-sort-order-retrievability-ascending = Ascending retrievability +deck-config-sort-order-retrievability-descending = Descending retrievability + deck-config-display-order-will-use-current-deck = Anki will use the display order from the deck you select to study, and not any subdecks it may have. diff --git a/ftl/core/decks.ftl b/ftl/core/decks.ftl index e4369884b..48ecdb569 100644 --- a/ftl/core/decks.ftl +++ b/ftl/core/decks.ftl @@ -24,7 +24,6 @@ decks-order-added = Order added decks-order-due = Order due decks-please-select-something = Please select something. decks-random = Random -decks-relative-overdueness = Relative overdueness decks-repeat-failed-cards-after = Delay Repeat failed cards after # e.g. "Delay for Again", "Delay for Hard", "Delay for Good" decks-delay-for-button = Delay for { $button } @@ -37,3 +36,8 @@ decks-learn-header = Learn # The count of cards waiting to be reviewed decks-review-header = Due decks-zero-minutes-hint = (0 = return card to original deck) + +## These strings are no longer used - you do not need to translate them if they +## are not already translated. + +decks-relative-overdueness = Relative overdueness diff --git a/proto/anki/deck_config.proto b/proto/anki/deck_config.proto index c3fc5298b..efd8a80d0 100644 --- a/proto/anki/deck_config.proto +++ b/proto/anki/deck_config.proto @@ -79,7 +79,8 @@ message DeckConfig { REVIEW_CARD_ORDER_INTERVALS_DESCENDING = 4; REVIEW_CARD_ORDER_EASE_ASCENDING = 5; REVIEW_CARD_ORDER_EASE_DESCENDING = 6; - REVIEW_CARD_ORDER_RELATIVE_OVERDUENESS = 7; + REVIEW_CARD_ORDER_RETRIEVABILITY_ASCENDING = 7; + REVIEW_CARD_ORDER_RETRIEVABILITY_DESCENDING = 11; REVIEW_CARD_ORDER_RANDOM = 8; REVIEW_CARD_ORDER_ADDED = 9; REVIEW_CARD_ORDER_REVERSE_ADDED = 10; diff --git a/proto/anki/decks.proto b/proto/anki/decks.proto index c91abb39a..bcd206b06 100644 --- a/proto/anki/decks.proto +++ b/proto/anki/decks.proto @@ -97,7 +97,8 @@ message Deck { ADDED = 5; DUE = 6; REVERSE_ADDED = 7; - DUE_PRIORITY = 8; + RETRIEVABILITY_ASCENDING = 8; + RETRIEVABILITY_DESCENDING = 9; } string search = 1; diff --git a/rslib/src/decks/filtered.rs b/rslib/src/decks/filtered.rs index dcc8585bb..ef77076af 100644 --- a/rslib/src/decks/filtered.rs +++ b/rslib/src/decks/filtered.rs @@ -60,7 +60,12 @@ fn search_order_label(order: FilteredSearchOrder, tr: &I18n) -> String { FilteredSearchOrder::Added => tr.decks_order_added(), FilteredSearchOrder::Due => tr.decks_order_due(), FilteredSearchOrder::ReverseAdded => tr.decks_latest_added_first(), - FilteredSearchOrder::DuePriority => tr.decks_relative_overdueness(), + FilteredSearchOrder::RetrievabilityAscending => { + tr.deck_config_sort_order_retrievability_ascending() + } + FilteredSearchOrder::RetrievabilityDescending => { + tr.deck_config_sort_order_retrievability_descending() + } } .into() } diff --git a/rslib/src/scheduler/queue/builder/mod.rs b/rslib/src/scheduler/queue/builder/mod.rs index a95d056a8..c707f8df1 100644 --- a/rslib/src/scheduler/queue/builder/mod.rs +++ b/rslib/src/scheduler/queue/builder/mod.rs @@ -464,7 +464,7 @@ mod test { cards.push(card); } col.update_cards_maybe_undoable(cards, false)?; - col.set_deck_review_order(&mut deck, ReviewCardOrder::RelativeOverdueness); + col.set_deck_review_order(&mut deck, ReviewCardOrder::RetrievabilityAscending); assert_eq!(col.queue_as_due_and_ivl(deck.id), expected_queue); Ok(()) diff --git a/rslib/src/storage/card/filtered.rs b/rslib/src/storage/card/filtered.rs index b4668bbec..8443c2e18 100644 --- a/rslib/src/storage/card/filtered.rs +++ b/rslib/src/storage/card/filtered.rs @@ -5,6 +5,7 @@ use crate::card::CardQueue; use crate::decks::FilteredSearchOrder; use crate::decks::FilteredSearchTerm; use crate::scheduler::timing::SchedTimingToday; +use crate::storage::sqlite::SqlSortOrder; pub(crate) fn order_and_limit_for_search( term: &FilteredSearchTerm, @@ -27,24 +28,40 @@ pub(crate) fn order_and_limit_for_search( "(case when c.due > 1000000000 then due else (due - {today}) * 86400 + {current_timestamp} end), c.ord"); &temp_string } - FilteredSearchOrder::DuePriority => { + FilteredSearchOrder::RetrievabilityAscending => { let next_day_at = timing.next_day_at.0; - temp_string = if fsrs { - format!( - "extract_fsrs_relative_overdueness(c.data, due, {today}, ivl, {next_day_at}) desc" - ) - } else { - format!( - " -(case when queue={rev_queue} and due <= {today} -then (ivl / cast({today}-due+0.001 as real)) else 100000+due end)", - rev_queue = CardQueue::Review as i8, - today = today - ) - }; + temp_string = + build_retrievability_query(fsrs, today, next_day_at, SqlSortOrder::Ascending); + &temp_string + } + FilteredSearchOrder::RetrievabilityDescending => { + let next_day_at = timing.next_day_at.0; + temp_string = + build_retrievability_query(fsrs, today, next_day_at, SqlSortOrder::Descending); &temp_string } }; format!("{}, fnvhash(c.id, c.mod) limit {}", order, term.limit) } + +fn build_retrievability_query( + fsrs: bool, + today: u32, + next_day_at: i64, + order: SqlSortOrder, +) -> String { + if fsrs { + format!( + "extract_fsrs_relative_retrievability(c.data, due, {today}, ivl, {next_day_at}) {order}" + ) + } else { + format!( + " +(case when queue={rev_queue} and due <= {today} +then (ivl / cast({today}-due+0.001 as real)) else 100000+due end) {order}", + rev_queue = CardQueue::Review as i8, + today = today + ) + } +} diff --git a/rslib/src/storage/card/mod.rs b/rslib/src/storage/card/mod.rs index 51263a8b1..812691523 100644 --- a/rslib/src/storage/card/mod.rs +++ b/rslib/src/storage/card/mod.rs @@ -20,6 +20,7 @@ use rusqlite::Row; use self::data::CardData; use super::ids_to_string; +use super::sqlite::SqlSortOrder; use crate::card::Card; use crate::card::CardId; use crate::card::CardQueue; @@ -747,11 +748,13 @@ enum ReviewOrderSubclause { DifficultyAscending, /// FSRS DifficultyDescending, - RelativeOverdueness { + RetrievabilitySm2 { today: u32, + order: SqlSortOrder, }, - RelativeOverduenessFsrs { + RetrievabilityFsrs { timing: SchedTimingToday, + order: SqlSortOrder, }, Added, ReverseAdded, @@ -770,15 +773,18 @@ impl fmt::Display for ReviewOrderSubclause { ReviewOrderSubclause::EaseDescending => "factor desc", ReviewOrderSubclause::DifficultyAscending => "extract_fsrs_variable(data, 'd') asc", ReviewOrderSubclause::DifficultyDescending => "extract_fsrs_variable(data, 'd') desc", - ReviewOrderSubclause::RelativeOverdueness { today } => { - temp_string = format!("ivl / cast({today}-due+0.001 as real)", today = today); + ReviewOrderSubclause::RetrievabilitySm2 { today, order } => { + temp_string = format!( + "ivl / cast({today}-due+0.001 as real) {order}", + today = today + ); &temp_string } - ReviewOrderSubclause::RelativeOverduenessFsrs { timing } => { + ReviewOrderSubclause::RetrievabilityFsrs { timing, order } => { 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, {next_day_at}) desc"); + format!("extract_fsrs_relative_retrievability(data, due, {today}, ivl, {next_day_at}) {order}"); &temp_string } ReviewOrderSubclause::Added => "nid asc, ord asc", @@ -807,14 +813,11 @@ fn review_order_sql(order: ReviewCardOrder, timing: SchedTimingToday, fsrs: bool } else { ReviewOrderSubclause::EaseDescending }], - ReviewCardOrder::RelativeOverdueness => { - vec![if fsrs { - ReviewOrderSubclause::RelativeOverduenessFsrs { timing } - } else { - ReviewOrderSubclause::RelativeOverdueness { - today: timing.days_elapsed, - } - }] + ReviewCardOrder::RetrievabilityAscending => { + build_retrievability_clauses(fsrs, timing, SqlSortOrder::Ascending) + } + ReviewCardOrder::RetrievabilityDescending => { + build_retrievability_clauses(fsrs, timing, SqlSortOrder::Descending) } ReviewCardOrder::Random => vec![], ReviewCardOrder::Added => vec![ReviewOrderSubclause::Added], @@ -829,6 +832,21 @@ fn review_order_sql(order: ReviewCardOrder, timing: SchedTimingToday, fsrs: bool v.join(", ") } +fn build_retrievability_clauses( + fsrs: bool, + timing: SchedTimingToday, + order: SqlSortOrder, +) -> Vec { + vec![if fsrs { + ReviewOrderSubclause::RetrievabilityFsrs { timing, order } + } else { + ReviewOrderSubclause::RetrievabilitySm2 { + today: timing.days_elapsed, + order, + } + }] +} + #[derive(Debug, Clone, Copy)] pub(crate) enum NewCardSorting { /// Ascending position, consecutive siblings, diff --git a/rslib/src/storage/sqlite.rs b/rslib/src/storage/sqlite.rs index 42d46c520..37ea06cb5 100644 --- a/rslib/src/storage/sqlite.rs +++ b/rslib/src/storage/sqlite.rs @@ -4,6 +4,7 @@ use std::borrow::Cow; use std::cmp::Ordering; use std::collections::HashSet; +use std::fmt::Display; use std::hash::Hasher; use std::path::Path; use std::sync::Arc; @@ -74,7 +75,7 @@ fn open_or_create_collection_db(path: &Path) -> Result { add_extract_custom_data_function(&db)?; add_extract_fsrs_variable(&db)?; add_extract_fsrs_retrievability(&db)?; - add_extract_fsrs_relative_overdueness(&db)?; + add_extract_fsrs_relative_retrievability(&db)?; db.create_collation("unicase", unicase_compare)?; @@ -333,12 +334,13 @@ fn add_extract_fsrs_retrievability(db: &Connection) -> rusqlite::Result<()> { ) } -/// eg. extract_fsrs_relative_overdueness(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 -/// higher the number, the more overdue. -fn add_extract_fsrs_relative_overdueness(db: &Connection) -> rusqlite::Result<()> { +/// higher the number, the higher the card's retrievability relative to the +/// configured desired retention. +fn add_extract_fsrs_relative_retrievability(db: &Connection) -> rusqlite::Result<()> { db.create_scalar_function( - "extract_fsrs_relative_overdueness", + "extract_fsrs_relative_retrievability", 5, FunctionFlags::SQLITE_DETERMINISTIC, move |ctx| { @@ -386,7 +388,7 @@ fn add_extract_fsrs_relative_overdueness(db: &Connection) -> rusqlite::Result<() .max(0.0001); Ok(Some( - (1. / current_retrievability - 1.) / (1. / desired_retrievability - 1.), + -(1. / current_retrievability - 1.) / (1. / desired_retrievability - 1.), )) }, ) @@ -590,3 +592,22 @@ impl SqliteStorage { self.db.query_row(sql, [], |r| r.get(0)).map_err(Into::into) } } + +#[derive(Debug, Clone, Copy)] +pub enum SqlSortOrder { + Ascending, + Descending, +} + +impl Display for SqlSortOrder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + SqlSortOrder::Ascending => "asc", + SqlSortOrder::Descending => "desc", + } + ) + } +} diff --git a/ts/routes/deck-options/choices.ts b/ts/routes/deck-options/choices.ts index 0b4611528..6f34eae0e 100644 --- a/ts/routes/deck-options/choices.ts +++ b/ts/routes/deck-options/choices.ts @@ -96,8 +96,12 @@ export function reviewOrderChoices(fsrs: boolean): Choice