diff --git a/rslib/src/browser_table.rs b/rslib/src/browser_table.rs index 0a03e5452..19fdb11f6 100644 --- a/rslib/src/browser_table.rs +++ b/rslib/src/browser_table.rs @@ -46,6 +46,7 @@ pub enum Column { NoteMod, #[strum(serialize = "note")] Notetype, + OriginalPosition, Question, #[strum(serialize = "cardReps")] Reps, @@ -161,6 +162,7 @@ impl Column { Self::NoteCreation => tr.browsing_created(), Self::NoteMod => tr.search_note_modified(), Self::Notetype => tr.card_stats_note_type(), + Self::OriginalPosition => tr.card_stats_new_card_position(), Self::Question => tr.browsing_question(), Self::Reps => tr.scheduling_reviews(), Self::SortField => tr.browsing_sort_field(), @@ -226,6 +228,7 @@ impl Column { | Column::Interval | Column::NoteCreation | Column::NoteMod + | Column::OriginalPosition | Column::Reps => Sorting::Descending, Column::Stability | Column::Difficulty | Column::Retrievability => { if notes { @@ -432,6 +435,7 @@ impl RowContext { Column::NoteCreation => self.note_creation_str(), Column::SortField => self.note_field_str(), Column::NoteMod => self.note.mtime.date_and_time_string(), + Column::OriginalPosition => self.card_original_position(), Column::Tags => self.note.tags.join(" "), Column::Notetype => self.notetype.name.to_owned(), Column::Stability => self.fsrs_stability_str(), @@ -441,6 +445,17 @@ impl RowContext { }) } + fn card_original_position(&self) -> String { + let card = &self.cards[0]; + if let Some(pos) = &card.original_position { + pos.to_string() + } else if card.ctype == CardType::New { + card.due.to_string() + } else { + String::new() + } + } + fn note_creation_str(&self) -> String { TimestampMillis(self.note.id.into()) .as_secs() diff --git a/rslib/src/search/mod.rs b/rslib/src/search/mod.rs index 905585437..63096dad8 100644 --- a/rslib/src/search/mod.rs +++ b/rslib/src/search/mod.rs @@ -370,6 +370,7 @@ fn card_order_from_sort_column(column: Column, timing: SchedTimingToday) -> Cow< Column::NoteCreation => "n.id asc, c.ord asc".into(), Column::NoteMod => "n.mod asc, c.ord asc".into(), Column::Notetype => "(select pos from sort_order where ntid = n.mid) asc".into(), + Column::OriginalPosition => "(select pos from sort_order where nid = c.nid) asc".into(), Column::Reps => "c.reps asc".into(), Column::SortField => "n.sfld collate nocase asc, c.ord asc".into(), Column::Tags => "n.tags asc".into(), @@ -394,6 +395,7 @@ fn note_order_from_sort_column(column: Column) -> Cow<'static, str> { | Column::Ease | Column::Interval | Column::Lapses + | Column::OriginalPosition | Column::Reps => "(select pos from sort_order where nid = n.id) asc".into(), Column::NoteCreation => "n.id asc".into(), Column::NoteMod => "n.mod asc".into(), @@ -416,6 +418,7 @@ fn prepare_sort(col: &mut Collection, column: Column, item_type: ReturnItemType) Column::Cards => include_str!("template_order.sql"), Column::Deck => include_str!("deck_order.sql"), Column::Notetype => include_str!("notetype_order.sql"), + Column::OriginalPosition => include_str!("note_original_position_order.sql"), _ => return Ok(()), }, ReturnItemType::Notes => match column { @@ -429,6 +432,7 @@ fn prepare_sort(col: &mut Collection, column: Column, item_type: ReturnItemType) Column::Ease => include_str!("note_ease_order.sql"), Column::Interval => include_str!("note_interval_order.sql"), Column::Lapses => include_str!("note_lapses_order.sql"), + Column::OriginalPosition => include_str!("note_original_position_order.sql"), Column::Reps => include_str!("note_reps_order.sql"), Column::Notetype => include_str!("notetype_order.sql"), _ => return Ok(()), diff --git a/rslib/src/search/note_original_position_order.sql b/rslib/src/search/note_original_position_order.sql new file mode 100644 index 000000000..aa79b0b6b --- /dev/null +++ b/rslib/src/search/note_original_position_order.sql @@ -0,0 +1,16 @@ +DROP TABLE IF EXISTS sort_order; +CREATE TEMPORARY TABLE sort_order ( + pos integer PRIMARY KEY, + nid integer NOT NULL UNIQUE +); +INSERT INTO sort_order (nid) +SELECT nid +FROM cards +GROUP BY nid +ORDER BY COALESCE( + extract_original_position(data), + CASE + WHEN type == 0 THEN due + ELSE 0 + END + ); \ No newline at end of file diff --git a/rslib/src/stats/card.rs b/rslib/src/stats/card.rs index 3c257af1a..3bd6eb3e9 100644 --- a/rslib/src/stats/card.rs +++ b/rslib/src/stats/card.rs @@ -3,7 +3,6 @@ use fsrs::FSRS; -use crate::card::CardQueue; use crate::card::CardType; use crate::prelude::*; use crate::revlog::RevlogEntry; @@ -28,7 +27,6 @@ impl Collection { let revlog = self.storage.get_revlog_entries_for_card(card.id)?; let (average_secs, total_secs) = average_and_total_secs_strings(&revlog); - let (due_date, due_position) = self.due_date_and_position(&card)?; let timing = self.timing_today()?; let days_elapsed = self .storage @@ -62,8 +60,8 @@ impl Collection { added: card.id.as_secs().0, first_review: revlog.first().map(|entry| entry.id.as_secs().0), latest_review: revlog.last().map(|entry| entry.id.as_secs().0), - due_date, - due_position, + due_date: self.due_date(&card)?, + due_position: self.position(&card), interval: card.interval, ease: card.ease_factor as u32, reviews: card.reps, @@ -85,37 +83,33 @@ impl Collection { }) } - fn due_date_and_position(&mut self, card: &Card) -> Result<(Option, Option)> { - let due = if card.original_due != 0 { - card.original_due - } else { - card.due - }; + fn due_date(&mut self, card: &Card) -> Result> { Ok(match card.ctype { - CardType::New => { - if matches!(card.queue, CardQueue::Review | CardQueue::DayLearn) { - // new preview card not answered yet - (None, card.original_position.map(|u| u as i32)) + CardType::New => None, + CardType::Review | CardType::Learn | CardType::Relearn => { + let due = card.due; + if !is_unix_epoch_timestamp(due) { + let days_remaining = due - (self.timing_today()?.days_elapsed as i32); + let mut due_timestamp = TimestampSecs::now(); + due_timestamp.0 += (days_remaining as i64) * 86_400; + Some(due_timestamp.0) } else { - (None, Some(due)) + Some(due as i64) } } - CardType::Review | CardType::Learn | CardType::Relearn => ( - { - if !is_unix_epoch_timestamp(due) { - let days_remaining = due - (self.timing_today()?.days_elapsed as i32); - let mut due = TimestampSecs::now(); - due.0 += (days_remaining as i64) * 86_400; - Some(due.0) - } else { - Some(due as i64) - } - }, - None, - ), }) } + fn position(&mut self, card: &Card) -> Option { + if let Some(original_pos) = card.original_position { + return Some(original_pos as i32); + } + match card.ctype { + CardType::New => Some(card.due), + _ => None, + } + } + fn stats_revlog_entries_with_memory_state( self: &mut Collection, card: &Card, diff --git a/rslib/src/storage/sqlite.rs b/rslib/src/storage/sqlite.rs index f7d70d00b..42d46c520 100644 --- a/rslib/src/storage/sqlite.rs +++ b/rslib/src/storage/sqlite.rs @@ -70,6 +70,7 @@ fn open_or_create_collection_db(path: &Path) -> Result { add_regexp_tags_function(&db)?; add_without_combining_function(&db)?; add_fnvhash_function(&db)?; + add_extract_original_position_function(&db)?; add_extract_custom_data_function(&db)?; add_extract_fsrs_variable(&db)?; add_extract_fsrs_retrievability(&db)?; @@ -205,6 +206,29 @@ fn add_regexp_tags_function(db: &Connection) -> rusqlite::Result<()> { ) } +/// eg. extract_original_position(c.data) -> number | null +/// Parse original card position from c.data (this is only populated after card +/// has been reviewed) +fn add_extract_original_position_function(db: &Connection) -> rusqlite::Result<()> { + db.create_scalar_function( + "extract_original_position", + 1, + FunctionFlags::SQLITE_DETERMINISTIC, + move |ctx| { + assert_eq!(ctx.len(), 1, "called with unexpected number of arguments"); + + let Ok(card_data) = ctx.get_raw(0).as_str() else { + return Ok(None); + }; + + match &CardData::from_str(card_data).original_position { + Some(position) => Ok(Some(*position as i64)), + None => Ok(None), + } + }, + ) +} + /// eg. extract_custom_data(card.data, 'r') -> string | null fn add_extract_custom_data_function(db: &Connection) -> rusqlite::Result<()> { db.create_scalar_function(