split rescheduling_filter, and more tidyups

This commit is contained in:
Damien Elmes 2021-02-23 15:45:50 +10:00
parent b887057448
commit 02e23e1063
5 changed files with 131 additions and 96 deletions

View file

@ -5,37 +5,14 @@ use crate::{
card::CardType, card::CardType,
decks::DeckKind, decks::DeckKind,
scheduler::states::{ scheduler::states::{
steps::LearningSteps, CardState, LearnState, NewState, NormalState, PreviewState, CardState, LearnState, NewState, NormalState, PreviewState, RelearnState,
RelearnState, ReschedulingFilterState, ReviewState, StateContext, ReschedulingFilterState, ReviewState,
}, },
}; };
use super::CardStateUpdater; use super::CardStateUpdater;
impl 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 { pub(crate) fn current_card_state(&self) -> CardState {
let due = match &self.deck.kind { let due = match &self.deck.kind {
DeckKind::Normal(_) => { DeckKind::Normal(_) => {
@ -122,12 +99,4 @@ impl CardStateUpdater {
.into(), .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,10 +1,11 @@
// 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 current;
mod learn; mod learning;
mod preview; mod preview;
mod relearn; mod relearning;
mod rescheduling_filter;
mod review; mod review;
mod revlog; mod revlog;
@ -20,7 +21,9 @@ use revlog::RevlogEntryPartial;
use super::{ use super::{
cutoff::SchedTimingToday, cutoff::SchedTimingToday,
states::{CardState, FilteredState, NextCardStates, NormalState, ReschedulingFilterState}, states::{
steps::LearningSteps, CardState, FilteredState, NextCardStates, NormalState, StateContext,
},
timespan::answer_button_time_collapsible, timespan::answer_button_time_collapsible,
}; };
@ -42,9 +45,11 @@ pub struct CardAnswer {
} }
// fixme: 4 buttons for previewing // fixme: 4 buttons for previewing
// fixme: log previewing // fixme: log preview review
// fixme: undo // fixme: undo
/// Holds the information required to determine a given card's
/// current state, and to apply a state change to it.
struct CardStateUpdater { struct CardStateUpdater {
card: Card, card: Card,
deck: Deck, deck: Deck,
@ -55,6 +60,40 @@ struct CardStateUpdater {
} }
impl CardStateUpdater { impl CardStateUpdater {
/// Returns information required when transitioning from one card state to
/// another with `next_states()`. This separate structure decouples the
/// state handling code from the rest of the Anki codebase.
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
},
}
}
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 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
} }
@ -96,15 +135,6 @@ impl CardStateUpdater {
Ok(revlog) Ok(revlog)
} }
fn apply_rescheduling_filter_state(
&mut self,
current: CardState,
next: ReschedulingFilterState,
) -> Result<Option<RevlogEntryPartial>> {
self.ensure_filtered()?;
self.apply_study_state(current, next.original_state.into())
}
fn ensure_filtered(&self) -> Result<()> { fn ensure_filtered(&self) -> Result<()> {
if self.card.original_deck_id.0 == 0 { if self.card.original_deck_id.0 == 0 {
Err(AnkiError::invalid_input( Err(AnkiError::invalid_input(
@ -116,8 +146,6 @@ impl CardStateUpdater {
} }
} }
impl Card {}
impl Rating { impl Rating {
fn as_number(self) -> u8 { fn as_number(self) -> u8 {
match self { match self {
@ -130,6 +158,16 @@ impl Rating {
} }
impl Collection { impl Collection {
/// Return the next states that will be applied for each answer button.
pub fn get_next_card_states(&mut self, cid: CardID) -> Result<NextCardStates> {
let card = self.storage.get_card(cid)?.ok_or(AnkiError::NotFound)?;
let ctx = self.card_state_updater(card)?;
let current = ctx.current_card_state();
let state_ctx = ctx.state_context();
Ok(current.next_states(&state_ctx))
}
/// Describe the next intervals, to display on the answer buttons.
pub fn describe_next_states(&self, choices: NextCardStates) -> Result<Vec<String>> { pub fn describe_next_states(&self, choices: NextCardStates) -> Result<Vec<String>> {
let collapse_time = self.learn_ahead_secs(); let collapse_time = self.learn_ahead_secs();
let now = TimestampSecs::now(); let now = TimestampSecs::now();
@ -175,6 +213,7 @@ impl Collection {
]) ])
} }
/// Answer card, writing its new state to the database.
pub fn answer_card(&mut self, answer: &CardAnswer) -> Result<()> { pub fn answer_card(&mut self, answer: &CardAnswer) -> Result<()> {
self.transact(None, |col| col.answer_card_inner(answer)) self.transact(None, |col| col.answer_card_inner(answer))
} }
@ -186,53 +225,22 @@ impl Collection {
.ok_or(AnkiError::NotFound)?; .ok_or(AnkiError::NotFound)?;
let original = card.clone(); let original = card.clone();
let usn = self.usn()?; let usn = self.usn()?;
let mut answer_context = self.answer_context(card)?;
let current_state = answer_context.current_card_state(); let mut updater = self.card_state_updater(card)?;
let current_state = updater.current_card_state();
if current_state != answer.current_state { if current_state != answer.current_state {
// fixme: unique error
return Err(AnkiError::invalid_input(format!( return Err(AnkiError::invalid_input(format!(
"card was modified: {:#?} {:#?}", "card was modified: {:#?} {:#?}",
current_state, answer.current_state, current_state, answer.current_state,
))); )));
} }
if let Some(revlog_partial) = if let Some(revlog_partial) = updater.apply_study_state(current_state, answer.new_state)? {
answer_context.apply_study_state(current_state, answer.new_state)? self.add_partial_revlog(revlog_partial, usn, &answer)?;
{
let button_chosen = answer.rating.as_number();
let revlog = revlog_partial.into_revlog_entry(
usn,
answer.card_id,
button_chosen,
answer.answered_at,
answer.milliseconds_taken,
);
self.storage.add_revlog_entry(&revlog)?;
} }
self.update_deck_stats_from_answer(usn, &answer, &updater)?;
// fixme: we're reusing code used by python, which means re-feteching the target deck let mut card = updater.into_card();
// - might want to avoid that in the future
self.update_deck_stats(
answer_context.timing.days_elapsed,
usn,
backend_proto::UpdateStatsIn {
deck_id: answer_context.deck.id.0,
new_delta: if matches!(current_state, CardState::Normal(NormalState::New(_))) {
1
} else {
0
},
review_delta: if matches!(current_state, CardState::Normal(NormalState::Review(_)))
{
1
} else {
0
},
millisecond_delta: answer.milliseconds_taken as i32,
},
)?;
let mut card = answer_context.into_card();
self.update_card(&mut card, &original, usn)?; self.update_card(&mut card, &original, usn)?;
if answer.new_state.leeched() { if answer.new_state.leeched() {
self.add_leech_tag(card.note_id)?; self.add_leech_tag(card.note_id)?;
@ -241,7 +249,53 @@ impl Collection {
Ok(()) Ok(())
} }
fn answer_context(&mut self, card: Card) -> Result<CardStateUpdater> { fn add_partial_revlog(
&self,
partial: RevlogEntryPartial,
usn: Usn,
answer: &CardAnswer,
) -> Result<()> {
let revlog = partial.into_revlog_entry(
usn,
answer.card_id,
answer.rating.as_number(),
answer.answered_at,
answer.milliseconds_taken,
);
self.storage.add_revlog_entry(&revlog)
}
fn update_deck_stats_from_answer(
&mut self,
usn: Usn,
answer: &CardAnswer,
updater: &CardStateUpdater,
) -> Result<()> {
self.update_deck_stats(
updater.timing.days_elapsed,
usn,
backend_proto::UpdateStatsIn {
deck_id: updater.deck.id.0,
new_delta: if matches!(answer.current_state, CardState::Normal(NormalState::New(_)))
{
1
} else {
0
},
review_delta: if matches!(
answer.current_state,
CardState::Normal(NormalState::Review(_))
) {
1
} else {
0
},
millisecond_delta: answer.milliseconds_taken as i32,
},
)
}
fn card_state_updater(&mut self, card: Card) -> Result<CardStateUpdater> {
let timing = self.timing_today()?; let timing = self.timing_today()?;
let deck = self let deck = self
.storage .storage
@ -276,14 +330,6 @@ impl Collection {
Ok(self.storage.get_deck_config(config_id)?.unwrap_or_default()) Ok(self.storage.get_deck_config(config_id)?.unwrap_or_default())
} }
pub fn get_next_card_states(&mut self, cid: CardID) -> Result<NextCardStates> {
let card = self.storage.get_card(cid)?.ok_or(AnkiError::NotFound)?;
let ctx = self.answer_context(card)?;
let current = ctx.current_card_state();
let state_ctx = ctx.state_context();
Ok(current.next_states(&state_ctx))
}
fn add_leech_tag(&mut self, nid: NoteID) -> Result<()> { fn add_leech_tag(&mut self, nid: NoteID) -> Result<()> {
self.update_note_tags(nid, |tags| tags.push("leech".into())) self.update_note_tags(nid, |tags| tags.push("leech".into()))
} }

View file

@ -0,0 +1,20 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::{
prelude::*,
scheduler::states::{CardState, ReschedulingFilterState},
};
use super::{CardStateUpdater, RevlogEntryPartial};
impl CardStateUpdater {
pub(super) fn apply_rescheduling_filter_state(
&mut self,
current: CardState,
next: ReschedulingFilterState,
) -> Result<Option<RevlogEntryPartial>> {
self.ensure_filtered()?;
self.apply_study_state(current, next.original_state.into())
}
}