Anki/rslib/src/scheduler/queue/builder/mod.rs
Abdo 964f0a5763
Add relative overdueness to review order (#1757)
* Add relative overdueness to review order

* Add test for relative overdue
2022-04-09 13:20:09 +10:00

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(())
}
}