Add descending retrievability (#3559)

* "relative overdueness" -> "retrievability ascending"

* Add 'retrievability descending'
This commit is contained in:
Damien Elmes 2024-11-08 22:53:13 +10:00 committed by GitHub
parent a150eda287
commit b646f09c68
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 115 additions and 42 deletions

View file

@ -209,7 +209,9 @@ deck-config-sort-order-ascending-ease = Ascending ease
deck-config-sort-order-descending-ease = Descending ease deck-config-sort-order-descending-ease = Descending ease
deck-config-sort-order-ascending-difficulty = Easy cards first deck-config-sort-order-ascending-difficulty = Easy cards first
deck-config-sort-order-descending-difficulty = Difficult 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 = deck-config-display-order-will-use-current-deck =
Anki will use the display order from the deck you Anki will use the display order from the deck you
select to study, and not any subdecks it may have. select to study, and not any subdecks it may have.

View file

@ -24,7 +24,6 @@ decks-order-added = Order added
decks-order-due = Order due decks-order-due = Order due
decks-please-select-something = Please select something. decks-please-select-something = Please select something.
decks-random = Random decks-random = Random
decks-relative-overdueness = Relative overdueness
decks-repeat-failed-cards-after = Delay Repeat failed cards after decks-repeat-failed-cards-after = Delay Repeat failed cards after
# e.g. "Delay for Again", "Delay for Hard", "Delay for Good" # e.g. "Delay for Again", "Delay for Hard", "Delay for Good"
decks-delay-for-button = Delay for { $button } decks-delay-for-button = Delay for { $button }
@ -37,3 +36,8 @@ decks-learn-header = Learn
# The count of cards waiting to be reviewed # The count of cards waiting to be reviewed
decks-review-header = Due decks-review-header = Due
decks-zero-minutes-hint = (0 = return card to original deck) 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

View file

@ -79,7 +79,8 @@ message DeckConfig {
REVIEW_CARD_ORDER_INTERVALS_DESCENDING = 4; REVIEW_CARD_ORDER_INTERVALS_DESCENDING = 4;
REVIEW_CARD_ORDER_EASE_ASCENDING = 5; REVIEW_CARD_ORDER_EASE_ASCENDING = 5;
REVIEW_CARD_ORDER_EASE_DESCENDING = 6; 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_RANDOM = 8;
REVIEW_CARD_ORDER_ADDED = 9; REVIEW_CARD_ORDER_ADDED = 9;
REVIEW_CARD_ORDER_REVERSE_ADDED = 10; REVIEW_CARD_ORDER_REVERSE_ADDED = 10;

View file

@ -97,7 +97,8 @@ message Deck {
ADDED = 5; ADDED = 5;
DUE = 6; DUE = 6;
REVERSE_ADDED = 7; REVERSE_ADDED = 7;
DUE_PRIORITY = 8; RETRIEVABILITY_ASCENDING = 8;
RETRIEVABILITY_DESCENDING = 9;
} }
string search = 1; string search = 1;

View file

@ -60,7 +60,12 @@ fn search_order_label(order: FilteredSearchOrder, tr: &I18n) -> String {
FilteredSearchOrder::Added => tr.decks_order_added(), FilteredSearchOrder::Added => tr.decks_order_added(),
FilteredSearchOrder::Due => tr.decks_order_due(), FilteredSearchOrder::Due => tr.decks_order_due(),
FilteredSearchOrder::ReverseAdded => tr.decks_latest_added_first(), 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() .into()
} }

View file

@ -464,7 +464,7 @@ mod test {
cards.push(card); cards.push(card);
} }
col.update_cards_maybe_undoable(cards, false)?; 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); assert_eq!(col.queue_as_due_and_ivl(deck.id), expected_queue);
Ok(()) Ok(())

View file

@ -5,6 +5,7 @@ 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; use crate::scheduler::timing::SchedTimingToday;
use crate::storage::sqlite::SqlSortOrder;
pub(crate) fn order_and_limit_for_search( pub(crate) fn order_and_limit_for_search(
term: &FilteredSearchTerm, 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"); "(case when c.due > 1000000000 then due else (due - {today}) * 86400 + {current_timestamp} end), c.ord");
&temp_string &temp_string
} }
FilteredSearchOrder::DuePriority => { FilteredSearchOrder::RetrievabilityAscending => {
let next_day_at = timing.next_day_at.0; let next_day_at = timing.next_day_at.0;
temp_string = if fsrs { temp_string =
format!( build_retrievability_query(fsrs, today, next_day_at, SqlSortOrder::Ascending);
"extract_fsrs_relative_overdueness(c.data, due, {today}, ivl, {next_day_at}) desc" &temp_string
) }
} else { FilteredSearchOrder::RetrievabilityDescending => {
format!( let next_day_at = timing.next_day_at.0;
" temp_string =
(case when queue={rev_queue} and due <= {today} build_retrievability_query(fsrs, today, next_day_at, SqlSortOrder::Descending);
then (ivl / cast({today}-due+0.001 as real)) else 100000+due end)",
rev_queue = CardQueue::Review as i8,
today = today
)
};
&temp_string &temp_string
} }
}; };
format!("{}, fnvhash(c.id, c.mod) limit {}", order, term.limit) 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
)
}
}

View file

@ -20,6 +20,7 @@ use rusqlite::Row;
use self::data::CardData; use self::data::CardData;
use super::ids_to_string; use super::ids_to_string;
use super::sqlite::SqlSortOrder;
use crate::card::Card; use crate::card::Card;
use crate::card::CardId; use crate::card::CardId;
use crate::card::CardQueue; use crate::card::CardQueue;
@ -747,11 +748,13 @@ enum ReviewOrderSubclause {
DifficultyAscending, DifficultyAscending,
/// FSRS /// FSRS
DifficultyDescending, DifficultyDescending,
RelativeOverdueness { RetrievabilitySm2 {
today: u32, today: u32,
order: SqlSortOrder,
}, },
RelativeOverduenessFsrs { RetrievabilityFsrs {
timing: SchedTimingToday, timing: SchedTimingToday,
order: SqlSortOrder,
}, },
Added, Added,
ReverseAdded, ReverseAdded,
@ -770,15 +773,18 @@ impl fmt::Display for ReviewOrderSubclause {
ReviewOrderSubclause::EaseDescending => "factor desc", ReviewOrderSubclause::EaseDescending => "factor desc",
ReviewOrderSubclause::DifficultyAscending => "extract_fsrs_variable(data, 'd') asc", ReviewOrderSubclause::DifficultyAscending => "extract_fsrs_variable(data, 'd') asc",
ReviewOrderSubclause::DifficultyDescending => "extract_fsrs_variable(data, 'd') desc", ReviewOrderSubclause::DifficultyDescending => "extract_fsrs_variable(data, 'd') desc",
ReviewOrderSubclause::RelativeOverdueness { today } => { ReviewOrderSubclause::RetrievabilitySm2 { today, order } => {
temp_string = format!("ivl / cast({today}-due+0.001 as real)", today = today); temp_string = format!(
"ivl / cast({today}-due+0.001 as real) {order}",
today = today
);
&temp_string &temp_string
} }
ReviewOrderSubclause::RelativeOverduenessFsrs { timing } => { 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;
temp_string = 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 &temp_string
} }
ReviewOrderSubclause::Added => "nid asc, ord asc", ReviewOrderSubclause::Added => "nid asc, ord asc",
@ -807,14 +813,11 @@ fn review_order_sql(order: ReviewCardOrder, timing: SchedTimingToday, fsrs: bool
} else { } else {
ReviewOrderSubclause::EaseDescending ReviewOrderSubclause::EaseDescending
}], }],
ReviewCardOrder::RelativeOverdueness => { ReviewCardOrder::RetrievabilityAscending => {
vec![if fsrs { build_retrievability_clauses(fsrs, timing, SqlSortOrder::Ascending)
ReviewOrderSubclause::RelativeOverduenessFsrs { timing } }
} else { ReviewCardOrder::RetrievabilityDescending => {
ReviewOrderSubclause::RelativeOverdueness { build_retrievability_clauses(fsrs, timing, SqlSortOrder::Descending)
today: timing.days_elapsed,
}
}]
} }
ReviewCardOrder::Random => vec![], ReviewCardOrder::Random => vec![],
ReviewCardOrder::Added => vec![ReviewOrderSubclause::Added], ReviewCardOrder::Added => vec![ReviewOrderSubclause::Added],
@ -829,6 +832,21 @@ fn review_order_sql(order: ReviewCardOrder, timing: SchedTimingToday, fsrs: bool
v.join(", ") v.join(", ")
} }
fn build_retrievability_clauses(
fsrs: bool,
timing: SchedTimingToday,
order: SqlSortOrder,
) -> Vec<ReviewOrderSubclause> {
vec![if fsrs {
ReviewOrderSubclause::RetrievabilityFsrs { timing, order }
} else {
ReviewOrderSubclause::RetrievabilitySm2 {
today: timing.days_elapsed,
order,
}
}]
}
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub(crate) enum NewCardSorting { pub(crate) enum NewCardSorting {
/// Ascending position, consecutive siblings, /// Ascending position, consecutive siblings,

View file

@ -4,6 +4,7 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::cmp::Ordering; use std::cmp::Ordering;
use std::collections::HashSet; use std::collections::HashSet;
use std::fmt::Display;
use std::hash::Hasher; use std::hash::Hasher;
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
@ -74,7 +75,7 @@ fn open_or_create_collection_db(path: &Path) -> Result<Connection> {
add_extract_custom_data_function(&db)?; add_extract_custom_data_function(&db)?;
add_extract_fsrs_variable(&db)?; add_extract_fsrs_variable(&db)?;
add_extract_fsrs_retrievability(&db)?; add_extract_fsrs_retrievability(&db)?;
add_extract_fsrs_relative_overdueness(&db)?; add_extract_fsrs_relative_retrievability(&db)?;
db.create_collation("unicase", unicase_compare)?; 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 /// timing.days_elapsed, card.ivl, timing.next_day_at) -> float | null. The
/// higher the number, the more overdue. /// higher the number, the higher the card's retrievability relative to the
fn add_extract_fsrs_relative_overdueness(db: &Connection) -> rusqlite::Result<()> { /// configured desired retention.
fn add_extract_fsrs_relative_retrievability(db: &Connection) -> rusqlite::Result<()> {
db.create_scalar_function( db.create_scalar_function(
"extract_fsrs_relative_overdueness", "extract_fsrs_relative_retrievability",
5, 5,
FunctionFlags::SQLITE_DETERMINISTIC, FunctionFlags::SQLITE_DETERMINISTIC,
move |ctx| { move |ctx| {
@ -386,7 +388,7 @@ fn add_extract_fsrs_relative_overdueness(db: &Connection) -> rusqlite::Result<()
.max(0.0001); .max(0.0001);
Ok(Some( 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) 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",
}
)
}
}

View file

@ -96,8 +96,12 @@ export function reviewOrderChoices(fsrs: boolean): Choice<DeckConfig_Config_Revi
...difficultyOrders(fsrs), ...difficultyOrders(fsrs),
...[ ...[
{ {
label: tr.deckConfigSortOrderRelativeOverdueness(), label: tr.deckConfigSortOrderRetrievabilityAscending(),
value: DeckConfig_Config_ReviewCardOrder.RELATIVE_OVERDUENESS, value: DeckConfig_Config_ReviewCardOrder.RETRIEVABILITY_ASCENDING,
},
{
label: tr.deckConfigSortOrderRetrievabilityDescending(),
value: DeckConfig_Config_ReviewCardOrder.RETRIEVABILITY_DESCENDING,
}, },
{ {
label: tr.deckConfigSortOrderRandom(), label: tr.deckConfigSortOrderRandom(),