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.
|
/// 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)
|
||||||
|
|
|
@ -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)?;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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!(
|
||||||
"
|
"
|
||||||
|
|
|
@ -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![],
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue