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:
Luc Mcgrady 2024-02-22 04:01:10 +00:00 committed by GitHub
parent 9642a69b88
commit 8b18a08b3b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 276 additions and 43 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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)?;
}

View file

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

View file

@ -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)
);
}
}

View file

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

View file

@ -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(),

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

View file

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