mirror of
https://github.com/ankitects/anki.git
synced 2025-12-10 21:36:55 -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
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
mod current_state;
|
||||||
|
mod preview;
|
||||||
|
mod revlog;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
backend_proto,
|
backend_proto,
|
||||||
card::{CardQueue, CardType},
|
card::{CardQueue, CardType},
|
||||||
deckconf::{DeckConf, LeechAction},
|
deckconf::{DeckConf, LeechAction},
|
||||||
decks::{Deck, DeckKind},
|
decks::Deck,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
revlog::{RevlogEntry, RevlogReviewKind},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use revlog::RevlogEntryPartial;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
cutoff::SchedTimingToday,
|
cutoff::SchedTimingToday,
|
||||||
states::{
|
states::{
|
||||||
steps::LearningSteps, CardState, FilteredState, IntervalKind, LearnState, NewState,
|
CardState, FilteredState, IntervalKind, LearnState, NewState, NextCardStates, NormalState,
|
||||||
NextCardStates, NormalState, PreviewState, RelearnState, ReschedulingFilterState,
|
RelearnState, ReschedulingFilterState, ReviewState,
|
||||||
ReviewState, StateContext,
|
|
||||||
},
|
},
|
||||||
timespan::answer_button_time_collapsible,
|
timespan::answer_button_time_collapsible,
|
||||||
};
|
};
|
||||||
|
|
@ -51,132 +55,14 @@ struct CardStateUpdater {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
fn secs_until_rollover(&self) -> u32 {
|
||||||
(self.timing.next_day_at - self.now.0).max(0) as 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 {
|
fn into_card(self) -> 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(
|
fn apply_study_state(
|
||||||
&mut self,
|
&mut self,
|
||||||
current: CardState,
|
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(
|
fn apply_rescheduling_filter_state(
|
||||||
&mut self,
|
&mut self,
|
||||||
current: CardState,
|
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 {
|
impl Collection {
|
||||||
pub fn describe_next_states(&self, choices: NextCardStates) -> Result<Vec<String>> {
|
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