Anki/rslib/src/card/mod.rs
RumovZ 4eddaafce2 Improve temporary table handling (#1976)
* Use all_cards_for_search() helper

* Add CardTableGuard

* Add for_each_card_in_search() helper

* Add all_cards_for_search_in_order() helper

* Add all_cards_for_ids() helper

* Return siblings for bury instead of only searching

* Remove redundant clear_searched_cards_table() calls

* Add with_searched_cards_table()

* Remove false comment

* Add NoteTableGuard

* Add with_ids_in_searched_notes_table() helper

* Make some last routines use table helpers
2022-07-22 17:51:26 +10:00

420 lines
13 KiB
Rust

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
pub(crate) mod undo;
use std::collections::{hash_map::Entry, HashMap, HashSet};
use num_enum::TryFromPrimitive;
use serde_repr::{Deserialize_repr, Serialize_repr};
use crate::{
collection::Collection,
config::SchedulerVersion,
deckconfig::DeckConfig,
decks::DeckId,
define_newtype,
error::{AnkiError, FilteredDeckError, Result},
notes::NoteId,
ops::StateChanges,
prelude::*,
timestamp::TimestampSecs,
types::Usn,
};
define_newtype!(CardId, i64);
impl CardId {
pub fn as_secs(self) -> TimestampSecs {
TimestampSecs(self.0 / 1000)
}
}
#[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, TryFromPrimitive, Clone, Copy)]
#[repr(u8)]
pub enum CardType {
New = 0,
Learn = 1,
Review = 2,
Relearn = 3,
}
#[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, TryFromPrimitive, Clone, Copy)]
#[repr(i8)]
pub enum CardQueue {
/// due is the order cards are shown in
New = 0,
/// due is a unix timestamp
Learn = 1,
/// due is days since creation date
Review = 2,
DayLearn = 3,
/// due is a unix timestamp.
/// preview cards only placed here when failed.
PreviewRepeat = 4,
/// cards are not due in these states
Suspended = -1,
SchedBuried = -2,
UserBuried = -3,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Card {
pub(crate) id: CardId,
pub(crate) note_id: NoteId,
pub(crate) deck_id: DeckId,
pub(crate) template_idx: u16,
pub(crate) mtime: TimestampSecs,
pub(crate) usn: Usn,
pub(crate) ctype: CardType,
pub(crate) queue: CardQueue,
pub(crate) due: i32,
pub(crate) interval: u32,
pub(crate) ease_factor: u16,
pub(crate) reps: u32,
pub(crate) lapses: u32,
pub(crate) remaining_steps: u32,
pub(crate) original_due: i32,
pub(crate) original_deck_id: DeckId,
pub(crate) flags: u8,
/// The position in the new queue before leaving it.
pub(crate) original_position: Option<u32>,
}
impl Default for Card {
fn default() -> Self {
Self {
id: CardId(0),
note_id: NoteId(0),
deck_id: DeckId(0),
template_idx: 0,
mtime: TimestampSecs(0),
usn: Usn(0),
ctype: CardType::New,
queue: CardQueue::New,
due: 0,
interval: 0,
ease_factor: 0,
reps: 0,
lapses: 0,
remaining_steps: 0,
original_due: 0,
original_deck_id: DeckId(0),
flags: 0,
original_position: None,
}
}
}
impl Card {
pub fn set_modified(&mut self, usn: Usn) {
self.mtime = TimestampSecs::now();
self.usn = usn;
}
/// Caller must ensure provided deck exists and is not filtered.
fn set_deck(&mut self, deck: DeckId, sched: SchedulerVersion) {
self.remove_from_filtered_deck_restoring_queue(sched);
self.deck_id = deck;
}
/// True if flag changed.
fn set_flag(&mut self, flag: u8) -> bool {
// The first 3 bits represent one of the 7 supported flags, the rest of
// the flag byte is preserved.
let updated_flags = (self.flags & !0b111) | flag;
if self.flags != updated_flags {
self.flags = updated_flags;
true
} else {
false
}
}
/// Return the total number of steps left to do, ignoring the
/// "steps today" number packed into the DB representation.
pub fn remaining_steps(&self) -> u32 {
self.remaining_steps % 1000
}
/// Return ease factor as a multiplier (eg 2.5)
pub fn ease_factor(&self) -> f32 {
(self.ease_factor as f32) / 1000.0
}
pub fn is_intraday_learning(&self) -> bool {
matches!(self.queue, CardQueue::Learn | CardQueue::PreviewRepeat)
}
pub fn new(note_id: NoteId, template_idx: u16, deck_id: DeckId, due: i32) -> Self {
Card {
note_id,
deck_id,
template_idx,
due,
..Default::default()
}
}
/// Remaining steps after configured steps have changed, disregarding "remaining today".
/// [None] if same as before. A step counts as remaining if the card has not passed a step
/// with the same or a greater delay, but output will be at least 1.
fn new_remaining_steps(&self, new_steps: &[f32], old_steps: &[f32]) -> Option<u32> {
let remaining = self.remaining_steps();
let new_remaining = old_steps
.len()
.checked_sub(remaining as usize + 1)
.and_then(|last_index| {
new_steps
.iter()
.rev()
.position(|&step| step <= old_steps[last_index])
})
// no last delay or last delay is less than all new steps → all steps remain
.unwrap_or(new_steps.len())
// (re)learning card must have at least 1 step remaining
.max(1) as u32;
(remaining != new_remaining).then(|| new_remaining)
}
}
impl Collection {
pub(crate) fn update_cards_maybe_undoable(
&mut self,
cards: Vec<Card>,
undoable: bool,
) -> Result<OpOutput<()>> {
if undoable {
self.transact(Op::UpdateCard, |col| {
for mut card in cards {
let existing = col.storage.get_card(card.id)?.ok_or(AnkiError::NotFound)?;
col.update_card_inner(&mut card, existing, col.usn()?)?
}
Ok(())
})
} else {
self.transact_no_undo(|col| {
for mut card in cards {
let existing = col.storage.get_card(card.id)?.ok_or(AnkiError::NotFound)?;
col.update_card_inner(&mut card, existing, col.usn()?)?;
}
Ok(OpOutput {
output: (),
changes: OpChanges {
op: Op::UpdateCard,
changes: StateChanges {
card: true,
..Default::default()
},
},
})
})
}
}
#[cfg(test)]
pub(crate) fn get_and_update_card<F, T>(&mut self, cid: CardId, func: F) -> Result<Card>
where
F: FnOnce(&mut Card) -> Result<T>,
{
let orig = self
.storage
.get_card(cid)?
.ok_or_else(|| AnkiError::invalid_input("no such card"))?;
let mut card = orig.clone();
func(&mut card)?;
self.update_card_inner(&mut card, orig, self.usn()?)?;
Ok(card)
}
/// Marks the card as modified, then saves it.
pub(crate) fn update_card_inner(
&mut self,
card: &mut Card,
original: Card,
usn: Usn,
) -> Result<()> {
card.set_modified(usn);
self.update_card_undoable(card, original)
}
pub(crate) fn add_card(&mut self, card: &mut Card) -> Result<()> {
if card.id.0 != 0 {
return Err(AnkiError::invalid_input("card id already set"));
}
card.mtime = TimestampSecs::now();
card.usn = self.usn()?;
self.add_card_undoable(card)
}
/// Remove cards and any resulting orphaned notes.
/// Expects a transaction.
pub(crate) fn remove_cards_and_orphaned_notes(&mut self, cids: &[CardId]) -> Result<()> {
let usn = self.usn()?;
let mut nids = HashSet::new();
for cid in cids {
if let Some(card) = self.storage.get_card(*cid)? {
nids.insert(card.note_id);
self.remove_card_and_add_grave_undoable(card, usn)?;
}
}
for nid in nids {
if self.storage.note_is_orphaned(nid)? {
self.remove_note_only_undoable(nid, usn)?;
}
}
Ok(())
}
pub fn set_deck(&mut self, cards: &[CardId], deck_id: DeckId) -> Result<OpOutput<usize>> {
let deck = self.get_deck(deck_id)?.ok_or(AnkiError::NotFound)?;
let config_id = deck.config_id().ok_or(AnkiError::FilteredDeckError(
FilteredDeckError::CanNotMoveCardsInto,
))?;
let config = self.get_deck_config(config_id, true)?.unwrap();
let mut steps_adjuster = RemainingStepsAdjuster::new(&config);
let sched = self.scheduler_version();
let usn = self.usn()?;
self.transact(Op::SetCardDeck, |col| {
let mut count = 0;
for mut card in col.all_cards_for_ids(cards, false)? {
if card.deck_id == deck_id {
continue;
}
count += 1;
let original = card.clone();
steps_adjuster.adjust_remaining_steps(col, &mut card)?;
card.set_deck(deck_id, sched);
col.update_card_inner(&mut card, original, usn)?;
}
Ok(count)
})
}
pub fn set_card_flag(&mut self, cards: &[CardId], flag: u32) -> Result<OpOutput<usize>> {
if flag > 7 {
return Err(AnkiError::invalid_input("invalid flag"));
}
let flag = flag as u8;
let usn = self.usn()?;
self.transact(Op::SetFlag, |col| {
let mut count = 0;
for mut card in col.all_cards_for_ids(cards, false)? {
let original = card.clone();
if card.set_flag(flag) {
// To avoid having to rebuild the study queues, we mark the card as requiring
// a sync, but do not change its modification time.
card.usn = usn;
col.update_card_undoable(&mut card, original)?;
count += 1;
}
}
Ok(count)
})
}
/// Get deck config for the given card. If missing, return default values.
#[allow(dead_code)]
pub(crate) fn deck_config_for_card(&mut self, card: &Card) -> Result<DeckConfig> {
if let Some(deck) = self.get_deck(card.original_or_current_deck_id())? {
if let Some(conf_id) = deck.config_id() {
return Ok(self.get_deck_config(conf_id, true)?.unwrap());
}
}
Ok(DeckConfig::default())
}
/// Adjust the remaining steps of the card according to the steps change.
/// Steps must be learning or relearning steps according to the card's type.
pub(crate) fn adjust_remaining_steps(
&mut self,
card: &mut Card,
old_steps: &[f32],
new_steps: &[f32],
usn: Usn,
) -> Result<()> {
if let Some(new_remaining) = card.new_remaining_steps(new_steps, old_steps) {
let original = card.clone();
card.remaining_steps = new_remaining;
self.update_card_inner(card, original, usn)
} else {
Ok(())
}
}
}
/// Adjusts the remaining steps of cards after their deck config has changed.
struct RemainingStepsAdjuster<'a> {
learn_steps: &'a [f32],
relearn_steps: &'a [f32],
configs: HashMap<DeckId, DeckConfig>,
}
impl<'a> RemainingStepsAdjuster<'a> {
fn new(new_config: &'a DeckConfig) -> Self {
RemainingStepsAdjuster {
learn_steps: &new_config.inner.learn_steps,
relearn_steps: &new_config.inner.relearn_steps,
configs: HashMap::new(),
}
}
fn adjust_remaining_steps(&mut self, col: &mut Collection, card: &mut Card) -> Result<()> {
if let Some(remaining) = match card.ctype {
CardType::Learn => card.new_remaining_steps(
self.learn_steps,
&self.config_for_card(col, card)?.inner.learn_steps,
),
CardType::Relearn => card.new_remaining_steps(
self.relearn_steps,
&self.config_for_card(col, card)?.inner.relearn_steps,
),
_ => None,
} {
card.remaining_steps = remaining;
}
Ok(())
}
fn config_for_card(&mut self, col: &mut Collection, card: &Card) -> Result<&mut DeckConfig> {
Ok(
match self.configs.entry(card.original_or_current_deck_id()) {
Entry::Occupied(e) => e.into_mut(),
Entry::Vacant(e) => e.insert(col.deck_config_for_card(card)?),
},
)
}
}
#[cfg(test)]
mod test {
use crate::tests::{
open_test_collection_with_learning_card, open_test_collection_with_relearning_card,
DeckAdder,
};
#[test]
fn should_increase_remaining_learning_steps_if_new_deck_has_more_unpassed_ones() {
let mut col = open_test_collection_with_learning_card();
let deck = DeckAdder::new("target")
.with_config(|config| config.inner.learn_steps.push(100.))
.add(&mut col);
let card_id = col.get_first_card().id;
col.set_deck(&[card_id], deck.id).unwrap();
assert_eq!(col.get_first_card().remaining_steps, 3);
}
#[test]
fn should_increase_remaining_relearning_steps_if_new_deck_has_more_unpassed_ones() {
let mut col = open_test_collection_with_relearning_card();
let deck = DeckAdder::new("target")
.with_config(|config| config.inner.relearn_steps.push(100.))
.add(&mut col);
let card_id = col.get_first_card().id;
col.set_deck(&[card_id], deck.id).unwrap();
assert_eq!(col.get_first_card().remaining_steps, 2);
}
}