mirror of
https://github.com/ankitects/anki.git
synced 2025-09-23 16:26:40 -04:00

The original rationale was avoiding a possible O(n) insertion if the learning card was due outside the cutoff, but the increased code complexity doesn't seem worth it, given that learning cards will rarely grow above 1000. Also added a currently-disabled test that demonstrates the current undo handling behaviour is yielding incorrect counts; that will be reworked in the next commit, and this change will make that easier.
227 lines
6.7 KiB
Rust
227 lines
6.7 KiB
Rust
// 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 limits;
|
|
mod main;
|
|
pub(crate) mod undo;
|
|
|
|
use std::collections::VecDeque;
|
|
|
|
pub(crate) use builder::{DueCard, NewCard};
|
|
pub(crate) use entry::{QueueEntry, QueueEntryKind};
|
|
pub(crate) use learning::LearningQueueEntry;
|
|
pub(crate) use main::{MainQueueEntry, MainQueueEntryKind};
|
|
|
|
use self::undo::QueueUpdate;
|
|
use super::{states::NextCardStates, timing::SchedTimingToday};
|
|
use crate::{backend_proto as pb, prelude::*, timestamp::TimestampSecs};
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct CardQueues {
|
|
counts: Counts,
|
|
/// Any undone items take precedence.
|
|
undo: Vec<QueueEntry>,
|
|
main: VecDeque<MainQueueEntry>,
|
|
learning: VecDeque<LearningQueueEntry>,
|
|
selected_deck: DeckId,
|
|
current_day: u32,
|
|
learn_ahead_secs: i64,
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone)]
|
|
pub(crate) struct Counts {
|
|
pub new: usize,
|
|
pub learning: usize,
|
|
pub review: usize,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct QueuedCard {
|
|
pub card: Card,
|
|
pub kind: QueueEntryKind,
|
|
pub next_states: NextCardStates,
|
|
}
|
|
|
|
pub(crate) struct QueuedCards {
|
|
pub cards: Vec<QueuedCard>,
|
|
pub new_count: usize,
|
|
pub learning_count: usize,
|
|
pub review_count: usize,
|
|
}
|
|
|
|
impl CardQueues {
|
|
/// Get the next due card, if there is one.
|
|
fn next_entry(&mut self, now: TimestampSecs) -> Option<QueueEntry> {
|
|
self.next_undo_entry()
|
|
.map(Into::into)
|
|
.or_else(|| self.next_learning_entry_due_before_now(now).map(Into::into))
|
|
.or_else(|| self.next_main_entry().map(Into::into))
|
|
.or_else(|| self.next_learning_entry_learning_ahead().map(Into::into))
|
|
}
|
|
|
|
/// Remove the provided card from the top of the queues.
|
|
/// If it was not at the top, return an error.
|
|
fn pop_answered(&mut self, id: CardId) -> Result<QueueEntry> {
|
|
if let Some(entry) = self.pop_undo_entry(id) {
|
|
Ok(entry)
|
|
} else if let Some(entry) = self.pop_main_entry(id) {
|
|
Ok(entry.into())
|
|
} else if let Some(entry) = self.pop_learning_entry(id) {
|
|
Ok(entry.into())
|
|
} else {
|
|
Err(AnkiError::invalid_input("not at top of queue"))
|
|
}
|
|
}
|
|
|
|
pub(crate) fn counts(&self) -> Counts {
|
|
self.counts
|
|
}
|
|
|
|
fn is_stale(&self, current_day: u32) -> bool {
|
|
self.current_day != current_day
|
|
}
|
|
|
|
fn update_after_answering_card(
|
|
&mut self,
|
|
card: &Card,
|
|
timing: SchedTimingToday,
|
|
) -> Result<Box<QueueUpdate>> {
|
|
let entry = self.pop_answered(card.id)?;
|
|
let requeued_learning = self.maybe_requeue_learning_card(card, timing);
|
|
|
|
Ok(Box::new(QueueUpdate {
|
|
entry,
|
|
learning_requeue: requeued_learning,
|
|
}))
|
|
}
|
|
}
|
|
|
|
impl Collection {
|
|
pub(crate) fn get_queued_cards(
|
|
&mut self,
|
|
fetch_limit: u32,
|
|
intraday_learning_only: bool,
|
|
) -> Result<pb::GetQueuedCardsOut> {
|
|
if let Some(next_cards) = self.next_cards(fetch_limit, intraday_learning_only)? {
|
|
Ok(pb::GetQueuedCardsOut {
|
|
value: Some(pb::get_queued_cards_out::Value::QueuedCards(
|
|
next_cards.into(),
|
|
)),
|
|
})
|
|
} else {
|
|
Ok(pb::GetQueuedCardsOut {
|
|
value: Some(pb::get_queued_cards_out::Value::CongratsInfo(
|
|
self.congrats_info()?,
|
|
)),
|
|
})
|
|
}
|
|
}
|
|
|
|
/// 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.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 mutation = queues.update_after_answering_card(card, timing)?;
|
|
self.save_queue_update_undo(mutation);
|
|
Ok(())
|
|
} else {
|
|
// we currenly allow the queues to be empty for unit tests
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub(crate) fn get_queues(&mut self) -> Result<&mut CardQueues> {
|
|
let timing = self.timing_today()?;
|
|
let deck = self.get_current_deck_id();
|
|
let need_rebuild = self
|
|
.state
|
|
.card_queues
|
|
.as_ref()
|
|
.map(|q| q.is_stale(timing.days_elapsed))
|
|
.unwrap_or(true);
|
|
if need_rebuild {
|
|
self.state.card_queues = Some(self.build_queues(deck)?);
|
|
}
|
|
|
|
Ok(self.state.card_queues.as_mut().unwrap())
|
|
}
|
|
|
|
fn next_cards(
|
|
&mut self,
|
|
_fetch_limit: u32,
|
|
_intraday_learning_only: bool,
|
|
) -> Result<Option<QueuedCards>> {
|
|
let queues = self.get_queues()?;
|
|
let mut cards = vec![];
|
|
if let Some(entry) = queues.next_entry(TimestampSecs::now()) {
|
|
let card = self
|
|
.storage
|
|
.get_card(entry.card_id())?
|
|
.ok_or(AnkiError::NotFound)?;
|
|
if card.mtime != entry.mtime() {
|
|
return Err(AnkiError::invalid_input(
|
|
"bug: card modified without updating queue",
|
|
));
|
|
}
|
|
|
|
// fixme: pass in card instead of id
|
|
let next_states = self.get_next_card_states(card.id)?;
|
|
|
|
cards.push(QueuedCard {
|
|
card,
|
|
next_states,
|
|
kind: entry.kind(),
|
|
});
|
|
}
|
|
|
|
if cards.is_empty() {
|
|
Ok(None)
|
|
} else {
|
|
let counts = self.get_queues()?.counts();
|
|
Ok(Some(QueuedCards {
|
|
cards,
|
|
new_count: counts.new,
|
|
learning_count: counts.learning,
|
|
review_count: counts.review,
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
|
|
// test helpers
|
|
#[cfg(test)]
|
|
impl Collection {
|
|
pub(crate) fn next_card(&mut self) -> Result<Option<QueuedCard>> {
|
|
Ok(self
|
|
.next_cards(1, false)?
|
|
.map(|mut resp| resp.cards.pop().unwrap()))
|
|
}
|
|
|
|
pub(crate) fn get_queue_single(&mut self) -> Result<QueuedCards> {
|
|
self.next_cards(1, false)?.ok_or(AnkiError::NotFound)
|
|
}
|
|
|
|
pub(crate) fn counts(&mut self) -> [usize; 3] {
|
|
self.get_queue_single()
|
|
.map(|q| [q.new_count, q.learning_count, q.review_count])
|
|
.unwrap_or([0; 3])
|
|
}
|
|
}
|