diff --git a/rslib/src/scheduler/answering/current_state.rs b/rslib/src/scheduler/answering/current_state.rs new file mode 100644 index 000000000..2ac4cda07 --- /dev/null +++ b/rslib/src/scheduler/answering/current_state.rs @@ -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) + } +} diff --git a/rslib/src/scheduler/answering.rs b/rslib/src/scheduler/answering/mod.rs similarity index 63% rename from rslib/src/scheduler/answering.rs rename to rslib/src/scheduler/answering/mod.rs index 1eb56cc5f..7014904c1 100644 --- a/rslib/src/scheduler/answering.rs +++ b/rslib/src/scheduler/answering/mod.rs @@ -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> { - 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 { - 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> { diff --git a/rslib/src/scheduler/answering/preview.rs b/rslib/src/scheduler/answering/preview.rs new file mode 100644 index 000000000..074990213 --- /dev/null +++ b/rslib/src/scheduler/answering/preview.rs @@ -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> { + 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(), + )) + } +} diff --git a/rslib/src/scheduler/answering/revlog.rs b/rslib/src/scheduler/answering/revlog.rs new file mode 100644 index 000000000..b5240cac2 --- /dev/null +++ b/rslib/src/scheduler/answering/revlog.rs @@ -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 { + 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, + } + } +}