diff --git a/pylib/anki/scheduler/v2.py b/pylib/anki/scheduler/v2.py index f3749822f..f2cb37228 100644 --- a/pylib/anki/scheduler/v2.py +++ b/pylib/anki/scheduler/v2.py @@ -645,14 +645,18 @@ limit ?""" return int(delay * 60) def _delayForRepeatingGrade(self, conf: QueueConfig, left: int) -> Any: - # halfway between last and next delay1 = self._delayForGrade(conf, left) - if len(conf["delays"]) > 1: - delay2 = self._delayForGrade(conf, left - 1) - else: - delay2 = delay1 * 2 - avg = (delay1 + max(delay1, delay2)) // 2 - return avg + # first step? + if len(conf["delays"]) == left % 1000: + # halfway between last and next to avoid same interval with Again + if len(conf["delays"]) > 1: + delay2 = self._delayForGrade(conf, left - 1) + else: + # no next step, use dummy + delay2 = delay1 * 2 + avg = (delay1 + max(delay1, delay2)) // 2 + return avg + return delay1 def _lrnConf(self, card: Card) -> Any: if card.type in (CARD_TYPE_REV, CARD_TYPE_RELEARNING): diff --git a/pylib/tests/test_schedv2.py b/pylib/tests/test_schedv2.py index 824733360..7bb09b4b2 100644 --- a/pylib/tests/test_schedv2.py +++ b/pylib/tests/test_schedv2.py @@ -585,7 +585,7 @@ def test_nextIvl(): assert ni(c, 4) == 4 * 86400 col.sched.answerCard(c, 3) assert ni(c, 1) == 30 - assert ni(c, 2) == (180 + 600) // 2 + assert ni(c, 2) == 180 assert ni(c, 3) == 600 assert ni(c, 4) == 4 * 86400 col.sched.answerCard(c, 3) diff --git a/rslib/src/scheduler/states/steps.rs b/rslib/src/scheduler/states/steps.rs index 82394f651..9d263fdd6 100644 --- a/rslib/src/scheduler/states/steps.rs +++ b/rslib/src/scheduler/states/steps.rs @@ -40,27 +40,23 @@ impl<'a> LearningSteps<'a> { self.secs_at_index(0) } - // fixme: the logic here is not ideal, but tries to match - // the current python code - pub(crate) fn hard_delay_secs(self, remaining: u32) -> Option { let idx = self.get_index(remaining); - if let Some(current) = self - .secs_at_index(idx) + self.secs_at_index(idx) // if current is invalid, try first step .or_else(|| self.steps.first().copied().map(to_secs)) - { - let next = if self.steps.len() > 1 { - self.secs_at_index(idx + 1).unwrap_or(60) - } else { - current.saturating_mul(2) - } - .max(current); - - Some(current.saturating_add(next) / 2) - } else { - None - } + .map(|current| { + // special case to avoid Hard and Again showing same interval + if idx == 0 { + // if there is no next step, simulate one with twice the interval of `current` + let next = self + .secs_at_index(idx + 1) + .unwrap_or_else(|| current.saturating_mul(2)); + current.saturating_add(next) / 2 + } else { + current + } + }) } pub(crate) fn good_delay_secs(self, remaining: u32) -> Option { @@ -82,3 +78,29 @@ impl<'a> LearningSteps<'a> { self.steps.len() as u32 } } + +#[cfg(test)] +mod test { + use super::*; + + macro_rules! assert_delay_secs { + ($steps:expr, $remaining:expr, $again_delay:expr, $hard_delay:expr, $good_delay:expr) => { + let steps = LearningSteps::new(&$steps); + assert_eq!(steps.again_delay_secs_learn(), $again_delay); + assert_eq!(steps.hard_delay_secs($remaining), $hard_delay); + assert_eq!(steps.good_delay_secs($remaining), $good_delay); + }; + } + + #[test] + fn delay_secs() { + assert_delay_secs!([10.0], 1, 600, Some(900), None); + + assert_delay_secs!([1.0, 10.0], 2, 60, Some(330), Some(600)); + assert_delay_secs!([1.0, 10.0], 1, 60, Some(600), None); + + assert_delay_secs!([1.0, 10.0, 100.0], 3, 60, Some(330), Some(600)); + assert_delay_secs!([1.0, 10.0, 100.0], 2, 60, Some(600), Some(6000)); + assert_delay_secs!([1.0, 10.0, 100.0], 1, 60, Some(6000), None); + } +}