mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
Let FSRS control short term schedule (#3375)
* graduate card when user press hard and has 0 learning steps * fix error: useless conversion to the same type * do the same thing to again * fix expected `Option<u32>`, found integer * ./ninja format * let FSRS control short term schedule * Update to FSRS-rs v1.3.0 * ./ninja check:clippy * Update to FSRS-rs v1.3.1 * Pin FSRS version (dae) https://github.com/ankidroid/Anki-Android-Backend/pull/417 * Remove redundant parens (dae)
This commit is contained in:
parent
378c955905
commit
598233299e
7 changed files with 151 additions and 56 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -1862,9 +1862,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "fsrs"
|
||||
version = "1.2.4"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6c3e9d1ab337e63735c4ceff913b9818eeac054d8dc17ca1b259195a7fbfb16"
|
||||
checksum = "2434366942bf285f3c0691e68d731e56f4e1fc1d8ec7b6a0e9411e94eda6ffbd"
|
||||
dependencies = [
|
||||
"burn",
|
||||
"itertools 0.12.1",
|
||||
|
|
|
@ -35,7 +35,7 @@ git = "https://github.com/ankitects/linkcheck.git"
|
|||
rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca"
|
||||
|
||||
[workspace.dependencies.fsrs]
|
||||
version = "1.2.4"
|
||||
version = "=1.3.1"
|
||||
# git = "https://github.com/open-spaced-repetition/fsrs-rs.git"
|
||||
# rev = "58ca25ed2bc4bb1dc376208bbcaed7f5a501b941"
|
||||
# path = "../open-spaced-repetition/fsrs-rs"
|
||||
|
|
|
@ -1225,7 +1225,7 @@
|
|||
},
|
||||
{
|
||||
"name": "fsrs",
|
||||
"version": "1.2.4",
|
||||
"version": "1.3.1",
|
||||
"authors": "Open Spaced Repetition",
|
||||
"repository": "https://github.com/open-spaced-repetition/fsrs-rs",
|
||||
"license": "BSD-3-Clause",
|
||||
|
|
|
@ -105,8 +105,7 @@ impl Collection {
|
|||
Some(state.stability),
|
||||
card.desired_retention.unwrap(),
|
||||
0,
|
||||
)
|
||||
as f32;
|
||||
);
|
||||
card.interval = with_review_fuzz(
|
||||
card.get_fuzz_factor(true),
|
||||
interval,
|
||||
|
|
|
@ -48,13 +48,27 @@ impl LearnState {
|
|||
.into()
|
||||
} else {
|
||||
let (minimum, maximum) = ctx.min_and_max_review_intervals(1);
|
||||
let interval = if let Some(states) = &ctx.fsrs_next_states {
|
||||
states.again.interval
|
||||
let (interval, short_term) = if let Some(states) = &ctx.fsrs_next_states {
|
||||
(states.again.interval, states.again.interval < 0.5)
|
||||
} else {
|
||||
ctx.graduating_interval_good
|
||||
(ctx.graduating_interval_good as f32, false)
|
||||
};
|
||||
|
||||
if short_term {
|
||||
LearnState {
|
||||
remaining_steps: ctx.steps.remaining_for_failed(),
|
||||
scheduled_secs: (interval * 86_400.0) as u32,
|
||||
elapsed_secs: 0,
|
||||
memory_state,
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
ReviewState {
|
||||
scheduled_days: ctx.with_review_fuzz(interval as f32, minimum, maximum),
|
||||
scheduled_days: ctx.with_review_fuzz(
|
||||
interval.round().max(1.0),
|
||||
minimum,
|
||||
maximum,
|
||||
),
|
||||
ease_factor: ctx.initial_ease_factor,
|
||||
memory_state,
|
||||
..Default::default()
|
||||
|
@ -62,6 +76,7 @@ impl LearnState {
|
|||
.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn answer_hard(self, ctx: &StateContext) -> CardState {
|
||||
let memory_state = ctx.fsrs_next_states.as_ref().map(|s| s.hard.memory.into());
|
||||
|
@ -75,13 +90,27 @@ impl LearnState {
|
|||
.into()
|
||||
} else {
|
||||
let (minimum, maximum) = ctx.min_and_max_review_intervals(1);
|
||||
let interval = if let Some(states) = &ctx.fsrs_next_states {
|
||||
states.hard.interval
|
||||
let (interval, short_term) = if let Some(states) = &ctx.fsrs_next_states {
|
||||
(states.hard.interval, states.hard.interval < 0.5)
|
||||
} else {
|
||||
ctx.graduating_interval_good
|
||||
(ctx.graduating_interval_good as f32, false)
|
||||
};
|
||||
|
||||
if short_term {
|
||||
LearnState {
|
||||
scheduled_secs: (interval * 86_400.0) as u32,
|
||||
elapsed_secs: 0,
|
||||
memory_state,
|
||||
..self
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
ReviewState {
|
||||
scheduled_days: ctx.with_review_fuzz(interval as f32, minimum, maximum),
|
||||
scheduled_days: ctx.with_review_fuzz(
|
||||
interval.round().max(1.0),
|
||||
minimum,
|
||||
maximum,
|
||||
),
|
||||
ease_factor: ctx.initial_ease_factor,
|
||||
memory_state,
|
||||
..Default::default()
|
||||
|
@ -89,6 +118,7 @@ impl LearnState {
|
|||
.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn answer_good(self, ctx: &StateContext) -> CardState {
|
||||
let memory_state = ctx.fsrs_next_states.as_ref().map(|s| s.good.memory.into());
|
||||
|
@ -102,13 +132,27 @@ impl LearnState {
|
|||
.into()
|
||||
} else {
|
||||
let (minimum, maximum) = ctx.min_and_max_review_intervals(1);
|
||||
let interval = if let Some(states) = &ctx.fsrs_next_states {
|
||||
states.good.interval
|
||||
let (interval, short_term) = if let Some(states) = &ctx.fsrs_next_states {
|
||||
(states.good.interval, states.good.interval < 0.5)
|
||||
} else {
|
||||
ctx.graduating_interval_good
|
||||
(ctx.graduating_interval_good as f32, false)
|
||||
};
|
||||
|
||||
if short_term {
|
||||
LearnState {
|
||||
scheduled_secs: (interval * 86_400.0) as u32,
|
||||
elapsed_secs: 0,
|
||||
memory_state,
|
||||
..self
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
ReviewState {
|
||||
scheduled_days: ctx.with_review_fuzz(interval as f32, minimum, maximum),
|
||||
scheduled_days: ctx.with_review_fuzz(
|
||||
interval.round().max(1.0),
|
||||
minimum,
|
||||
maximum,
|
||||
),
|
||||
ease_factor: ctx.initial_ease_factor,
|
||||
memory_state,
|
||||
..Default::default()
|
||||
|
@ -116,13 +160,14 @@ impl LearnState {
|
|||
.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn answer_easy(self, ctx: &StateContext) -> ReviewState {
|
||||
let (mut minimum, maximum) = ctx.min_and_max_review_intervals(1);
|
||||
let interval = if let Some(states) = &ctx.fsrs_next_states {
|
||||
let good = ctx.with_review_fuzz(states.good.interval as f32, minimum, maximum);
|
||||
let good = ctx.with_review_fuzz(states.good.interval, minimum, maximum);
|
||||
minimum = good + 1;
|
||||
states.easy.interval
|
||||
states.easy.interval.round().max(1.0) as u32
|
||||
} else {
|
||||
ctx.graduating_interval_easy
|
||||
};
|
||||
|
|
|
@ -45,7 +45,7 @@ impl RelearnState {
|
|||
memory_state,
|
||||
},
|
||||
review: ReviewState {
|
||||
scheduled_days,
|
||||
scheduled_days: scheduled_days.round().max(1.0) as u32,
|
||||
elapsed_days: 0,
|
||||
memory_state,
|
||||
..self.review
|
||||
|
@ -55,11 +55,24 @@ impl RelearnState {
|
|||
} else if let Some(states) = &ctx.fsrs_next_states {
|
||||
let (minimum, maximum) = ctx.min_and_max_review_intervals(1);
|
||||
let interval = states.again.interval;
|
||||
ReviewState {
|
||||
scheduled_days: ctx.with_review_fuzz(interval as f32, minimum, maximum),
|
||||
let again_review = ReviewState {
|
||||
scheduled_days: ctx.with_review_fuzz(interval.round().max(1.0), minimum, maximum),
|
||||
..self.review
|
||||
};
|
||||
let again_relearn = RelearnState {
|
||||
learning: LearnState {
|
||||
remaining_steps: ctx.relearn_steps.remaining_for_failed(),
|
||||
scheduled_secs: (interval * 86_400.0) as u32,
|
||||
elapsed_secs: 0,
|
||||
memory_state,
|
||||
},
|
||||
review: again_review,
|
||||
};
|
||||
if interval > 0.5 {
|
||||
again_review.into()
|
||||
} else {
|
||||
again_relearn.into()
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
self.review.into()
|
||||
}
|
||||
|
@ -87,11 +100,23 @@ impl RelearnState {
|
|||
} else if let Some(states) = &ctx.fsrs_next_states {
|
||||
let (minimum, maximum) = ctx.min_and_max_review_intervals(1);
|
||||
let interval = states.hard.interval;
|
||||
ReviewState {
|
||||
scheduled_days: ctx.with_review_fuzz(interval as f32, minimum, maximum),
|
||||
let hard_review = ReviewState {
|
||||
scheduled_days: ctx.with_review_fuzz(interval.round().max(1.0), minimum, maximum),
|
||||
..self.review
|
||||
};
|
||||
let hard_relearn = RelearnState {
|
||||
learning: LearnState {
|
||||
scheduled_secs: (interval * 86_400.0) as u32,
|
||||
memory_state,
|
||||
..self.learning
|
||||
},
|
||||
review: hard_review,
|
||||
};
|
||||
if interval > 0.5 {
|
||||
hard_review.into()
|
||||
} else {
|
||||
hard_relearn.into()
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
self.review.into()
|
||||
}
|
||||
|
@ -122,11 +147,26 @@ impl RelearnState {
|
|||
} else if let Some(states) = &ctx.fsrs_next_states {
|
||||
let (minimum, maximum) = ctx.min_and_max_review_intervals(1);
|
||||
let interval = states.good.interval;
|
||||
ReviewState {
|
||||
scheduled_days: ctx.with_review_fuzz(interval as f32, minimum, maximum),
|
||||
let good_review = ReviewState {
|
||||
scheduled_days: ctx.with_review_fuzz(interval.round().max(1.0), minimum, maximum),
|
||||
..self.review
|
||||
};
|
||||
let good_relearn = RelearnState {
|
||||
learning: LearnState {
|
||||
scheduled_secs: (interval * 86_400.0) as u32,
|
||||
remaining_steps: ctx
|
||||
.relearn_steps
|
||||
.remaining_for_good(self.learning.remaining_steps),
|
||||
memory_state,
|
||||
..self.learning
|
||||
},
|
||||
review: good_review,
|
||||
};
|
||||
if interval > 0.5 {
|
||||
good_review.into()
|
||||
} else {
|
||||
good_relearn.into()
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
self.review.into()
|
||||
}
|
||||
|
@ -135,10 +175,10 @@ impl RelearnState {
|
|||
fn answer_easy(self, ctx: &StateContext) -> ReviewState {
|
||||
let scheduled_days = if let Some(states) = &ctx.fsrs_next_states {
|
||||
let (mut minimum, maximum) = ctx.min_and_max_review_intervals(1);
|
||||
let good = ctx.with_review_fuzz(states.good.interval as f32, minimum, maximum);
|
||||
let good = ctx.with_review_fuzz(states.good.interval, minimum, maximum);
|
||||
minimum = good + 1;
|
||||
let interval = states.easy.interval;
|
||||
ctx.with_review_fuzz(interval as f32, minimum, maximum)
|
||||
ctx.with_review_fuzz(interval.round().max(1.0), minimum, maximum)
|
||||
} else {
|
||||
self.review.scheduled_days + 1
|
||||
};
|
||||
|
|
|
@ -75,7 +75,7 @@ impl ReviewState {
|
|||
pub(crate) fn failing_review_interval(
|
||||
self,
|
||||
ctx: &StateContext,
|
||||
) -> (u32, Option<FsrsMemoryState>) {
|
||||
) -> (f32, Option<FsrsMemoryState>) {
|
||||
if let Some(states) = &ctx.fsrs_next_states {
|
||||
// In FSRS, fuzz is applied when the card leaves the relearning
|
||||
// stage
|
||||
|
@ -87,7 +87,7 @@ impl ReviewState {
|
|||
minimum,
|
||||
maximum,
|
||||
);
|
||||
(interval, None)
|
||||
(interval as f32, None)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,13 +96,22 @@ impl ReviewState {
|
|||
let leeched = leech_threshold_met(lapses, ctx.leech_threshold);
|
||||
let (scheduled_days, memory_state) = self.failing_review_interval(ctx);
|
||||
let again_review = ReviewState {
|
||||
scheduled_days,
|
||||
scheduled_days: scheduled_days.round().max(1.0) as u32,
|
||||
elapsed_days: 0,
|
||||
ease_factor: (self.ease_factor + EASE_FACTOR_AGAIN_DELTA).max(MINIMUM_EASE_FACTOR),
|
||||
lapses,
|
||||
leeched,
|
||||
memory_state,
|
||||
};
|
||||
let again_relearn = RelearnState {
|
||||
learning: LearnState {
|
||||
remaining_steps: ctx.relearn_steps.remaining_for_failed(),
|
||||
scheduled_secs: (scheduled_days * 86_400.0) as u32,
|
||||
elapsed_secs: 0,
|
||||
memory_state,
|
||||
},
|
||||
review: again_review,
|
||||
};
|
||||
|
||||
if let Some(again_delay) = ctx.relearn_steps.again_delay_secs_learn() {
|
||||
RelearnState {
|
||||
|
@ -115,6 +124,8 @@ impl ReviewState {
|
|||
review: again_review,
|
||||
}
|
||||
.into()
|
||||
} else if scheduled_days < 0.5 {
|
||||
again_relearn.into()
|
||||
} else {
|
||||
again_review.into()
|
||||
}
|
||||
|
@ -177,20 +188,20 @@ impl ReviewState {
|
|||
};
|
||||
let hard = constrain_passing_interval(
|
||||
ctx,
|
||||
states.hard.interval as f32,
|
||||
greater_than_last(states.hard.interval).max(1),
|
||||
states.hard.interval,
|
||||
greater_than_last(states.hard.interval.round() as u32).max(1),
|
||||
true,
|
||||
);
|
||||
let good = constrain_passing_interval(
|
||||
ctx,
|
||||
states.good.interval as f32,
|
||||
greater_than_last(states.good.interval).max(hard + 1),
|
||||
states.good.interval,
|
||||
greater_than_last(states.good.interval.round() as u32).max(hard + 1),
|
||||
true,
|
||||
);
|
||||
let easy = constrain_passing_interval(
|
||||
ctx,
|
||||
states.easy.interval as f32,
|
||||
greater_than_last(states.easy.interval).max(good + 1),
|
||||
states.easy.interval,
|
||||
greater_than_last(states.easy.interval.round() as u32).max(good + 1),
|
||||
true,
|
||||
);
|
||||
(hard, good, easy)
|
||||
|
|
Loading…
Reference in a new issue