mirror of
https://github.com/ankitects/anki.git
synced 2025-11-15 09:07:11 -05:00
* Export last_interval to Python * Add last_interval field to StatsRevlogEntry * Implement last_interval_secs function * Update last_interval field type in stats.proto * Update last_interval to use last_interval_secs
181 lines
5.9 KiB
Rust
181 lines
5.9 KiB
Rust
// Copyright: Ankitects Pty Ltd and contributors
|
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
pub(crate) mod undo;
|
|
|
|
use num_enum::TryFromPrimitive;
|
|
use serde::Deserialize;
|
|
use serde_repr::Deserialize_repr;
|
|
use serde_repr::Serialize_repr;
|
|
use serde_tuple::Serialize_tuple;
|
|
|
|
use crate::define_newtype;
|
|
use crate::prelude::*;
|
|
use crate::serde::default_on_invalid;
|
|
use crate::serde::deserialize_int_from_number;
|
|
|
|
define_newtype!(RevlogId, i64);
|
|
|
|
impl RevlogId {
|
|
pub fn new() -> Self {
|
|
RevlogId(TimestampMillis::now().0)
|
|
}
|
|
|
|
pub fn as_secs(self) -> TimestampSecs {
|
|
TimestampSecs(self.0 / 1000)
|
|
}
|
|
}
|
|
|
|
impl From<TimestampMillis> for RevlogId {
|
|
fn from(m: TimestampMillis) -> Self {
|
|
RevlogId(m.0)
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize_tuple, Deserialize, Debug, Default, PartialEq, Eq, Clone)]
|
|
pub struct RevlogEntry {
|
|
pub id: RevlogId,
|
|
pub cid: CardId,
|
|
pub usn: Usn,
|
|
/// - In the V1 scheduler, 3 represents easy in the learning case.
|
|
/// - 0 represents manual rescheduling.
|
|
#[serde(rename = "ease")]
|
|
pub button_chosen: u8,
|
|
/// Positive values are in days, negative values in seconds.
|
|
#[serde(rename = "ivl", deserialize_with = "deserialize_int_from_number")]
|
|
pub interval: i32,
|
|
/// Positive values are in days, negative values in seconds.
|
|
#[serde(rename = "lastIvl", deserialize_with = "deserialize_int_from_number")]
|
|
pub last_interval: i32,
|
|
/// Card's ease after answering, stored as 10x the %, eg 2500 represents
|
|
/// 250%. When FSRS is active, difficulty is normalized to 100-1100 range,
|
|
/// so a 0 difficulty can be distinguished from SM-2 learning.
|
|
#[serde(rename = "factor", deserialize_with = "deserialize_int_from_number")]
|
|
pub ease_factor: u32,
|
|
/// Amount of milliseconds taken to answer the card.
|
|
#[serde(rename = "time", deserialize_with = "deserialize_int_from_number")]
|
|
pub taken_millis: u32,
|
|
#[serde(rename = "type", default, deserialize_with = "default_on_invalid")]
|
|
pub review_kind: RevlogReviewKind,
|
|
}
|
|
|
|
#[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, Eq, TryFromPrimitive, Clone, Copy)]
|
|
#[repr(u8)]
|
|
#[derive(Default)]
|
|
pub enum RevlogReviewKind {
|
|
#[default]
|
|
Learning = 0,
|
|
Review = 1,
|
|
Relearning = 2,
|
|
/// Old Anki versions called this "Cram" or "Early". It's assigned when
|
|
/// reviewing cards before they're due, or when rescheduling is
|
|
/// disabled.
|
|
Filtered = 3,
|
|
Manual = 4,
|
|
Rescheduled = 5,
|
|
}
|
|
|
|
impl RevlogEntry {
|
|
pub(crate) fn interval_secs(&self) -> u32 {
|
|
u32::try_from(if self.interval > 0 {
|
|
self.interval.saturating_mul(86_400)
|
|
} else {
|
|
self.interval.saturating_mul(-1)
|
|
})
|
|
.unwrap()
|
|
}
|
|
|
|
pub(crate) fn last_interval_secs(&self) -> u32 {
|
|
u32::try_from(if self.last_interval > 0 {
|
|
self.last_interval.saturating_mul(86_400)
|
|
} else {
|
|
self.last_interval.saturating_mul(-1)
|
|
})
|
|
.unwrap()
|
|
}
|
|
|
|
/// Returns true if this entry represents a reset operation.
|
|
/// These entries are created when a card is reset using
|
|
/// [`Collection::reschedule_cards_as_new`].
|
|
/// The 0 value of `ease_factor` differentiates it
|
|
/// from entry created by [`Collection::set_due_date`] that has
|
|
/// `RevlogReviewKind::Manual` but non-zero `ease_factor`.
|
|
pub(crate) fn is_reset(&self) -> bool {
|
|
self.review_kind == RevlogReviewKind::Manual && self.ease_factor == 0
|
|
}
|
|
|
|
/// Returns true if this entry represents a cramming operation.
|
|
/// These entries are created when a card is reviewed in a
|
|
/// filtered deck with "Reschedule cards based on my answers
|
|
/// in this deck" disabled.
|
|
/// [`crate::scheduler::answering::CardStateUpdater::apply_preview_state`].
|
|
/// The 0 value of `ease_factor` distinguishes it from the entry
|
|
/// created when a card is reviewed before its due date in a
|
|
/// filtered deck with reschedule enabled or using Grade Now.
|
|
pub(crate) fn is_cramming(&self) -> bool {
|
|
self.review_kind == RevlogReviewKind::Filtered && self.ease_factor == 0
|
|
}
|
|
|
|
pub(crate) fn has_rating(&self) -> bool {
|
|
self.button_chosen > 0
|
|
}
|
|
|
|
/// Returns true if the review entry is not manually rescheduled and not
|
|
/// cramming. Used to filter out entries that shouldn't be considered
|
|
/// for statistics and scheduling.
|
|
pub(crate) fn has_rating_and_affects_scheduling(&self) -> bool {
|
|
// not rescheduled/set due date/reset
|
|
self.has_rating()
|
|
// not cramming
|
|
&& !self.is_cramming()
|
|
}
|
|
}
|
|
|
|
impl Collection {
|
|
// set due date or reset
|
|
pub(crate) fn log_manually_scheduled_review(
|
|
&mut self,
|
|
card: &Card,
|
|
original_interval: u32,
|
|
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<()> {
|
|
let ease_factor = u32::from(
|
|
card.memory_state
|
|
.map(|s| ((s.difficulty_shifted() * 1000.) as u16))
|
|
.unwrap_or(card.ease_factor),
|
|
);
|
|
let entry = RevlogEntry {
|
|
id: RevlogId::new(),
|
|
cid: card.id,
|
|
usn,
|
|
button_chosen: 0,
|
|
interval: i32::try_from(card.interval).unwrap_or(i32::MAX),
|
|
last_interval: i32::try_from(original_interval).unwrap_or(i32::MAX),
|
|
ease_factor,
|
|
taken_millis: 0,
|
|
review_kind,
|
|
};
|
|
self.add_revlog_entry_undoable(entry)?;
|
|
Ok(())
|
|
}
|
|
}
|