This commit is contained in:
user1823 2026-01-06 23:59:57 +05:30 committed by GitHub
commit a890d4fd81
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 46 additions and 37 deletions

View file

@ -9,7 +9,9 @@ mod sorting;
use std::collections::HashMap; use std::collections::HashMap;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::hash::Hasher;
use fnv::FnvHasher;
use intersperser::Intersperser; use intersperser::Intersperser;
use sized_chain::SizedChain; use sized_chain::SizedChain;
@ -38,6 +40,7 @@ pub(crate) struct DueCard {
pub current_deck_id: DeckId, pub current_deck_id: DeckId,
pub original_deck_id: DeckId, pub original_deck_id: DeckId,
pub kind: DueCardKind, pub kind: DueCardKind,
pub reps: u32,
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -87,6 +90,7 @@ impl From<DueCard> for LearningQueueEntry {
due: TimestampSecs(c.due as i64), due: TimestampSecs(c.due as i64),
id: c.id, id: c.id,
mtime: c.mtime, mtime: c.mtime,
reps: c.reps,
} }
} }
} }
@ -188,12 +192,8 @@ impl QueueBuilder {
let intraday_learning = sort_learning(self.learning); let intraday_learning = sort_learning(self.learning);
let now = TimestampSecs::now(); let now = TimestampSecs::now();
let cutoff = now.adding_secs(learn_ahead_secs); let cutoff = now.adding_secs(learn_ahead_secs);
let learn_count = intraday_learning let learn_count =
.iter() intraday_learning.iter().filter(|e| e.due <= cutoff).count() + self.day_learning.len();
.take_while(|e| e.due <= cutoff)
.count()
+ self.day_learning.len();
let review_count = self.review.len(); let review_count = self.review.len();
let new_count = self.new.len(); let new_count = self.new.len();
@ -274,9 +274,27 @@ fn merge_new(
} }
} }
fn sort_learning(mut learning: Vec<DueCard>) -> VecDeque<LearningQueueEntry> { fn sort_learning(learning: Vec<DueCard>) -> VecDeque<LearningQueueEntry> {
learning.sort_unstable_by(|a, b| a.due.cmp(&b.due)); // Prioritize intraday learning cards that were previously attempted
learning.into_iter().map(LearningQueueEntry::from).collect() // (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<DueCard>, Vec<DueCard>) =
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 { impl Collection {

View file

@ -55,6 +55,7 @@ impl From<&Card> for QueueEntry {
due: TimestampSecs(card.due as i64), due: TimestampSecs(card.due as i64),
id: card.id, id: card.id,
mtime: card.mtime, mtime: card.mtime,
reps: card.reps,
}); });
} }
CardQueue::New => MainQueueEntryKind::New, CardQueue::New => MainQueueEntryKind::New,

View file

@ -12,6 +12,7 @@ pub(crate) struct LearningQueueEntry {
pub due: TimestampSecs, pub due: TimestampSecs,
pub id: CardId, pub id: CardId,
pub mtime: TimestampSecs, pub mtime: TimestampSecs,
pub reps: u32,
} }
impl CardQueues { impl CardQueues {
@ -20,7 +21,7 @@ impl CardQueues {
let cutoff = self.current_learning_cutoff; let cutoff = self.current_learning_cutoff;
self.intraday_learning self.intraday_learning
.iter() .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. /// 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(); let ahead_cutoff = self.current_learn_ahead_cutoff();
self.intraday_learning self.intraday_learning
.iter() .iter()
.skip_while(move |e| e.due <= cutoff) .filter(move |e| e.due > cutoff && e.due <= ahead_cutoff)
.take_while(move |e| e.due <= ahead_cutoff)
} }
/// Increase the cutoff to the current time, and increase the learning count /// Increase the cutoff to the current time, and increase the learning count
@ -46,8 +46,7 @@ impl CardQueues {
let new_learning_cards = self let new_learning_cards = self
.intraday_learning .intraday_learning
.iter() .iter()
.skip_while(|e| e.due <= last_ahead_cutoff) .filter(|e| e.due > last_ahead_cutoff && e.due <= new_ahead_cutoff)
.take_while(|e| e.due <= new_ahead_cutoff)
.count(); .count();
self.counts.learning += new_learning_cards; self.counts.learning += new_learning_cards;
@ -71,6 +70,7 @@ impl CardQueues {
due: TimestampSecs(card.due as i64), due: TimestampSecs(card.due as i64),
id: card.id, id: card.id,
mtime: card.mtime, mtime: card.mtime,
reps: card.reps,
}; };
Some(self.requeue_learning_entry(entry)) Some(self.requeue_learning_entry(entry))
@ -116,16 +116,6 @@ impl CardQueues {
self.main.is_empty() self.main.is_empty()
} }
/// Remove the head of the intraday learning queue, and update counts.
pub(super) fn pop_intraday_learning(&mut self) -> Option<LearningQueueEntry> {
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. /// Add an undone entry to the top of the intraday learning queue.
pub(super) fn push_intraday_learning(&mut self, entry: LearningQueueEntry) { pub(super) fn push_intraday_learning(&mut self, entry: LearningQueueEntry) {
self.intraday_learning.push_front(entry); self.intraday_learning.push_front(entry);

View file

@ -159,17 +159,13 @@ impl CardQueues {
/// Remove the provided card from the top of the queues and /// Remove the provided card from the top of the queues and
/// adjust the counts. If it was not at the top, return an error. /// adjust the counts. If it was not at the top, return an error.
fn pop_entry(&mut self, id: CardId) -> Result<QueueEntry> { fn pop_entry(&mut self, id: CardId) -> Result<QueueEntry> {
// This ignores the current cutoff, so may match if the provided if let Some(pos) = self.intraday_learning.iter().position(|e| e.id == id) {
// learning card is not yet due. It should not happen in normal let entry = self.intraday_learning.remove(pos).unwrap();
// practice, but does happen in the Python unit tests, as they answer // FIXME:
// learning cards early. // under normal circumstances this should not go below 0, but currently
if self // the Python unit tests answer learning cards before they're due
.intraday_learning self.counts.learning = self.counts.learning.saturating_sub(1);
.front() Ok(entry.into())
.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() { } else if self.main.front().filter(|e| e.id == id).is_some() {
Ok(self.pop_main().unwrap().into()) Ok(self.pop_main().unwrap().into())
} else { } else {

View file

@ -4,7 +4,8 @@ SELECT id,
cast(ivl AS integer), cast(ivl AS integer),
cast(mod AS integer), cast(mod AS integer),
did, did,
odid odid,
reps
FROM cards FROM cards
WHERE did IN ( WHERE did IN (
SELECT id SELECT id

View file

@ -3,7 +3,8 @@ SELECT id,
due, due,
cast(mod AS integer), cast(mod AS integer),
did, did,
odid odid,
reps
FROM cards FROM cards
WHERE did IN ( WHERE did IN (
SELECT id SELECT id

View file

@ -267,6 +267,7 @@ impl super::SqliteStorage {
mtime: row.get(3)?, mtime: row.get(3)?,
current_deck_id: row.get(4)?, current_deck_id: row.get(4)?,
original_deck_id: row.get(5)?, original_deck_id: row.get(5)?,
reps: row.get(6)?,
kind: DueCardKind::Learning, kind: DueCardKind::Learning,
}) })
} }
@ -306,6 +307,7 @@ impl super::SqliteStorage {
mtime: row.get(4)?, mtime: row.get(4)?,
current_deck_id: row.get(5)?, current_deck_id: row.get(5)?,
original_deck_id: row.get(6)?, original_deck_id: row.get(6)?,
reps: row.get(7)?,
kind, kind,
})? { })? {
break; break;