mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
FSRS - Ignore revlogs before date while optimizing (#2922)
* Added: Date input button * Added: ignoreDate to config * Added: Backend * Optimize function passes value * Fix: Spelling * Moved: filter logic from revlog_for_srs to update_memory_state * fmt * Copyright header * ./check * Fix: Test * Renamed: Ignore_date -> Ignore_before_date * Neaten parameters * evaluate weights * ./check * Optimize all presets * Added: Label localizations * Removed globe label * Added: Tooltip * Changed error type * fmt * Moved filter to own function * missing function call replacement * Fix: Typo * Apply suggestions from code review Co-authored-by: Damien Elmes <dae@users.noreply.github.com> * timestamp * 1000 -> timestamp_millis * ignoreBefore -> ignore_before * clarified ignore_before variables * i64 -> TimestampMillis * Un-traitified remove_revlogs_before * Added: ms == 0 guard * Added: Ignore_before affects scheduling * Moved filter to fsrs_items_for_training * removed filter from revlog_for_srs * Tuple -> UpdateMemoryStateEntry * Removed unused function * Removed superfluous _ms from variables * cid -> id * Different ignore method * Added: Unit test * cid -> id * Test: Exact ms edge case * ./check * Fix: re-learns could be before ignore date in cards without learning steps * getignoreRevlogsBeforeMs -> getIgnoreRevlogsBeforeMs * Removed pub(crate) * Clarified unit test * last_learn_entry -> first_of_last_learn_entries * @user1823's method * IOS fix * ./check * Fix: width defined twice
This commit is contained in:
parent
9642a69b88
commit
8b18a08b3b
13 changed files with 276 additions and 43 deletions
|
@ -346,6 +346,7 @@ deck-config-compute-optimal-weights = Optimize FSRS parameters
|
|||
deck-config-compute-optimal-retention = Compute optimal retention
|
||||
deck-config-optimize-button = Optimize
|
||||
deck-config-compute-button = Compute
|
||||
deck-config-ignore-before = Ignore reviews before
|
||||
deck-config-optimize-all-tip = You can optimize all presets at once by using the dropdown button next to "Save".
|
||||
deck-config-evaluate-button = Evaluate
|
||||
deck-config-desired-retention = Desired retention
|
||||
|
@ -398,6 +399,9 @@ deck-config-reschedule-cards-warning =
|
|||
|
||||
Use this option sparingly, as it will add a review entry to each of your cards, and
|
||||
increase the size of your collection.
|
||||
deck-config-ignore-before-tooltip =
|
||||
If set, reviews before the provided date will be ignored when optimizing & evaluating FSRS parameters.
|
||||
This can be useful if you imported someone else's scheduling data, or have changed the way you use the answer buttons.
|
||||
deck-config-compute-optimal-weights-tooltip =
|
||||
Once you've done 1000+ reviews in Anki, you can use the Optimize button to analyze your review history,
|
||||
and automatically generate parameters that are optimal for your memory and the content you're studying.
|
||||
|
|
|
@ -154,6 +154,7 @@ message DeckConfig {
|
|||
|
||||
// for fsrs
|
||||
float desired_retention = 37;
|
||||
string ignore_revlogs_before_date = 46;
|
||||
// used for fsrs_reschedule in the past
|
||||
reserved 39;
|
||||
float sm2_retention = 40;
|
||||
|
|
|
@ -339,6 +339,7 @@ message ComputeFsrsWeightsRequest {
|
|||
/// The search used to gather cards for training
|
||||
string search = 1;
|
||||
repeated float current_weights = 2;
|
||||
int64 ignore_revlogs_before_ms = 3;
|
||||
}
|
||||
|
||||
message ComputeFsrsWeightsResponse {
|
||||
|
@ -400,6 +401,7 @@ message GetOptimalRetentionParametersResponse {
|
|||
message EvaluateWeightsRequest {
|
||||
repeated float weights = 1;
|
||||
string search = 2;
|
||||
int64 ignore_revlogs_before_ms = 3;
|
||||
}
|
||||
|
||||
message EvaluateWeightsResponse {
|
||||
|
|
|
@ -77,6 +77,7 @@ const DEFAULT_DECK_CONFIG_INNER: DeckConfigInner = DeckConfigInner {
|
|||
other: Vec::new(),
|
||||
sm2_retention: 0.9,
|
||||
weight_search: String::new(),
|
||||
ignore_revlogs_before_date: String::new(),
|
||||
};
|
||||
|
||||
impl Default for DeckConfig {
|
||||
|
|
|
@ -74,6 +74,8 @@ pub struct DeckConfSchema11 {
|
|||
#[serde(default)]
|
||||
desired_retention: f32,
|
||||
#[serde(default)]
|
||||
ignore_revlogs_before_date: String,
|
||||
#[serde(default)]
|
||||
stop_timer_on_answer: bool,
|
||||
#[serde(default)]
|
||||
seconds_to_show_question: f32,
|
||||
|
@ -294,6 +296,7 @@ impl Default for DeckConfSchema11 {
|
|||
desired_retention: 0.9,
|
||||
sm2_retention: 0.9,
|
||||
weight_search: "".to_string(),
|
||||
ignore_revlogs_before_date: "".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -368,6 +371,7 @@ impl From<DeckConfSchema11> for DeckConfig {
|
|||
bury_reviews: c.rev.bury,
|
||||
bury_interday_learning: c.bury_interday_learning,
|
||||
fsrs_weights: c.fsrs_weights,
|
||||
ignore_revlogs_before_date: c.ignore_revlogs_before_date,
|
||||
desired_retention: c.desired_retention,
|
||||
sm2_retention: c.sm2_retention,
|
||||
weight_search: c.weight_search,
|
||||
|
@ -477,6 +481,7 @@ impl From<DeckConfig> for DeckConfSchema11 {
|
|||
desired_retention: i.desired_retention,
|
||||
sm2_retention: i.sm2_retention,
|
||||
weight_search: i.weight_search,
|
||||
ignore_revlogs_before_date: i.ignore_revlogs_before_date,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -507,6 +512,7 @@ static RESERVED_DECKCONF_KEYS: Set<&'static str> = phf_set! {
|
|||
"waitForAudio",
|
||||
"sm2Retention",
|
||||
"weightSearch",
|
||||
"ignoreRevlogsBeforeDate",
|
||||
};
|
||||
|
||||
static RESERVED_DECKCONF_NEW_KEYS: Set<&'static str> = phf_set! {
|
||||
|
|
|
@ -17,7 +17,9 @@ use fsrs::DEFAULT_WEIGHTS;
|
|||
use crate::config::StringKey;
|
||||
use crate::decks::NormalDeck;
|
||||
use crate::prelude::*;
|
||||
use crate::scheduler::fsrs::memory_state::UpdateMemoryStateEntry;
|
||||
use crate::scheduler::fsrs::memory_state::UpdateMemoryStateRequest;
|
||||
use crate::scheduler::fsrs::weights::ignore_revlogs_before_ms_from_config;
|
||||
use crate::search::JoinSearches;
|
||||
use crate::search::SearchNode;
|
||||
use crate::storage::comma_separated_ids;
|
||||
|
@ -238,29 +240,32 @@ impl Collection {
|
|||
}
|
||||
|
||||
if !decks_needing_memory_recompute.is_empty() {
|
||||
let input: Vec<(Option<UpdateMemoryStateRequest>, SearchNode)> =
|
||||
decks_needing_memory_recompute
|
||||
.into_iter()
|
||||
.map(|(conf_id, search)| {
|
||||
let weights = configs_after_update.get(&conf_id).and_then(|c| {
|
||||
if req.fsrs {
|
||||
Some(UpdateMemoryStateRequest {
|
||||
weights: c.inner.fsrs_weights.clone(),
|
||||
desired_retention: c.inner.desired_retention,
|
||||
max_interval: c.inner.maximum_review_interval,
|
||||
reschedule: req.fsrs_reschedule,
|
||||
sm2_retention: c.inner.sm2_retention,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
Ok((
|
||||
weights,
|
||||
SearchNode::DeckIdsWithoutChildren(comma_separated_ids(&search)),
|
||||
))
|
||||
let input: Vec<UpdateMemoryStateEntry> = decks_needing_memory_recompute
|
||||
.into_iter()
|
||||
.map(|(conf_id, search)| {
|
||||
let config = configs_after_update.get(&conf_id);
|
||||
let weights = config.and_then(|c| {
|
||||
if req.fsrs {
|
||||
Some(UpdateMemoryStateRequest {
|
||||
weights: c.inner.fsrs_weights.clone(),
|
||||
desired_retention: c.inner.desired_retention,
|
||||
max_interval: c.inner.maximum_review_interval,
|
||||
reschedule: req.fsrs_reschedule,
|
||||
sm2_retention: c.inner.sm2_retention,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
Ok(UpdateMemoryStateEntry {
|
||||
req: weights,
|
||||
search: SearchNode::DeckIdsWithoutChildren(comma_separated_ids(&search)),
|
||||
ignore_before: config
|
||||
.map(ignore_revlogs_before_ms_from_config)
|
||||
.unwrap_or(Ok(0.into()))?,
|
||||
})
|
||||
.collect::<Result<_>>()?;
|
||||
})
|
||||
.collect::<Result<_>>()?;
|
||||
self.update_memory_state(input)?;
|
||||
}
|
||||
|
||||
|
@ -332,8 +337,10 @@ impl Collection {
|
|||
} else {
|
||||
config.inner.weight_search.clone()
|
||||
};
|
||||
let ignore_revlogs_before_ms = ignore_revlogs_before_ms_from_config(config)?;
|
||||
match self.compute_weights(
|
||||
&search,
|
||||
ignore_revlogs_before_ms,
|
||||
idx as u32 + 1,
|
||||
config_len,
|
||||
&config.inner.fsrs_weights,
|
||||
|
|
|
@ -14,6 +14,7 @@ use rand::prelude::*;
|
|||
use rand::rngs::StdRng;
|
||||
use revlog::RevlogEntryPartial;
|
||||
|
||||
use super::fsrs::weights::ignore_revlogs_before_ms_from_config;
|
||||
use super::queue::BuryMode;
|
||||
use super::states::steps::LearningSteps;
|
||||
use super::states::CardState;
|
||||
|
@ -382,6 +383,7 @@ impl Collection {
|
|||
revlog,
|
||||
timing.next_day_at,
|
||||
config.inner.sm2_retention,
|
||||
ignore_revlogs_before_ms_from_config(&config)?,
|
||||
)?;
|
||||
card.set_memory_state(&fsrs, item, config.inner.sm2_retention)?;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ use fsrs::MemoryState;
|
|||
use fsrs::FSRS;
|
||||
use itertools::Itertools;
|
||||
|
||||
use super::weights::ignore_revlogs_before_ms_from_config;
|
||||
use crate::card::CardType;
|
||||
use crate::prelude::*;
|
||||
use crate::revlog::RevlogEntry;
|
||||
|
@ -35,6 +36,12 @@ pub(crate) struct UpdateMemoryStateRequest {
|
|||
pub reschedule: bool,
|
||||
}
|
||||
|
||||
pub(crate) struct UpdateMemoryStateEntry {
|
||||
pub req: Option<UpdateMemoryStateRequest>,
|
||||
pub search: SearchNode,
|
||||
pub ignore_before: TimestampMillis,
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
/// For each provided set of weights, locate cards with the provided search,
|
||||
/// and update their memory state.
|
||||
|
@ -43,11 +50,16 @@ impl Collection {
|
|||
/// memory state should be removed.
|
||||
pub(crate) fn update_memory_state(
|
||||
&mut self,
|
||||
entries: Vec<(Option<UpdateMemoryStateRequest>, SearchNode)>,
|
||||
entries: Vec<UpdateMemoryStateEntry>,
|
||||
) -> Result<()> {
|
||||
let timing = self.timing_today()?;
|
||||
let usn = self.usn()?;
|
||||
for (req, search) in entries {
|
||||
for UpdateMemoryStateEntry {
|
||||
req,
|
||||
search,
|
||||
ignore_before,
|
||||
} in entries
|
||||
{
|
||||
let search =
|
||||
SearchBuilder::all([search.into(), SearchNode::State(StateKind::New).negated()]);
|
||||
let revlog = self.revlog_for_srs(search)?;
|
||||
|
@ -64,6 +76,7 @@ impl Collection {
|
|||
revlog,
|
||||
timing.next_day_at,
|
||||
sm2_retention.unwrap_or(0.9),
|
||||
ignore_before,
|
||||
)?;
|
||||
let desired_retention = req.as_ref().map(|w| w.desired_retention);
|
||||
let mut progress = self.new_progress_handler::<ComputeMemoryProgress>();
|
||||
|
@ -148,6 +161,7 @@ impl Collection {
|
|||
revlog,
|
||||
self.timing_today()?.next_day_at,
|
||||
sm2_retention,
|
||||
ignore_revlogs_before_ms_from_config(&config)?,
|
||||
)?;
|
||||
card.set_memory_state(&fsrs, item, sm2_retention)?;
|
||||
Ok(ComputeMemoryStateResponse {
|
||||
|
@ -196,6 +210,7 @@ pub(crate) fn fsrs_items_for_memory_state(
|
|||
revlogs: Vec<RevlogEntry>,
|
||||
next_day_at: TimestampSecs,
|
||||
sm2_retention: f32,
|
||||
ignore_revlogs_before: TimestampMillis,
|
||||
) -> Result<Vec<(CardId, Option<FsrsItemWithStartingState>)>> {
|
||||
revlogs
|
||||
.into_iter()
|
||||
|
@ -204,7 +219,13 @@ pub(crate) fn fsrs_items_for_memory_state(
|
|||
.map(|(card_id, group)| {
|
||||
Ok((
|
||||
card_id,
|
||||
single_card_revlog_to_item(fsrs, group.collect(), next_day_at, sm2_retention)?,
|
||||
single_card_revlog_to_item(
|
||||
fsrs,
|
||||
group.collect(),
|
||||
next_day_at,
|
||||
sm2_retention,
|
||||
ignore_revlogs_before,
|
||||
)?,
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
|
@ -257,6 +278,7 @@ pub(crate) fn single_card_revlog_to_item(
|
|||
entries: Vec<RevlogEntry>,
|
||||
next_day_at: TimestampSecs,
|
||||
sm2_retention: f32,
|
||||
ignore_revlogs_before: TimestampMillis,
|
||||
) -> Result<Option<FsrsItemWithStartingState>> {
|
||||
struct FirstReview {
|
||||
interval: f32,
|
||||
|
@ -275,7 +297,7 @@ pub(crate) fn single_card_revlog_to_item(
|
|||
/ 1000.0,
|
||||
});
|
||||
if let Some((mut items, revlogs_complete)) =
|
||||
single_card_revlog_to_items(entries, next_day_at, false)
|
||||
single_card_revlog_to_items(entries, next_day_at, false, ignore_revlogs_before)
|
||||
{
|
||||
let mut item = items.pop().unwrap();
|
||||
if revlogs_complete {
|
||||
|
@ -345,6 +367,7 @@ mod tests {
|
|||
],
|
||||
TimestampSecs::now(),
|
||||
0.9,
|
||||
0.into(),
|
||||
)?
|
||||
.unwrap();
|
||||
assert_int_eq(
|
||||
|
@ -377,6 +400,7 @@ mod tests {
|
|||
}],
|
||||
TimestampSecs::now(),
|
||||
0.9,
|
||||
0.into(),
|
||||
)?;
|
||||
assert!(item.is_none());
|
||||
card.interval = 123;
|
||||
|
|
|
@ -9,6 +9,8 @@ use anki_io::write_file;
|
|||
use anki_proto::scheduler::ComputeFsrsWeightsResponse;
|
||||
use anki_proto::stats::revlog_entry;
|
||||
use anki_proto::stats::RevlogEntries;
|
||||
use chrono::NaiveDate;
|
||||
use chrono::NaiveTime;
|
||||
use fsrs::CombinedProgressState;
|
||||
use fsrs::FSRSItem;
|
||||
use fsrs::FSRSReview;
|
||||
|
@ -26,6 +28,23 @@ use crate::search::SortMode;
|
|||
|
||||
pub(crate) type Weights = Vec<f32>;
|
||||
|
||||
fn ignore_revlogs_before_date_to_ms(
|
||||
ignore_revlogs_before_date: &String,
|
||||
) -> Result<TimestampMillis> {
|
||||
Ok(match ignore_revlogs_before_date {
|
||||
s if s.is_empty() => 0,
|
||||
s => NaiveDate::parse_from_str(s.as_str(), "%Y-%m-%d")
|
||||
.or_else(|err| invalid_input!(err, "Error parsing date: {s}"))?
|
||||
.and_time(NaiveTime::from_hms_milli_opt(0, 0, 0, 0).unwrap())
|
||||
.timestamp_millis(),
|
||||
}
|
||||
.into())
|
||||
}
|
||||
|
||||
pub(crate) fn ignore_revlogs_before_ms_from_config(config: &DeckConfig) -> Result<TimestampMillis> {
|
||||
ignore_revlogs_before_date_to_ms(&config.inner.ignore_revlogs_before_date)
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
/// Note this does not return an error if there are less than 400 items -
|
||||
/// the caller should instead check the fsrs_items count in the return
|
||||
|
@ -33,6 +52,7 @@ impl Collection {
|
|||
pub fn compute_weights(
|
||||
&mut self,
|
||||
search: &str,
|
||||
ignore_revlogs_before: TimestampMillis,
|
||||
current_preset: u32,
|
||||
total_presets: u32,
|
||||
current_weights: &Weights,
|
||||
|
@ -45,7 +65,8 @@ impl Collection {
|
|||
count: revlogs.len(),
|
||||
});
|
||||
}
|
||||
let items = fsrs_items_for_training(revlogs.clone(), timing.next_day_at);
|
||||
let items =
|
||||
fsrs_items_for_training(revlogs.clone(), timing.next_day_at, ignore_revlogs_before);
|
||||
let fsrs_items = items.len() as u32;
|
||||
anki_progress.update(false, |p| {
|
||||
p.fsrs_items = fsrs_items;
|
||||
|
@ -122,11 +143,16 @@ impl Collection {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn evaluate_weights(&mut self, weights: &Weights, search: &str) -> Result<ModelEvaluation> {
|
||||
pub fn evaluate_weights(
|
||||
&mut self,
|
||||
weights: &Weights,
|
||||
search: &str,
|
||||
ignore_revlogs_before: TimestampMillis,
|
||||
) -> Result<ModelEvaluation> {
|
||||
let timing = self.timing_today()?;
|
||||
let mut anki_progress = self.new_progress_handler::<ComputeWeightsProgress>();
|
||||
let guard = self.search_cards_into_table(search, SortMode::NoOrder)?;
|
||||
let revlogs = guard
|
||||
let revlogs: Vec<RevlogEntry> = guard
|
||||
.col
|
||||
.storage
|
||||
.get_revlog_entries_for_searched_cards_in_card_order()?;
|
||||
|
@ -135,8 +161,8 @@ impl Collection {
|
|||
count: revlogs.len(),
|
||||
});
|
||||
}
|
||||
let items = fsrs_items_for_training(revlogs, timing.next_day_at);
|
||||
anki_progress.state.fsrs_items = items.len() as u32;
|
||||
anki_progress.state.fsrs_items = revlogs.len() as u32;
|
||||
let items = fsrs_items_for_training(revlogs, timing.next_day_at, ignore_revlogs_before);
|
||||
let fsrs = FSRS::new(Some(weights))?;
|
||||
Ok(fsrs.evaluate(items, |ip| {
|
||||
anki_progress
|
||||
|
@ -161,13 +187,17 @@ pub struct ComputeWeightsProgress {
|
|||
}
|
||||
|
||||
/// Convert a series of revlog entries sorted by card id into FSRS items.
|
||||
fn fsrs_items_for_training(revlogs: Vec<RevlogEntry>, next_day_at: TimestampSecs) -> Vec<FSRSItem> {
|
||||
fn fsrs_items_for_training(
|
||||
revlogs: Vec<RevlogEntry>,
|
||||
next_day_at: TimestampSecs,
|
||||
review_revlogs_before: TimestampMillis,
|
||||
) -> Vec<FSRSItem> {
|
||||
let mut revlogs = revlogs
|
||||
.into_iter()
|
||||
.group_by(|r| r.cid)
|
||||
.into_iter()
|
||||
.filter_map(|(_cid, entries)| {
|
||||
single_card_revlog_to_items(entries.collect(), next_day_at, true)
|
||||
single_card_revlog_to_items(entries.collect(), next_day_at, true, review_revlogs_before)
|
||||
})
|
||||
.flat_map(|i| i.0)
|
||||
.collect_vec();
|
||||
|
@ -189,24 +219,25 @@ pub(crate) fn single_card_revlog_to_items(
|
|||
mut entries: Vec<RevlogEntry>,
|
||||
next_day_at: TimestampSecs,
|
||||
training: bool,
|
||||
ignore_revlogs_before: TimestampMillis,
|
||||
) -> Option<(Vec<FSRSItem>, bool)> {
|
||||
let mut last_learn_entry = None;
|
||||
let mut first_of_last_learn_entries = None;
|
||||
let mut revlogs_complete = false;
|
||||
for (index, entry) in entries.iter().enumerate().rev() {
|
||||
if matches!(
|
||||
(entry.review_kind, entry.button_chosen),
|
||||
(RevlogReviewKind::Learning, 1..=4)
|
||||
) {
|
||||
last_learn_entry = Some(index);
|
||||
first_of_last_learn_entries = Some(index);
|
||||
revlogs_complete = true;
|
||||
} else if last_learn_entry.is_some() {
|
||||
} else if first_of_last_learn_entries.is_some() {
|
||||
break;
|
||||
// if we find the `Forget` entry before the `Learn` entry, we should
|
||||
// ignore all the entries
|
||||
} else if matches!(
|
||||
(entry.review_kind, entry.ease_factor),
|
||||
(RevlogReviewKind::Manual, 0)
|
||||
) && last_learn_entry.is_none()
|
||||
) && first_of_last_learn_entries.is_none()
|
||||
{
|
||||
revlogs_complete = false;
|
||||
break;
|
||||
|
@ -221,12 +252,40 @@ pub(crate) fn single_card_revlog_to_items(
|
|||
})
|
||||
);
|
||||
}
|
||||
if training {
|
||||
// While training ignore the entire card if the first learning step of the last
|
||||
// group of learning steps is before the ignore_revlogs_before date
|
||||
if let Some(idx) = first_of_last_learn_entries {
|
||||
if entries[idx].id.0 < ignore_revlogs_before.0 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// While reviewing if the first learning step is before the ignore date,
|
||||
// ignore every review before and including the last learning step
|
||||
if let Some(idx) = first_of_last_learn_entries {
|
||||
if entries[idx].id.0 < ignore_revlogs_before.0 && idx < entries.len() - 1 {
|
||||
let last_learn_entry = entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.find(|(_idx, e)| e.review_kind == RevlogReviewKind::Learning)
|
||||
.map(|(idx, _)| idx);
|
||||
|
||||
entries.drain(..(last_learn_entry? + 1));
|
||||
revlogs_complete = false;
|
||||
first_of_last_learn_entries = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
let first_relearn = entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_idx, e)| e.review_kind == RevlogReviewKind::Relearning)
|
||||
.find(|(_idx, e)| {
|
||||
e.id.0 > ignore_revlogs_before.0 && e.review_kind == RevlogReviewKind::Relearning
|
||||
})
|
||||
.map(|(idx, _)| idx);
|
||||
if let Some(idx) = last_learn_entry.or(first_relearn) {
|
||||
if let Some(idx) = first_of_last_learn_entries.or(first_relearn) {
|
||||
// start from the (re)learning step
|
||||
if idx > 0 {
|
||||
entries.drain(..idx);
|
||||
|
@ -315,10 +374,14 @@ pub(crate) mod tests {
|
|||
|
||||
const NEXT_DAY_AT: TimestampSecs = TimestampSecs(86400 * 100);
|
||||
|
||||
fn days_ago_ms(days_ago: i64) -> TimestampMillis {
|
||||
((NEXT_DAY_AT.0 - days_ago * 86400) * 1000).into()
|
||||
}
|
||||
|
||||
pub(crate) fn revlog(review_kind: RevlogReviewKind, days_ago: i64) -> RevlogEntry {
|
||||
RevlogEntry {
|
||||
review_kind,
|
||||
id: ((NEXT_DAY_AT.0 - days_ago * 86400) * 1000).into(),
|
||||
id: days_ago_ms(days_ago).into(),
|
||||
button_chosen: 3,
|
||||
..Default::default()
|
||||
}
|
||||
|
@ -328,8 +391,17 @@ pub(crate) mod tests {
|
|||
FSRSReview { rating: 3, delta_t }
|
||||
}
|
||||
|
||||
pub(crate) fn convert_ignore_before(
|
||||
revlog: &[RevlogEntry],
|
||||
training: bool,
|
||||
ignore_before: TimestampMillis,
|
||||
) -> Option<Vec<FSRSItem>> {
|
||||
single_card_revlog_to_items(revlog.to_vec(), NEXT_DAY_AT, training, ignore_before)
|
||||
.map(|i| i.0)
|
||||
}
|
||||
|
||||
pub(crate) fn convert(revlog: &[RevlogEntry], training: bool) -> Option<Vec<FSRSItem>> {
|
||||
single_card_revlog_to_items(revlog.to_vec(), NEXT_DAY_AT, training).map(|i| i.0)
|
||||
convert_ignore_before(revlog, training, 0.into())
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
|
@ -454,4 +526,53 @@ pub(crate) mod tests {
|
|||
fsrs_items!([review(0)])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_cards_before_ignore_before_date_when_training() {
|
||||
let revlogs = &[
|
||||
revlog(RevlogReviewKind::Learning, 10),
|
||||
revlog(RevlogReviewKind::Learning, 8),
|
||||
];
|
||||
// | = Ignore before
|
||||
// L = learning step
|
||||
// L L |
|
||||
assert_eq!(convert_ignore_before(revlogs, true, days_ago_ms(7)), None);
|
||||
// L | L
|
||||
assert_eq!(convert_ignore_before(revlogs, true, days_ago_ms(9)), None);
|
||||
// L (|L) (exact same millisecond)
|
||||
assert_eq!(
|
||||
convert_ignore_before(revlogs, true, days_ago_ms(10)),
|
||||
convert(revlogs, true)
|
||||
);
|
||||
// | L L
|
||||
assert_eq!(
|
||||
convert_ignore_before(revlogs, true, days_ago_ms(11)),
|
||||
convert(revlogs, true)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignore_before_date_between_learning_steps_when_reviewing() {
|
||||
let revlogs = &[
|
||||
revlog(RevlogReviewKind::Learning, 10),
|
||||
revlog(RevlogReviewKind::Learning, 8),
|
||||
revlog(RevlogReviewKind::Review, 2),
|
||||
];
|
||||
// L | L R
|
||||
assert_ne!(
|
||||
convert_ignore_before(revlogs, false, days_ago_ms(9)),
|
||||
convert(revlogs, false)
|
||||
);
|
||||
assert_eq!(
|
||||
convert_ignore_before(revlogs, false, days_ago_ms(9))
|
||||
.unwrap()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
// | L L R
|
||||
assert_eq!(
|
||||
convert_ignore_before(revlogs, false, days_ago_ms(11)),
|
||||
convert(revlogs, false)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -254,7 +254,13 @@ impl crate::services::SchedulerService for Collection {
|
|||
&mut self,
|
||||
input: scheduler::ComputeFsrsWeightsRequest,
|
||||
) -> Result<scheduler::ComputeFsrsWeightsResponse> {
|
||||
self.compute_weights(&input.search, 1, 1, &input.current_weights)
|
||||
self.compute_weights(
|
||||
&input.search,
|
||||
input.ignore_revlogs_before_ms.into(),
|
||||
1,
|
||||
1,
|
||||
&input.current_weights,
|
||||
)
|
||||
}
|
||||
|
||||
fn compute_optimal_retention(
|
||||
|
@ -270,7 +276,11 @@ impl crate::services::SchedulerService for Collection {
|
|||
&mut self,
|
||||
input: scheduler::EvaluateWeightsRequest,
|
||||
) -> Result<scheduler::EvaluateWeightsResponse> {
|
||||
let ret = self.evaluate_weights(&input.weights, &input.search)?;
|
||||
let ret = self.evaluate_weights(
|
||||
&input.weights,
|
||||
&input.search,
|
||||
input.ignore_revlogs_before_ms.into(),
|
||||
)?;
|
||||
Ok(scheduler::EvaluateWeightsResponse {
|
||||
log_loss: ret.log_loss,
|
||||
rmse_bins: ret.rmse_bins,
|
||||
|
|
|
@ -62,6 +62,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
help: tr.deckConfigRescheduleCardsOnChangeTooltip(),
|
||||
sched: HelpItemScheduler.FSRS,
|
||||
},
|
||||
ignoreRevlogsBeforeMs: {
|
||||
title: tr.deckConfigIgnoreBefore(),
|
||||
help: tr.deckConfigIgnoreBeforeTooltip(),
|
||||
sched: HelpItemScheduler.FSRS,
|
||||
},
|
||||
computeOptimalWeights: {
|
||||
title: tr.deckConfigComputeOptimalWeights(),
|
||||
help: tr.deckConfigComputeOptimalWeightsTooltip(),
|
||||
|
|
34
ts/deck-options/DateInput.svelte
Normal file
34
ts/deck-options/DateInput.svelte
Normal file
|
@ -0,0 +1,34 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script>
|
||||
import Col from "../components/Col.svelte";
|
||||
import ConfigInput from "../components/ConfigInput.svelte";
|
||||
import Row from "../components/Row.svelte";
|
||||
|
||||
export let date;
|
||||
$: date = date ? date : "1970-01-01";
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<ConfigInput>
|
||||
<Row --cols={2}>
|
||||
<Col>
|
||||
<slot />
|
||||
</Col>
|
||||
<Col>
|
||||
<input bind:value={date} type="date" />
|
||||
</Col>
|
||||
</Row>
|
||||
</ConfigInput>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
input {
|
||||
width: 100%;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: 1.5em;
|
||||
}
|
||||
</style>
|
|
@ -19,6 +19,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import SwitchRow from "components/SwitchRow.svelte";
|
||||
|
||||
import SettingTitle from "../components/SettingTitle.svelte";
|
||||
import DateInput from "./DateInput.svelte";
|
||||
import GlobalLabel from "./GlobalLabel.svelte";
|
||||
import type { DeckOptionsState } from "./lib";
|
||||
import SpinBoxFloatRow from "./SpinBoxFloatRow.svelte";
|
||||
|
@ -86,6 +87,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
}
|
||||
|
||||
function getIgnoreRevlogsBeforeMs() {
|
||||
return BigInt(
|
||||
$config.ignoreRevlogsBeforeDate
|
||||
? new Date($config.ignoreRevlogsBeforeDate).getTime()
|
||||
: 0,
|
||||
);
|
||||
}
|
||||
|
||||
async function computeWeights(): Promise<void> {
|
||||
if (computingWeights) {
|
||||
await setWantsAbort({});
|
||||
|
@ -104,6 +113,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
search: $config.weightSearch
|
||||
? $config.weightSearch
|
||||
: defaultWeightSearch,
|
||||
ignoreRevlogsBeforeMs: getIgnoreRevlogsBeforeMs(),
|
||||
currentWeights: $config.fsrsWeights,
|
||||
});
|
||||
if (computeWeightsProgress) {
|
||||
|
@ -148,6 +158,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
const resp = await evaluateWeights({
|
||||
weights: $config.fsrsWeights,
|
||||
search,
|
||||
ignoreRevlogsBeforeMs: getIgnoreRevlogsBeforeMs(),
|
||||
});
|
||||
if (computeWeightsProgress) {
|
||||
computeWeightsProgress.current = computeWeightsProgress.total;
|
||||
|
@ -317,6 +328,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
{tr.deckConfigEvaluateButton()}
|
||||
{/if}
|
||||
</button>
|
||||
<DateInput bind:date={$config.ignoreRevlogsBeforeDate}>
|
||||
<SettingTitle on:click={() => openHelpModal("ignoreBefore")}>
|
||||
{tr.deckConfigIgnoreBefore()}
|
||||
</SettingTitle>
|
||||
</DateInput>
|
||||
{#if computingWeights || checkingWeights}<div>
|
||||
{computeWeightsProgressString}
|
||||
</div>{/if}
|
||||
|
|
Loading…
Reference in a new issue