// Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html mod builder; mod entry; mod learning; mod main; pub(crate) mod undo; use std::collections::VecDeque; pub(crate) use builder::DueCard; pub(crate) use builder::DueCardKind; pub(crate) use builder::NewCard; pub(crate) use entry::QueueEntry; pub(crate) use entry::QueueEntryKind; pub(crate) use learning::LearningQueueEntry; pub(crate) use main::MainQueueEntry; pub(crate) use main::MainQueueEntryKind; use self::undo::QueueUpdate; use super::states::SchedulingStates; use super::timing::SchedTimingToday; use crate::prelude::*; use crate::timestamp::TimestampSecs; #[derive(Debug)] pub(crate) struct CardQueues { counts: Counts, main: VecDeque, intraday_learning: VecDeque, current_day: u32, learn_ahead_secs: i64, build_time: TimestampMillis, /// Updated each time a card is answered, and by get_queued_cards() when the /// counts are zero. Ensures we don't show a newly-due learning card after a /// user returns from editing a review card. current_learning_cutoff: TimestampSecs, } #[derive(Debug, Copy, Clone)] pub struct Counts { pub new: usize, pub learning: usize, pub review: usize, } impl Counts { fn all_zero(self) -> bool { self.new == 0 && self.learning == 0 && self.review == 0 } } #[derive(Debug, Clone)] pub struct QueuedCard { pub card: Card, pub kind: QueueEntryKind, pub states: SchedulingStates, } #[derive(Debug)] pub struct QueuedCards { pub cards: Vec, pub new_count: usize, pub learning_count: usize, pub review_count: usize, } impl Collection { pub fn get_next_card(&mut self) -> Result> { self.get_queued_cards(1, false) .map(|queued| queued.cards.get(0).cloned()) } pub fn get_queued_cards( &mut self, fetch_limit: usize, intraday_learning_only: bool, ) -> Result { let queues = self.get_queues()?; let counts = queues.counts(); let entries: Vec<_> = if intraday_learning_only { queues .intraday_now_iter() .chain(queues.intraday_ahead_iter()) .map(Into::into) .collect() } else { queues.iter().take(fetch_limit).collect() }; let cards: Vec<_> = entries .into_iter() .map(|entry| { let card = self .storage .get_card(entry.card_id())? .or_not_found(entry.card_id())?; require!( card.mtime == entry.mtime(), "bug: card modified without updating queue: id:{} card:{} entry:{}", card.id, card.mtime, entry.mtime() ); // fixme: pass in card instead of id let next_states = self.get_scheduling_states(card.id)?; Ok(QueuedCard { card, states: next_states, kind: entry.kind(), }) }) .collect::>()?; Ok(QueuedCards { cards, new_count: counts.new, learning_count: counts.learning, review_count: counts.review, }) } } impl CardQueues { /// An iterator over the card queues, in the order the cards will /// be presented. fn iter(&self) -> impl Iterator + '_ { self.intraday_now_iter() .map(Into::into) .chain(self.main.iter().map(Into::into)) .chain(self.intraday_ahead_iter().map(Into::into)) } /// Remove the provided card from the top of the queues and /// adjust the counts. If it was not at the top, return an error. fn pop_entry(&mut self, id: CardId) -> Result { // This ignores the current cutoff, so may match if the provided // learning card is not yet due. It should not happen in normal // practice, but does happen in the Python unit tests, as they answer // learning cards early. if self .intraday_learning .front() .filter(|e| e.id == id) .is_some() { Ok(self.pop_intraday_learning().unwrap().into()) } else if self.main.front().filter(|e| e.id == id).is_some() { Ok(self.pop_main().unwrap().into()) } else { invalid_input!("not at top of queue") } } fn push_undo_entry(&mut self, entry: QueueEntry) { match entry { QueueEntry::IntradayLearning(entry) => self.push_intraday_learning(entry), QueueEntry::Main(entry) => self.push_main(entry), } } /// Return the current due counts. If there are no due cards, the learning /// cutoff is updated to the current time first, and any newly-due learning /// cards are added to the counts. pub(crate) fn counts(&mut self) -> Counts { if self.counts.all_zero() { // we discard the returned undo information in this case self.update_learning_cutoff_and_count(); } self.counts } fn is_stale(&self, current_day: u32) -> bool { self.current_day != current_day } } impl Collection { /// This is automatically done when transact() is called for everything /// except card answers, so unless you are modifying state outside of a /// transaction, you probably don't need this. pub(crate) fn clear_study_queues(&mut self) { self.state.card_queues = None; } pub(crate) fn maybe_clear_study_queues_after_op(&mut self, op: &OpChanges) { if op.op != Op::AnswerCard && op.requires_study_queue_rebuild() { self.state.card_queues = None; } } pub(crate) fn update_queues_after_answering_card( &mut self, card: &Card, timing: SchedTimingToday, ) -> Result<()> { if let Some(queues) = &mut self.state.card_queues { let entry = queues.pop_entry(card.id)?; let requeued_learning = queues.maybe_requeue_learning_card(card, timing); let cutoff_snapshot = queues.update_learning_cutoff_and_count(); let queue_build_time = queues.build_time; self.save_queue_update_undo(Box::new(QueueUpdate { entry, learning_requeue: requeued_learning, queue_build_time, cutoff_snapshot, })); } else { // we currently allow the queues to be empty for unit tests } Ok(()) } /// Get the card queues, building if necessary. pub(crate) fn get_queues(&mut self) -> Result<&mut CardQueues> { let deck = self.get_current_deck()?; self.clear_queues_if_day_changed()?; if self.state.card_queues.is_none() { self.state.card_queues = Some(self.build_queues(deck.id)?); } Ok(self.state.card_queues.as_mut().unwrap()) } // Returns queues if they are valid and have not been rebuilt. If build time has // changed, they are cleared. pub(crate) fn get_or_invalidate_queues( &mut self, build_time: TimestampMillis, ) -> Result> { self.clear_queues_if_day_changed()?; let same_build = self .state .card_queues .as_ref() .map(|q| q.build_time == build_time) .unwrap_or_default(); if same_build { Ok(self.state.card_queues.as_mut()) } else { self.clear_study_queues(); Ok(None) } } fn clear_queues_if_day_changed(&mut self) -> Result<()> { let timing = self.timing_today()?; let day_rolled_over = self .state .card_queues .as_ref() .map(|q| q.is_stale(timing.days_elapsed)) .unwrap_or(false); if day_rolled_over { self.discard_undo_and_study_queues(); self.unbury_on_day_rollover(timing.days_elapsed)?; } Ok(()) } } // test helpers #[cfg(test)] impl Collection { pub(crate) fn counts(&mut self) -> [usize; 3] { self.get_queued_cards(1, false) .map(|q| [q.new_count, q.learning_count, q.review_count]) .unwrap_or([0; 3]) } }