// Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html mod burying; mod gathering; pub(crate) mod intersperser; pub(crate) mod sized_chain; mod sorting; use std::collections::{HashMap, VecDeque}; use intersperser::Intersperser; use sized_chain::SizedChain; use super::{CardQueues, Counts, LearningQueueEntry, MainQueueEntry, MainQueueEntryKind}; use crate::{ deckconfig::{NewCardGatherPriority, NewCardSortOrder, ReviewCardOrder, ReviewMix}, decks::limits::LimitTreeMap, prelude::*, scheduler::timing::SchedTimingToday, }; /// Temporary holder for review cards that will be built into a queue. #[derive(Debug, Clone, Copy)] pub(crate) struct DueCard { pub id: CardId, pub note_id: NoteId, pub mtime: TimestampSecs, pub due: i32, pub current_deck_id: DeckId, pub original_deck_id: DeckId, pub kind: DueCardKind, } #[derive(Debug, Clone, Copy)] pub(crate) enum DueCardKind { Review, Learning, } /// Temporary holder for new cards that will be built into a queue. #[derive(Debug, Default, Clone, Copy)] pub(crate) struct NewCard { pub id: CardId, pub note_id: NoteId, pub mtime: TimestampSecs, pub current_deck_id: DeckId, pub original_deck_id: DeckId, pub template_index: u32, pub hash: u64, } impl From for MainQueueEntry { fn from(c: DueCard) -> Self { MainQueueEntry { id: c.id, mtime: c.mtime, kind: match c.kind { DueCardKind::Review => MainQueueEntryKind::Review, DueCardKind::Learning => MainQueueEntryKind::InterdayLearning, }, } } } impl From for MainQueueEntry { fn from(c: NewCard) -> Self { MainQueueEntry { id: c.id, mtime: c.mtime, kind: MainQueueEntryKind::New, } } } impl From for LearningQueueEntry { fn from(c: DueCard) -> Self { LearningQueueEntry { due: TimestampSecs(c.due as i64), id: c.id, mtime: c.mtime, } } } /// When we encounter a card with new or review burying enabled, all future /// siblings need to be buried, regardless of their own settings. #[derive(Default, Debug, Clone, Copy)] pub(super) struct BuryMode { bury_new: bool, bury_reviews: bool, bury_interday_learning: bool, } #[derive(Default, Clone, Debug)] pub(super) struct QueueSortOptions { pub(super) new_order: NewCardSortOrder, pub(super) new_gather_priority: NewCardGatherPriority, pub(super) review_order: ReviewCardOrder, pub(super) day_learn_mix: ReviewMix, pub(super) new_review_mix: ReviewMix, } #[derive(Debug, Clone)] pub(super) struct QueueBuilder { pub(super) new: Vec, pub(super) review: Vec, pub(super) learning: Vec, pub(super) day_learning: Vec, limits: LimitTreeMap, context: Context, } /// Data container and helper for building queues. #[derive(Debug, Clone)] struct Context { timing: SchedTimingToday, config_map: HashMap, root_deck: Deck, sort_options: QueueSortOptions, seen_note_ids: HashMap, deck_map: HashMap, } impl QueueBuilder { pub(super) fn new(col: &mut Collection, deck_id: DeckId) -> Result { let timing = col.timing_for_timestamp(TimestampSecs::now())?; let config_map = col.storage.get_deck_config_map()?; let root_deck = col.storage.get_deck(deck_id)?.ok_or(AnkiError::NotFound)?; let child_decks = col.storage.child_decks(&root_deck)?; let limits = LimitTreeMap::build(&root_deck, child_decks, &config_map, timing.days_elapsed); let sort_options = sort_options(&root_deck, &config_map); let deck_map = col.storage.get_decks_map()?; Ok(QueueBuilder { new: Vec::new(), review: Vec::new(), learning: Vec::new(), day_learning: Vec::new(), limits, context: Context { timing, config_map, root_deck, sort_options, seen_note_ids: HashMap::new(), deck_map, }, }) } pub(super) fn build(mut self, learn_ahead_secs: i64) -> CardQueues { self.sort_new(); // intraday learning and total learn count let intraday_learning = sort_learning(self.learning); let now = TimestampSecs::now(); let cutoff = now.adding_secs(learn_ahead_secs); let learn_count = intraday_learning .iter() .take_while(|e| e.due <= cutoff) .count() + self.day_learning.len(); let review_count = self.review.len(); let new_count = self.new.len(); // merge interday and new cards into main let with_interday_learn = merge_day_learning( self.review, self.day_learning, self.context.sort_options.day_learn_mix, ); let main_iter = merge_new( with_interday_learn, self.new, self.context.sort_options.new_review_mix, ); CardQueues { counts: Counts { new: new_count, review: review_count, learning: learn_count, }, main: main_iter.collect(), intraday_learning, learn_ahead_secs, current_day: self.context.timing.days_elapsed, build_time: TimestampMillis::now(), current_learning_cutoff: now, } } } fn sort_options(deck: &Deck, config_map: &HashMap) -> QueueSortOptions { deck.config_id() .and_then(|config_id| config_map.get(&config_id)) .map(|config| QueueSortOptions { new_order: config.inner.new_card_sort_order(), new_gather_priority: config.inner.new_card_gather_priority(), review_order: config.inner.review_order(), day_learn_mix: config.inner.interday_learning_mix(), new_review_mix: config.inner.new_mix(), }) .unwrap_or_else(|| { // filtered decks do not space siblings QueueSortOptions { new_order: NewCardSortOrder::NoSort, ..Default::default() } }) } fn merge_day_learning( reviews: Vec, day_learning: Vec, mode: ReviewMix, ) -> Box> { let day_learning_iter = day_learning.into_iter().map(Into::into); let reviews_iter = reviews.into_iter().map(Into::into); match mode { ReviewMix::AfterReviews => Box::new(SizedChain::new(reviews_iter, day_learning_iter)), ReviewMix::BeforeReviews => Box::new(SizedChain::new(day_learning_iter, reviews_iter)), ReviewMix::MixWithReviews => Box::new(Intersperser::new(reviews_iter, day_learning_iter)), } } fn merge_new( review_iter: impl ExactSizeIterator + 'static, new: Vec, mode: ReviewMix, ) -> Box> { let new_iter = new.into_iter().map(Into::into); match mode { ReviewMix::BeforeReviews => Box::new(SizedChain::new(new_iter, review_iter)), ReviewMix::AfterReviews => Box::new(SizedChain::new(review_iter, new_iter)), ReviewMix::MixWithReviews => Box::new(Intersperser::new(review_iter, new_iter)), } } fn sort_learning(mut learning: Vec) -> VecDeque { learning.sort_unstable_by(|a, b| a.due.cmp(&b.due)); learning.into_iter().map(LearningQueueEntry::from).collect() } impl Collection { pub(crate) fn build_queues(&mut self, deck_id: DeckId) -> Result { let mut queues = QueueBuilder::new(self, deck_id)?; self.storage .update_active_decks(&queues.context.root_deck)?; queues.gather_cards(self)?; let queues = queues.build(self.learn_ahead_secs() as i64); Ok(queues) } } #[cfg(test)] mod test { use super::*; use crate::{ backend_proto::deck_config::config::{NewCardGatherPriority, NewCardSortOrder}, card::{CardQueue, CardType}, collection::open_test_collection, }; impl Collection { fn set_deck_gather_order(&mut self, deck: &mut Deck, order: NewCardGatherPriority) { let mut conf = DeckConfig::default(); conf.inner.new_card_gather_priority = order as i32; conf.inner.new_card_sort_order = NewCardSortOrder::NoSort as i32; self.add_or_update_deck_config(&mut conf).unwrap(); deck.normal_mut().unwrap().config_id = conf.id.0; self.add_or_update_deck(deck).unwrap(); } fn set_deck_new_limit(&mut self, deck: &mut Deck, new_limit: u32) { let mut conf = DeckConfig::default(); conf.inner.new_per_day = new_limit; self.add_or_update_deck_config(&mut conf).unwrap(); deck.normal_mut().unwrap().config_id = conf.id.0; self.add_or_update_deck(deck).unwrap(); } fn queue_as_deck_and_template(&mut self, deck_id: DeckId) -> Vec<(DeckId, u16)> { self.build_queues(deck_id) .unwrap() .iter() .map(|entry| { let card = self.storage.get_card(entry.card_id()).unwrap().unwrap(); (card.deck_id, card.template_idx) }) .collect() } fn set_deck_review_order(&mut self, deck: &mut Deck, order: ReviewCardOrder) { let mut conf = DeckConfig::default(); conf.inner.review_order = order as i32; self.add_or_update_deck_config(&mut conf).unwrap(); deck.normal_mut().unwrap().config_id = conf.id.0; self.add_or_update_deck(deck).unwrap(); } fn queue_as_due_and_ivl(&mut self, deck_id: DeckId) -> Vec<(i32, u32)> { self.build_queues(deck_id) .unwrap() .iter() .map(|entry| { let card = self.storage.get_card(entry.card_id()).unwrap().unwrap(); (card.due, card.interval) }) .collect() } } #[test] fn new_queue_building() -> Result<()> { let mut col = open_test_collection(); col.set_config_bool(BoolKey::Sched2021, true, false)?; // parent // ┣━━child━━grandchild // ┗━━child_2 let mut parent = col.get_or_create_normal_deck("Default").unwrap(); let mut child = col.get_or_create_normal_deck("Default::child").unwrap(); let child_2 = col.get_or_create_normal_deck("Default::child_2").unwrap(); let grandchild = col .get_or_create_normal_deck("Default::child::grandchild") .unwrap(); // add 2 new cards to each deck let nt = col.get_notetype_by_name("Cloze")?.unwrap(); let mut note = nt.new_note(); note.set_field(0, "{{c1::}} {{c2::}}")?; for deck in [&parent, &child, &child_2, &grandchild] { note.id.0 = 0; col.add_note(&mut note, deck.id)?; } // set child's new limit to 3, which should affect grandchild col.set_deck_new_limit(&mut child, 3); // depth-first tree order col.set_deck_gather_order(&mut parent, NewCardGatherPriority::Deck); let cards = vec![ (parent.id, 0), (parent.id, 1), (child.id, 0), (child.id, 1), (grandchild.id, 0), (child_2.id, 0), (child_2.id, 1), ]; assert_eq!(col.queue_as_deck_and_template(parent.id), cards); // insertion order col.set_deck_gather_order(&mut parent, NewCardGatherPriority::LowestPosition); let cards = vec![ (parent.id, 0), (parent.id, 1), (child.id, 0), (child.id, 1), (child_2.id, 0), (child_2.id, 1), (grandchild.id, 0), ]; assert_eq!(col.queue_as_deck_and_template(parent.id), cards); // inverted insertion order, but sibling order is preserved col.set_deck_gather_order(&mut parent, NewCardGatherPriority::HighestPosition); let cards = vec![ (grandchild.id, 0), (grandchild.id, 1), (child_2.id, 0), (child_2.id, 1), (child.id, 0), (parent.id, 0), (parent.id, 1), ]; assert_eq!(col.queue_as_deck_and_template(parent.id), cards); Ok(()) } #[test] fn review_queue_building() -> Result<()> { let mut col = open_test_collection(); col.set_config_bool(BoolKey::Sched2021, true, false)?; let mut deck = col.get_or_create_normal_deck("Default").unwrap(); let nt = col.get_notetype_by_name("Basic")?.unwrap(); let mut cards = vec![]; // relative overdueness let expected_queue = vec![ (-150, 1), (-100, 1), (-50, 1), (-150, 5), (-100, 5), (-50, 5), (-150, 20), (-150, 20), (-100, 20), (-50, 20), (-150, 100), (-100, 100), (-50, 100), (0, 1), (0, 5), (0, 20), (0, 100), ]; for t in expected_queue.iter() { let mut note = nt.new_note(); note.set_field(0, "foo")?; note.id.0 = 0; col.add_note(&mut note, deck.id)?; let mut card = col.storage.get_card_by_ordinal(note.id, 0)?.unwrap(); card.interval = t.1; card.due = t.0; card.ctype = CardType::Review; card.queue = CardQueue::Review; cards.push(card); } col.update_cards_maybe_undoable(cards, false)?; col.set_deck_review_order(&mut deck, ReviewCardOrder::RelativeOverdueness); assert_eq!(col.queue_as_due_and_ivl(deck.id), expected_queue); Ok(()) } }