mirror of
https://github.com/ankitects/anki.git
synced 2025-12-10 13:26:56 -05:00
split state fetching, revlog and preview code out
This commit is contained in:
parent
6a44269280
commit
e74210717a
4 changed files with 240 additions and 200 deletions
133
rslib/src/scheduler/answering/current_state.rs
Normal file
133
rslib/src/scheduler/answering/current_state.rs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use crate::{
|
||||
card::CardType,
|
||||
decks::DeckKind,
|
||||
scheduler::states::{
|
||||
steps::LearningSteps, CardState, LearnState, NewState, NormalState, PreviewState,
|
||||
RelearnState, ReschedulingFilterState, ReviewState, StateContext,
|
||||
},
|
||||
};
|
||||
|
||||
use super::CardStateUpdater;
|
||||
|
||||
impl CardStateUpdater {
|
||||
pub(crate) fn state_context(&self) -> StateContext<'_> {
|
||||
StateContext {
|
||||
fuzz_seed: self.fuzz_seed,
|
||||
steps: self.learn_steps(),
|
||||
graduating_interval_good: self.config.inner.graduating_interval_good,
|
||||
graduating_interval_easy: self.config.inner.graduating_interval_easy,
|
||||
hard_multiplier: self.config.inner.hard_multiplier,
|
||||
easy_multiplier: self.config.inner.easy_multiplier,
|
||||
interval_multiplier: self.config.inner.interval_multiplier,
|
||||
maximum_review_interval: self.config.inner.maximum_review_interval,
|
||||
leech_threshold: self.config.inner.leech_threshold,
|
||||
relearn_steps: self.relearn_steps(),
|
||||
lapse_multiplier: self.config.inner.lapse_multiplier,
|
||||
minimum_lapse_interval: self.config.inner.minimum_lapse_interval,
|
||||
in_filtered_deck: self.deck.is_filtered(),
|
||||
preview_step: if let DeckKind::Filtered(deck) = &self.deck.kind {
|
||||
deck.preview_delay
|
||||
} else {
|
||||
0
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn current_card_state(&self) -> CardState {
|
||||
let due = match &self.deck.kind {
|
||||
DeckKind::Normal(_) => {
|
||||
// if not in a filtered deck, ensure due time is not before today,
|
||||
// which avoids tripping up test_nextIvl() in the Python tests
|
||||
if matches!(self.card.ctype, CardType::Review) {
|
||||
self.card.due.min(self.timing.days_elapsed as i32)
|
||||
} else {
|
||||
self.card.due
|
||||
}
|
||||
}
|
||||
DeckKind::Filtered(_) => {
|
||||
if self.card.original_due != 0 {
|
||||
self.card.original_due
|
||||
} else {
|
||||
// v2 scheduler resets original_due on first answer
|
||||
self.card.due
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let normal_state = self.normal_study_state(due);
|
||||
|
||||
match &self.deck.kind {
|
||||
// normal decks have normal state
|
||||
DeckKind::Normal(_) => normal_state.into(),
|
||||
// filtered decks wrap the normal state
|
||||
DeckKind::Filtered(filtered) => {
|
||||
if filtered.reschedule {
|
||||
ReschedulingFilterState {
|
||||
original_state: normal_state,
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
PreviewState {
|
||||
scheduled_secs: filtered.preview_delay * 60,
|
||||
original_state: normal_state,
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn normal_study_state(&self, due: i32) -> NormalState {
|
||||
let interval = self.card.interval;
|
||||
let lapses = self.card.lapses;
|
||||
let ease_factor = self.card.ease_factor();
|
||||
let remaining_steps = self.card.remaining_steps();
|
||||
|
||||
match self.card.ctype {
|
||||
CardType::New => NormalState::New(NewState {
|
||||
position: due.max(0) as u32,
|
||||
}),
|
||||
CardType::Learn => {
|
||||
LearnState {
|
||||
scheduled_secs: self.learn_steps().current_delay_secs(remaining_steps),
|
||||
remaining_steps,
|
||||
}
|
||||
}
|
||||
.into(),
|
||||
CardType::Review => ReviewState {
|
||||
scheduled_days: interval,
|
||||
elapsed_days: ((interval as i32) - (due - self.timing.days_elapsed as i32)).max(0)
|
||||
as u32,
|
||||
ease_factor,
|
||||
lapses,
|
||||
leeched: false,
|
||||
}
|
||||
.into(),
|
||||
CardType::Relearn => RelearnState {
|
||||
learning: LearnState {
|
||||
scheduled_secs: self.relearn_steps().current_delay_secs(remaining_steps),
|
||||
remaining_steps,
|
||||
},
|
||||
review: ReviewState {
|
||||
scheduled_days: interval,
|
||||
elapsed_days: interval,
|
||||
ease_factor,
|
||||
lapses,
|
||||
leeched: false,
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn learn_steps(&self) -> LearningSteps<'_> {
|
||||
LearningSteps::new(&self.config.inner.learn_steps)
|
||||
}
|
||||
|
||||
fn relearn_steps(&self) -> LearningSteps<'_> {
|
||||
LearningSteps::new(&self.config.inner.relearn_steps)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +1,25 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
mod current_state;
|
||||
mod preview;
|
||||
mod revlog;
|
||||
|
||||
use crate::{
|
||||
backend_proto,
|
||||
card::{CardQueue, CardType},
|
||||
deckconf::{DeckConf, LeechAction},
|
||||
decks::{Deck, DeckKind},
|
||||
decks::Deck,
|
||||
prelude::*,
|
||||
revlog::{RevlogEntry, RevlogReviewKind},
|
||||
};
|
||||
|
||||
use revlog::RevlogEntryPartial;
|
||||
|
||||
use super::{
|
||||
cutoff::SchedTimingToday,
|
||||
states::{
|
||||
steps::LearningSteps, CardState, FilteredState, IntervalKind, LearnState, NewState,
|
||||
NextCardStates, NormalState, PreviewState, RelearnState, ReschedulingFilterState,
|
||||
ReviewState, StateContext,
|
||||
CardState, FilteredState, IntervalKind, LearnState, NewState, NextCardStates, NormalState,
|
||||
RelearnState, ReschedulingFilterState, ReviewState,
|
||||
},
|
||||
timespan::answer_button_time_collapsible,
|
||||
};
|
||||
|
|
@ -51,132 +55,14 @@ struct CardStateUpdater {
|
|||
}
|
||||
|
||||
impl CardStateUpdater {
|
||||
fn state_context(&self) -> StateContext<'_> {
|
||||
StateContext {
|
||||
fuzz_seed: self.fuzz_seed,
|
||||
steps: self.learn_steps(),
|
||||
graduating_interval_good: self.config.inner.graduating_interval_good,
|
||||
graduating_interval_easy: self.config.inner.graduating_interval_easy,
|
||||
hard_multiplier: self.config.inner.hard_multiplier,
|
||||
easy_multiplier: self.config.inner.easy_multiplier,
|
||||
interval_multiplier: self.config.inner.interval_multiplier,
|
||||
maximum_review_interval: self.config.inner.maximum_review_interval,
|
||||
leech_threshold: self.config.inner.leech_threshold,
|
||||
relearn_steps: self.relearn_steps(),
|
||||
lapse_multiplier: self.config.inner.lapse_multiplier,
|
||||
minimum_lapse_interval: self.config.inner.minimum_lapse_interval,
|
||||
in_filtered_deck: self.deck.is_filtered(),
|
||||
preview_step: if let DeckKind::Filtered(deck) = &self.deck.kind {
|
||||
deck.preview_delay
|
||||
} else {
|
||||
0
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn secs_until_rollover(&self) -> u32 {
|
||||
(self.timing.next_day_at - self.now.0).max(0) as u32
|
||||
}
|
||||
|
||||
fn normal_study_state(&self, due: i32) -> NormalState {
|
||||
let interval = self.card.interval;
|
||||
let lapses = self.card.lapses;
|
||||
let ease_factor = self.card.ease_factor();
|
||||
let remaining_steps = self.card.remaining_steps();
|
||||
|
||||
match self.card.ctype {
|
||||
CardType::New => NormalState::New(NewState {
|
||||
position: due.max(0) as u32,
|
||||
}),
|
||||
CardType::Learn => {
|
||||
LearnState {
|
||||
scheduled_secs: self.learn_steps().current_delay_secs(remaining_steps),
|
||||
remaining_steps,
|
||||
}
|
||||
}
|
||||
.into(),
|
||||
CardType::Review => ReviewState {
|
||||
scheduled_days: interval,
|
||||
elapsed_days: ((interval as i32) - (due - self.timing.days_elapsed as i32)).max(0)
|
||||
as u32,
|
||||
ease_factor,
|
||||
lapses,
|
||||
leeched: false,
|
||||
}
|
||||
.into(),
|
||||
CardType::Relearn => RelearnState {
|
||||
learning: LearnState {
|
||||
scheduled_secs: self.relearn_steps().current_delay_secs(remaining_steps),
|
||||
remaining_steps,
|
||||
},
|
||||
review: ReviewState {
|
||||
scheduled_days: interval,
|
||||
elapsed_days: interval,
|
||||
ease_factor,
|
||||
lapses,
|
||||
leeched: false,
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn current_card_state(&self) -> CardState {
|
||||
let due = match &self.deck.kind {
|
||||
DeckKind::Normal(_) => {
|
||||
// if not in a filtered deck, ensure due time is not before today,
|
||||
// which avoids tripping up test_nextIvl() in the Python tests
|
||||
if matches!(self.card.ctype, CardType::Review) {
|
||||
self.card.due.min(self.timing.days_elapsed as i32)
|
||||
} else {
|
||||
self.card.due
|
||||
}
|
||||
}
|
||||
DeckKind::Filtered(_) => {
|
||||
if self.card.original_due != 0 {
|
||||
self.card.original_due
|
||||
} else {
|
||||
// v2 scheduler resets original_due on first answer
|
||||
self.card.due
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let normal_state = self.normal_study_state(due);
|
||||
|
||||
match &self.deck.kind {
|
||||
// normal decks have normal state
|
||||
DeckKind::Normal(_) => normal_state.into(),
|
||||
// filtered decks wrap the normal state
|
||||
DeckKind::Filtered(filtered) => {
|
||||
if filtered.reschedule {
|
||||
ReschedulingFilterState {
|
||||
original_state: normal_state,
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
PreviewState {
|
||||
scheduled_secs: filtered.preview_delay * 60,
|
||||
original_state: normal_state,
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn into_card(self) -> Card {
|
||||
self.card
|
||||
}
|
||||
|
||||
fn learn_steps(&self) -> LearningSteps<'_> {
|
||||
LearningSteps::new(&self.config.inner.learn_steps)
|
||||
}
|
||||
|
||||
fn relearn_steps(&self) -> LearningSteps<'_> {
|
||||
LearningSteps::new(&self.config.inner.relearn_steps)
|
||||
}
|
||||
|
||||
fn apply_study_state(
|
||||
&mut self,
|
||||
current: CardState,
|
||||
|
|
@ -309,34 +195,6 @@ impl CardStateUpdater {
|
|||
))
|
||||
}
|
||||
|
||||
// fixme: check learning card moved into preview
|
||||
// restores correctly in both learn and day-learn case
|
||||
fn apply_preview_state(
|
||||
&mut self,
|
||||
current: CardState,
|
||||
next: PreviewState,
|
||||
) -> Result<Option<RevlogEntryPartial>> {
|
||||
self.ensure_filtered()?;
|
||||
self.card.queue = CardQueue::PreviewRepeat;
|
||||
|
||||
let interval = next.interval_kind();
|
||||
match interval {
|
||||
IntervalKind::InSecs(secs) => {
|
||||
self.card.due = TimestampSecs::now().0 as i32 + secs as i32;
|
||||
}
|
||||
IntervalKind::InDays(_days) => {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
Ok(RevlogEntryPartial::maybe_new(
|
||||
current,
|
||||
next.into(),
|
||||
0.0,
|
||||
self.secs_until_rollover(),
|
||||
))
|
||||
}
|
||||
|
||||
fn apply_rescheduling_filter_state(
|
||||
&mut self,
|
||||
current: CardState,
|
||||
|
|
@ -369,55 +227,6 @@ impl Rating {
|
|||
}
|
||||
}
|
||||
}
|
||||
pub struct RevlogEntryPartial {
|
||||
interval: IntervalKind,
|
||||
last_interval: IntervalKind,
|
||||
ease_factor: f32,
|
||||
review_kind: RevlogReviewKind,
|
||||
}
|
||||
|
||||
impl RevlogEntryPartial {
|
||||
/// Returns None in the Preview case, since preview cards do not currently log.
|
||||
fn maybe_new(
|
||||
current: CardState,
|
||||
next: CardState,
|
||||
ease_factor: f32,
|
||||
secs_until_rollover: u32,
|
||||
) -> Option<Self> {
|
||||
current.revlog_kind().map(|review_kind| {
|
||||
let next_interval = next.interval_kind().maybe_as_days(secs_until_rollover);
|
||||
let current_interval = current.interval_kind().maybe_as_days(secs_until_rollover);
|
||||
|
||||
RevlogEntryPartial {
|
||||
interval: next_interval,
|
||||
last_interval: current_interval,
|
||||
ease_factor,
|
||||
review_kind,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn into_revlog_entry(
|
||||
self,
|
||||
usn: Usn,
|
||||
cid: CardID,
|
||||
button_chosen: u8,
|
||||
answered_at: TimestampMillis,
|
||||
taken_millis: u32,
|
||||
) -> RevlogEntry {
|
||||
RevlogEntry {
|
||||
id: answered_at,
|
||||
cid,
|
||||
usn,
|
||||
button_chosen,
|
||||
interval: self.interval.as_revlog_interval(),
|
||||
last_interval: self.last_interval.as_revlog_interval(),
|
||||
ease_factor: (self.ease_factor * 1000.0).round() as u32,
|
||||
taken_millis,
|
||||
review_kind: self.review_kind,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
pub fn describe_next_states(&self, choices: NextCardStates) -> Result<Vec<String>> {
|
||||
40
rslib/src/scheduler/answering/preview.rs
Normal file
40
rslib/src/scheduler/answering/preview.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use crate::{
|
||||
card::CardQueue,
|
||||
prelude::*,
|
||||
scheduler::states::{CardState, IntervalKind, PreviewState},
|
||||
};
|
||||
|
||||
use super::{CardStateUpdater, RevlogEntryPartial};
|
||||
|
||||
impl CardStateUpdater {
|
||||
// fixme: check learning card moved into preview
|
||||
// restores correctly in both learn and day-learn case
|
||||
pub(super) fn apply_preview_state(
|
||||
&mut self,
|
||||
current: CardState,
|
||||
next: PreviewState,
|
||||
) -> Result<Option<RevlogEntryPartial>> {
|
||||
self.ensure_filtered()?;
|
||||
self.card.queue = CardQueue::PreviewRepeat;
|
||||
|
||||
let interval = next.interval_kind();
|
||||
match interval {
|
||||
IntervalKind::InSecs(secs) => {
|
||||
self.card.due = self.now.0 as i32 + secs as i32;
|
||||
}
|
||||
IntervalKind::InDays(_days) => {
|
||||
// unsupported
|
||||
}
|
||||
}
|
||||
|
||||
Ok(RevlogEntryPartial::maybe_new(
|
||||
current,
|
||||
next.into(),
|
||||
0.0,
|
||||
self.secs_until_rollover(),
|
||||
))
|
||||
}
|
||||
}
|
||||
58
rslib/src/scheduler/answering/revlog.rs
Normal file
58
rslib/src/scheduler/answering/revlog.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use crate::{
|
||||
prelude::*,
|
||||
revlog::{RevlogEntry, RevlogReviewKind},
|
||||
scheduler::states::{CardState, IntervalKind},
|
||||
};
|
||||
|
||||
pub struct RevlogEntryPartial {
|
||||
interval: IntervalKind,
|
||||
last_interval: IntervalKind,
|
||||
ease_factor: f32,
|
||||
review_kind: RevlogReviewKind,
|
||||
}
|
||||
|
||||
impl RevlogEntryPartial {
|
||||
/// Returns None in the Preview case, since preview cards do not currently log.
|
||||
pub(super) fn maybe_new(
|
||||
current: CardState,
|
||||
next: CardState,
|
||||
ease_factor: f32,
|
||||
secs_until_rollover: u32,
|
||||
) -> Option<Self> {
|
||||
current.revlog_kind().map(|review_kind| {
|
||||
let next_interval = next.interval_kind().maybe_as_days(secs_until_rollover);
|
||||
let current_interval = current.interval_kind().maybe_as_days(secs_until_rollover);
|
||||
|
||||
RevlogEntryPartial {
|
||||
interval: next_interval,
|
||||
last_interval: current_interval,
|
||||
ease_factor,
|
||||
review_kind,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn into_revlog_entry(
|
||||
self,
|
||||
usn: Usn,
|
||||
cid: CardID,
|
||||
button_chosen: u8,
|
||||
answered_at: TimestampMillis,
|
||||
taken_millis: u32,
|
||||
) -> RevlogEntry {
|
||||
RevlogEntry {
|
||||
id: answered_at,
|
||||
cid,
|
||||
usn,
|
||||
button_chosen,
|
||||
interval: self.interval.as_revlog_interval(),
|
||||
last_interval: self.last_interval.as_revlog_interval(),
|
||||
ease_factor: (self.ease_factor * 1000.0).round() as u32,
|
||||
taken_millis,
|
||||
review_kind: self.review_kind,
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue