split state fetching, revlog and preview code out

This commit is contained in:
Damien Elmes 2021-02-23 15:05:28 +10:00
parent 6a44269280
commit e74210717a
4 changed files with 240 additions and 200 deletions

View 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)
}
}

View file

@ -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>> {

View 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(),
))
}
}

View 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,
}
}
}