Create a new kind of revlog entry for reschedule cards on change (#3508)

* create a new kind of revlog entry for Reschedule cards on change

* add comments

* exclude the rescheduled case in reviews graph
This commit is contained in:
Jarrett Ye 2024-10-21 14:47:01 +08:00 committed by GitHub
parent 30e734b925
commit 26ae51fafd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 76 additions and 24 deletions

View file

@ -23,6 +23,7 @@ card-stats-review-log-type-review = Review
card-stats-review-log-type-relearn = Relearn card-stats-review-log-type-relearn = Relearn
card-stats-review-log-type-filtered = Filtered card-stats-review-log-type-filtered = Filtered
card-stats-review-log-type-manual = Manual card-stats-review-log-type-manual = Manual
card-stats-review-log-type-rescheduled = Rescheduled
card-stats-review-log-elapsed-time = Elapsed Time card-stats-review-log-elapsed-time = Elapsed Time
card-stats-no-card = (No card to display.) card-stats-no-card = (No card to display.)
card-stats-custom-data = Custom Data card-stats-custom-data = Custom Data

View file

@ -207,6 +207,7 @@ message RevlogEntry {
RELEARNING = 2; RELEARNING = 2;
FILTERED = 3; FILTERED = 3;
MANUAL = 4; MANUAL = 4;
RESCHEDULED = 5;
} }
int64 id = 1; int64 id = 1;
int64 cid = 2; int64 cid = 2;

View file

@ -72,6 +72,7 @@ pub enum RevlogReviewKind {
/// disabled. /// disabled.
Filtered = 3, Filtered = 3,
Manual = 4, Manual = 4,
Rescheduled = 5,
} }
impl RevlogEntry { impl RevlogEntry {
@ -86,11 +87,32 @@ impl RevlogEntry {
} }
impl Collection { impl Collection {
// set due date or reset
pub(crate) fn log_manually_scheduled_review( pub(crate) fn log_manually_scheduled_review(
&mut self, &mut self,
card: &Card, card: &Card,
original_interval: u32, original_interval: u32,
usn: Usn, usn: Usn,
) -> Result<()> {
self.log_scheduled_review(card, original_interval, usn, RevlogReviewKind::Manual)
}
// reschedule cards on change
pub(crate) fn log_rescheduled_review(
&mut self,
card: &Card,
original_interval: u32,
usn: Usn,
) -> Result<()> {
self.log_scheduled_review(card, original_interval, usn, RevlogReviewKind::Rescheduled)
}
fn log_scheduled_review(
&mut self,
card: &Card,
original_interval: u32,
usn: Usn,
review_kind: RevlogReviewKind,
) -> Result<()> { ) -> Result<()> {
let ease_factor = u32::from( let ease_factor = u32::from(
card.memory_state card.memory_state
@ -106,7 +128,7 @@ impl Collection {
last_interval: i32::try_from(original_interval).unwrap_or(i32::MAX), last_interval: i32::try_from(original_interval).unwrap_or(i32::MAX),
ease_factor, ease_factor,
taken_millis: 0, taken_millis: 0,
review_kind: RevlogReviewKind::Manual, review_kind,
}; };
self.add_revlog_entry_undoable(entry)?; self.add_revlog_entry_undoable(entry)?;
Ok(()) Ok(())

View file

@ -119,9 +119,10 @@ impl Collection {
}; };
*due = (timing.days_elapsed as i32) - days_elapsed *due = (timing.days_elapsed as i32) - days_elapsed
+ card.interval as i32; + card.interval as i32;
// Add a manual revlog entry if the last entry wasn't manual // Add a rescheduled revlog entry if the last entry wasn't
if !last_info.last_revlog_is_manual { // rescheduled
self.log_manually_scheduled_review( if !last_info.last_revlog_is_rescheduled {
self.log_rescheduled_review(
&card, &card,
original_interval, original_interval,
usn, usn,
@ -237,7 +238,7 @@ struct LastRevlogInfo {
last_reviewed_at: Option<TimestampSecs>, last_reviewed_at: Option<TimestampSecs>,
/// If true, the last action on this card was a reschedule, so we /// If true, the last action on this card was a reschedule, so we
/// can avoid writing an extra revlog entry on another reschedule. /// can avoid writing an extra revlog entry on another reschedule.
last_revlog_is_manual: bool, last_revlog_is_rescheduled: bool,
} }
/// Return a map of cards to info about last review/reschedule. /// Return a map of cards to info about last review/reschedule.
@ -249,18 +250,18 @@ fn get_last_revlog_info(revlogs: &[RevlogEntry]) -> HashMap<CardId, LastRevlogIn
.into_iter() .into_iter()
.for_each(|(card_id, group)| { .for_each(|(card_id, group)| {
let mut last_reviewed_at = None; let mut last_reviewed_at = None;
let mut last_revlog_is_manual = false; let mut last_revlog_is_rescheduled = false;
for e in group.into_iter() { for e in group.into_iter() {
if e.button_chosen >= 1 { if e.button_chosen >= 1 {
last_reviewed_at = Some(e.id.as_secs()); last_reviewed_at = Some(e.id.as_secs());
} }
last_revlog_is_manual = e.review_kind == RevlogReviewKind::Manual; last_revlog_is_rescheduled = e.review_kind == RevlogReviewKind::Rescheduled;
} }
out.insert( out.insert(
card_id, card_id,
LastRevlogInfo { LastRevlogInfo {
last_reviewed_at, last_reviewed_at,
last_revlog_is_manual, last_revlog_is_rescheduled,
}, },
); );
}); });
@ -321,7 +322,7 @@ pub(crate) fn single_card_revlog_to_item(
})) }))
} }
} else { } else {
// only manual rescheduling; treat like empty // only manual and rescheduled revlogs; treat like empty
Ok(None) Ok(None)
} }
} else { } else {

View file

@ -84,7 +84,8 @@ impl From<crate::revlog::RevlogReviewKind> for fsrs::RevlogReviewKind {
crate::revlog::RevlogReviewKind::Review => fsrs::RevlogReviewKind::Review, crate::revlog::RevlogReviewKind::Review => fsrs::RevlogReviewKind::Review,
crate::revlog::RevlogReviewKind::Relearning => fsrs::RevlogReviewKind::Relearning, crate::revlog::RevlogReviewKind::Relearning => fsrs::RevlogReviewKind::Relearning,
crate::revlog::RevlogReviewKind::Filtered => fsrs::RevlogReviewKind::Filtered, crate::revlog::RevlogReviewKind::Filtered => fsrs::RevlogReviewKind::Filtered,
crate::revlog::RevlogReviewKind::Manual => fsrs::RevlogReviewKind::Manual, crate::revlog::RevlogReviewKind::Manual
| crate::revlog::RevlogReviewKind::Rescheduled => fsrs::RevlogReviewKind::Manual,
} }
} }
} }

View file

@ -294,6 +294,9 @@ pub(crate) fn single_card_revlog_to_items(
Some(RevlogEntry { Some(RevlogEntry {
review_kind: RevlogReviewKind::Manual, review_kind: RevlogReviewKind::Manual,
.. ..
}) | Some(RevlogEntry {
review_kind: RevlogReviewKind::Rescheduled,
..
}) })
); );
} }
@ -343,10 +346,12 @@ pub(crate) fn single_card_revlog_to_items(
// Filter out unwanted entries // Filter out unwanted entries
entries.retain(|entry| { entries.retain(|entry| {
!( !(
// manually rescheduled // set due date or reset
(entry.review_kind == RevlogReviewKind::Manual || entry.button_chosen == 0) (entry.review_kind == RevlogReviewKind::Manual || entry.button_chosen == 0)
|| // cram || // cram
(entry.review_kind == RevlogReviewKind::Filtered && entry.ease_factor == 0) (entry.review_kind == RevlogReviewKind::Filtered && entry.ease_factor == 0)
|| // rescheduled
(entry.review_kind == RevlogReviewKind::Rescheduled)
) )
}); });
@ -407,6 +412,7 @@ fn revlog_entry_to_proto(e: RevlogEntry) -> anki_proto::stats::RevlogEntry {
RevlogReviewKind::Relearning => revlog_entry::ReviewKind::Relearning, RevlogReviewKind::Relearning => revlog_entry::ReviewKind::Relearning,
RevlogReviewKind::Filtered => revlog_entry::ReviewKind::Filtered, RevlogReviewKind::Filtered => revlog_entry::ReviewKind::Filtered,
RevlogReviewKind::Manual => revlog_entry::ReviewKind::Manual, RevlogReviewKind::Manual => revlog_entry::ReviewKind::Manual,
RevlogReviewKind::Rescheduled => revlog_entry::ReviewKind::Rescheduled,
} as i32, } as i32,
} }
} }

View file

@ -79,7 +79,7 @@ fn interval_bucket(review: &RevlogEntry) -> Option<IntervalBucket> {
} else { } else {
IntervalBucket::Mature IntervalBucket::Mature
}), }),
RevlogReviewKind::Manual => None, RevlogReviewKind::Manual | RevlogReviewKind::Rescheduled => None,
} }
} }

View file

@ -32,7 +32,9 @@ impl GraphsContext {
'outer: for review in &self.revlog { 'outer: for review in &self.revlog {
if matches!( if matches!(
review.review_kind, review.review_kind,
RevlogReviewKind::Filtered | RevlogReviewKind::Manual RevlogReviewKind::Filtered
| RevlogReviewKind::Manual
| RevlogReviewKind::Rescheduled
) { ) {
continue; continue;
} }

View file

@ -53,7 +53,7 @@ impl GraphsContext {
self.revlog self.revlog
.iter() .iter()
.filter(|review| { .filter(|review| {
// not manually rescheduled // not rescheduled/set due date/reset
review.button_chosen > 0 review.button_chosen > 0
// not cramming // not cramming
&& (review.review_kind != RevlogReviewKind::Filtered || review.ease_factor != 0) && (review.review_kind != RevlogReviewKind::Filtered || review.ease_factor != 0)

View file

@ -10,7 +10,9 @@ impl GraphsContext {
pub(super) fn review_counts_and_times(&self) -> ReviewCountsAndTimes { pub(super) fn review_counts_and_times(&self) -> ReviewCountsAndTimes {
let mut data = ReviewCountsAndTimes::default(); let mut data = ReviewCountsAndTimes::default();
for review in &self.revlog { for review in &self.revlog {
if review.review_kind == RevlogReviewKind::Manual { if review.review_kind == RevlogReviewKind::Manual
|| review.review_kind == RevlogReviewKind::Rescheduled
{
continue; continue;
} }
let day = (review.id.as_secs().elapsed_secs_since(self.next_day_start) / 86_400) as i32; let day = (review.id.as_secs().elapsed_secs_since(self.next_day_start) / 86_400) as i32;
@ -38,7 +40,7 @@ impl GraphsContext {
count.filtered += 1; count.filtered += 1;
time.filtered += review.taken_millis; time.filtered += review.taken_millis;
} }
RevlogReviewKind::Manual => unreachable!(), RevlogReviewKind::Manual | RevlogReviewKind::Rescheduled => unreachable!(),
} }
} }
data data

View file

@ -14,7 +14,9 @@ impl GraphsContext {
if review.id.0 < start_of_today_ms { if review.id.0 < start_of_today_ms {
continue; continue;
} }
if review.review_kind == RevlogReviewKind::Manual { if review.review_kind == RevlogReviewKind::Manual
|| review.review_kind == RevlogReviewKind::Rescheduled
{
continue; continue;
} }
// total // total
@ -37,7 +39,7 @@ impl GraphsContext {
RevlogReviewKind::Review => today.review_count += 1, RevlogReviewKind::Review => today.review_count += 1,
RevlogReviewKind::Relearning => today.relearn_count += 1, RevlogReviewKind::Relearning => today.relearn_count += 1,
RevlogReviewKind::Filtered => today.early_review_count += 1, RevlogReviewKind::Filtered => today.early_review_count += 1,
RevlogReviewKind::Manual => unreachable!(), RevlogReviewKind::Manual | RevlogReviewKind::Rescheduled => unreachable!(),
} }
} }
today today

View file

@ -46,6 +46,9 @@ impl From<RevlogReviewKind> for i32 {
RevlogReviewKind::Relearning => anki_proto::stats::revlog_entry::ReviewKind::Relearning, RevlogReviewKind::Relearning => anki_proto::stats::revlog_entry::ReviewKind::Relearning,
RevlogReviewKind::Filtered => anki_proto::stats::revlog_entry::ReviewKind::Filtered, RevlogReviewKind::Filtered => anki_proto::stats::revlog_entry::ReviewKind::Filtered,
RevlogReviewKind::Manual => anki_proto::stats::revlog_entry::ReviewKind::Manual, RevlogReviewKind::Manual => anki_proto::stats::revlog_entry::ReviewKind::Manual,
RevlogReviewKind::Rescheduled => {
anki_proto::stats::revlog_entry::ReviewKind::Rescheduled
}
}) as i32 }) as i32
} }
} }

View file

@ -172,12 +172,19 @@ impl SqliteStorage {
let start = day_cutoff.adding_secs(-86_400).as_millis(); let start = day_cutoff.adding_secs(-86_400).as_millis();
self.db self.db
.prepare_cached(include_str!("studied_today.sql"))? .prepare_cached(include_str!("studied_today.sql"))?
.query_map([start.0, RevlogReviewKind::Manual as i64], |row| { .query_map(
[
start.0,
RevlogReviewKind::Manual as i64,
RevlogReviewKind::Rescheduled as i64,
],
|row| {
Ok(StudiedToday { Ok(StudiedToday {
cards: row.get(0)?, cards: row.get(0)?,
seconds: row.get(1)?, seconds: row.get(1)?,
}) })
})? },
)?
.next() .next()
.unwrap() .unwrap()
.map_err(Into::into) .map_err(Into::into)

View file

@ -3,3 +3,4 @@ SELECT COUNT(),
FROM revlog FROM revlog
WHERE id > ? WHERE id > ?
AND type != ? AND type != ?
AND type != ?

View file

@ -36,6 +36,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
return tr2.cardStatsReviewLogTypeFiltered(); return tr2.cardStatsReviewLogTypeFiltered();
case ReviewKind.MANUAL: case ReviewKind.MANUAL:
return tr2.cardStatsReviewLogTypeManual(); return tr2.cardStatsReviewLogTypeManual();
case ReviewKind.RESCHEDULED:
return tr2.cardStatsReviewLogTypeRescheduled();
} }
} }

View file

@ -48,6 +48,7 @@ function filterDataByTimeRange(data: DataPoint[], maxDays: number): DataPoint[]
export function filterRevlogEntryByReviewKind(entry: RevlogEntry): boolean { export function filterRevlogEntryByReviewKind(entry: RevlogEntry): boolean {
return ( return (
entry.reviewKind !== RevlogEntry_ReviewKind.MANUAL entry.reviewKind !== RevlogEntry_ReviewKind.MANUAL
&& entry.reviewKind !== RevlogEntry_ReviewKind.RESCHEDULED
&& (entry.reviewKind !== RevlogEntry_ReviewKind.FILTERED || entry.ease !== 0) && (entry.reviewKind !== RevlogEntry_ReviewKind.FILTERED || entry.ease !== 0)
); );
} }