mirror of
https://github.com/ankitects/anki.git
synced 2025-12-01 00:47:12 -05:00
437 lines
14 KiB
Rust
437 lines
14 KiB
Rust
// 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<DueCard> 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<NewCard> for MainQueueEntry {
|
|
fn from(c: NewCard) -> Self {
|
|
MainQueueEntry {
|
|
id: c.id,
|
|
mtime: c.mtime,
|
|
kind: MainQueueEntryKind::New,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<DueCard> 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<NewCard>,
|
|
pub(super) review: Vec<DueCard>,
|
|
pub(super) learning: Vec<DueCard>,
|
|
pub(super) day_learning: Vec<DueCard>,
|
|
limits: LimitTreeMap,
|
|
context: Context,
|
|
}
|
|
|
|
/// Data container and helper for building queues.
|
|
#[derive(Debug, Clone)]
|
|
struct Context {
|
|
timing: SchedTimingToday,
|
|
config_map: HashMap<DeckConfigId, DeckConfig>,
|
|
root_deck: Deck,
|
|
sort_options: QueueSortOptions,
|
|
seen_note_ids: HashMap<NoteId, BuryMode>,
|
|
deck_map: HashMap<DeckId, Deck>,
|
|
}
|
|
|
|
impl QueueBuilder {
|
|
pub(super) fn new(col: &mut Collection, deck_id: DeckId) -> Result<Self> {
|
|
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<DeckConfigId, DeckConfig>) -> 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<DueCard>,
|
|
day_learning: Vec<DueCard>,
|
|
mode: ReviewMix,
|
|
) -> Box<dyn ExactSizeIterator<Item = MainQueueEntry>> {
|
|
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<Item = MainQueueEntry> + 'static,
|
|
new: Vec<NewCard>,
|
|
mode: ReviewMix,
|
|
) -> Box<dyn ExactSizeIterator<Item = MainQueueEntry>> {
|
|
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<DueCard>) -> VecDeque<LearningQueueEntry> {
|
|
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<CardQueues> {
|
|
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(())
|
|
}
|
|
}
|