This commit is contained in:
user1823 2025-10-29 11:31:08 -07:00 committed by GitHub
commit 2b7542fc9c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 79 additions and 5 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,
} }
} }
} }
@ -274,9 +278,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 {
@ -543,4 +565,49 @@ mod test {
col.set_current_deck(child.id).unwrap(); col.set_current_deck(child.id).unwrap();
assert_eq!(col.card_queue_len(), 0); assert_eq!(col.card_queue_len(), 0);
} }
#[test]
fn intraday_learning_prioritizes_previously_attempted() -> Result<()> {
let mut col = Collection::new();
// create two notes with one card each
let nt = col.get_notetype_by_name("Basic")?.unwrap();
let mut n1 = nt.new_note();
n1.set_field(0, "one")?;
col.add_note(&mut n1, DeckId(1))?;
let mut n2 = nt.new_note();
n2.set_field(0, "two")?;
col.add_note(&mut n2, DeckId(1))?;
// fetch their cards and make them intraday learning cards
let mut c1 = col.storage.get_card_by_ordinal(n1.id, 0)?.unwrap();
let mut c2 = col.storage.get_card_by_ordinal(n2.id, 0)?.unwrap();
c1.queue = CardQueue::Learn;
c2.queue = CardQueue::Learn;
// c1: never attempted, due earlier (0)
c1.due = 0;
c1.reps = 0;
// c2: previously attempted, due later (600)
c2.due = 600;
c2.reps = 1;
let id1 = c1.id;
let id2 = c2.id;
col.update_cards_maybe_undoable(vec![c1, c2], false)?;
// build queues and inspect intraday learning order
let queues = col.build_queues(DeckId(1))?;
let ids: Vec<_> = queues.intraday_learning.iter().map(|e| e.id).collect();
// ensure the previously attempted card appears before the never-attempted one
assert!(
ids.iter().position(|&id| id == id2).unwrap()
< ids.iter().position(|&id| id == id1).unwrap()
);
Ok(())
}
} }

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 {
@ -71,6 +72,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))

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;