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::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<DueCard> 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<DueCard>) -> VecDeque<LearningQueueEntry> {
learning.sort_unstable_by(|a, b| a.due.cmp(&b.due));
learning.into_iter().map(LearningQueueEntry::from).collect()
fn sort_learning(learning: Vec<DueCard>) -> VecDeque<LearningQueueEntry> {
// 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<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 {

View file

@ -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,

View file

@ -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<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.
pub(super) fn push_intraday_learning(&mut self, entry: LearningQueueEntry) {
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
/// adjust the counts. If it was not at the top, return an error.
fn pop_entry(&mut self, id: CardId) -> Result<QueueEntry> {
// 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 {

View file

@ -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

View file

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

View file

@ -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;