mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
Calculate elapsed days for intraday learning cards
https://forums.ankiweb.net/t/anki-23-12-beta/37771/109
This commit is contained in:
parent
400686d2ae
commit
edd38ca067
8 changed files with 66 additions and 42 deletions
|
@ -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<u32> {
|
||||
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)
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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!(
|
||||
"
|
||||
|
|
|
@ -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<F>(
|
||||
&self,
|
||||
day_cutoff: u32,
|
||||
timing: SchedTimingToday,
|
||||
order: ReviewCardOrder,
|
||||
kind: DueCardKind,
|
||||
fsrs: bool,
|
||||
|
@ -259,7 +260,7 @@ impl super::SqliteStorage {
|
|||
where
|
||||
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!(
|
||||
"{} 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![],
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue