diff --git a/rslib/src/scheduler/queue/builder/mod.rs b/rslib/src/scheduler/queue/builder/mod.rs index 064220bce..86f82fbc6 100644 --- a/rslib/src/scheduler/queue/builder/mod.rs +++ b/rslib/src/scheduler/queue/builder/mod.rs @@ -9,7 +9,9 @@ mod sorting; use std::collections::HashMap; use std::collections::VecDeque; +use std::hash::Hasher; +use fnv::FnvHasher; use intersperser::Intersperser; use sized_chain::SizedChain; @@ -38,6 +40,7 @@ pub(crate) struct DueCard { pub current_deck_id: DeckId, pub original_deck_id: DeckId, pub kind: DueCardKind, + pub reps: u32, } #[derive(Debug, Clone, Copy)] @@ -87,6 +90,7 @@ impl From for LearningQueueEntry { due: TimestampSecs(c.due as i64), id: c.id, mtime: c.mtime, + reps: c.reps, } } } @@ -188,12 +192,8 @@ impl QueueBuilder { 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 learn_count = + intraday_learning.iter().filter(|e| e.due <= cutoff).count() + self.day_learning.len(); let review_count = self.review.len(); let new_count = self.new.len(); @@ -274,9 +274,27 @@ fn merge_new( } } -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() +fn sort_learning(learning: Vec) -> VecDeque { + // Prioritize intraday learning cards that were previously attempted + // (reps > 0) before never-attempted cards (reps == 0). Sort previously + // attempted cards by due-time and never-attempted cards deterministically + // by an FNV hash of their id. + let (mut previously_attempted, mut never_attempted): (Vec, Vec) = + learning.into_iter().partition(|c| c.reps > 0); + + previously_attempted.sort_unstable_by(|a, b| a.due.cmp(&b.due)); + + never_attempted.sort_unstable_by_key(|c| { + let mut hasher = FnvHasher::default(); + hasher.write_i64(c.id.0); + hasher.finish() + }); + + previously_attempted + .into_iter() + .chain(never_attempted) + .map(LearningQueueEntry::from) + .collect() } impl Collection { diff --git a/rslib/src/scheduler/queue/entry.rs b/rslib/src/scheduler/queue/entry.rs index c9e587747..1ca9de478 100644 --- a/rslib/src/scheduler/queue/entry.rs +++ b/rslib/src/scheduler/queue/entry.rs @@ -55,6 +55,7 @@ impl From<&Card> for QueueEntry { due: TimestampSecs(card.due as i64), id: card.id, mtime: card.mtime, + reps: card.reps, }); } CardQueue::New => MainQueueEntryKind::New, diff --git a/rslib/src/scheduler/queue/learning.rs b/rslib/src/scheduler/queue/learning.rs index 79d0304c9..997348147 100644 --- a/rslib/src/scheduler/queue/learning.rs +++ b/rslib/src/scheduler/queue/learning.rs @@ -12,6 +12,7 @@ pub(crate) struct LearningQueueEntry { pub due: TimestampSecs, pub id: CardId, pub mtime: TimestampSecs, + pub reps: u32, } impl CardQueues { @@ -20,7 +21,7 @@ impl CardQueues { let cutoff = self.current_learning_cutoff; self.intraday_learning .iter() - .take_while(move |e| e.due <= cutoff) + .filter(move |e| e.due <= cutoff) } /// Intraday learning cards that can be shown after the main queue is empty. @@ -29,8 +30,7 @@ impl CardQueues { let ahead_cutoff = self.current_learn_ahead_cutoff(); self.intraday_learning .iter() - .skip_while(move |e| e.due <= cutoff) - .take_while(move |e| e.due <= ahead_cutoff) + .filter(move |e| e.due > cutoff && e.due <= ahead_cutoff) } /// Increase the cutoff to the current time, and increase the learning count @@ -46,8 +46,7 @@ impl CardQueues { let new_learning_cards = self .intraday_learning .iter() - .skip_while(|e| e.due <= last_ahead_cutoff) - .take_while(|e| e.due <= new_ahead_cutoff) + .filter(|e| e.due > last_ahead_cutoff && e.due <= new_ahead_cutoff) .count(); self.counts.learning += new_learning_cards; @@ -71,6 +70,7 @@ impl CardQueues { due: TimestampSecs(card.due as i64), id: card.id, mtime: card.mtime, + reps: card.reps, }; Some(self.requeue_learning_entry(entry)) @@ -116,16 +116,6 @@ impl CardQueues { self.main.is_empty() } - /// Remove the head of the intraday learning queue, and update counts. - pub(super) fn pop_intraday_learning(&mut self) -> Option { - self.intraday_learning.pop_front().inspect(|_head| { - // FIXME: - // under normal circumstances this should not go below 0, but currently - // the Python unit tests answer learning cards before they're due - self.counts.learning = self.counts.learning.saturating_sub(1); - }) - } - /// Add an undone entry to the top of the intraday learning queue. pub(super) fn push_intraday_learning(&mut self, entry: LearningQueueEntry) { self.intraday_learning.push_front(entry); diff --git a/rslib/src/scheduler/queue/mod.rs b/rslib/src/scheduler/queue/mod.rs index 1352d6735..f869fd85a 100644 --- a/rslib/src/scheduler/queue/mod.rs +++ b/rslib/src/scheduler/queue/mod.rs @@ -159,17 +159,13 @@ impl CardQueues { /// 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()) + if let Some(pos) = self.intraday_learning.iter().position(|e| e.id == id) { + let entry = self.intraday_learning.remove(pos).unwrap(); + // FIXME: + // under normal circumstances this should not go below 0, but currently + // the Python unit tests answer learning cards before they're due + self.counts.learning = self.counts.learning.saturating_sub(1); + Ok(entry.into()) } else if self.main.front().filter(|e| e.id == id).is_some() { Ok(self.pop_main().unwrap().into()) } else { diff --git a/rslib/src/storage/card/due_cards.sql b/rslib/src/storage/card/due_cards.sql index 0c29b5a31..149cc0f27 100644 --- a/rslib/src/storage/card/due_cards.sql +++ b/rslib/src/storage/card/due_cards.sql @@ -4,7 +4,8 @@ SELECT id, cast(ivl AS integer), cast(mod AS integer), did, - odid + odid, + reps FROM cards WHERE did IN ( SELECT id diff --git a/rslib/src/storage/card/intraday_due.sql b/rslib/src/storage/card/intraday_due.sql index 634cacd6b..a24874659 100644 --- a/rslib/src/storage/card/intraday_due.sql +++ b/rslib/src/storage/card/intraday_due.sql @@ -3,7 +3,8 @@ SELECT id, due, cast(mod AS integer), did, - odid + odid, + reps FROM cards WHERE did IN ( SELECT id diff --git a/rslib/src/storage/card/mod.rs b/rslib/src/storage/card/mod.rs index 9e06edf07..28eaee9b3 100644 --- a/rslib/src/storage/card/mod.rs +++ b/rslib/src/storage/card/mod.rs @@ -267,6 +267,7 @@ impl super::SqliteStorage { mtime: row.get(3)?, current_deck_id: row.get(4)?, original_deck_id: row.get(5)?, + reps: row.get(6)?, kind: DueCardKind::Learning, }) } @@ -306,6 +307,7 @@ impl super::SqliteStorage { mtime: row.get(4)?, current_deck_id: row.get(5)?, original_deck_id: row.get(6)?, + reps: row.get(7)?, kind, })? { break;