Anki/rslib/src/scheduler/reviews.rs
Damien Elmes 3d4cf26758 expose undoable config changes to frontend; refresh sidebar
The browser header handling still needs updating
2021-05-21 17:50:41 +10:00

213 lines
6.3 KiB
Rust

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use lazy_static::lazy_static;
use rand::distributions::{Distribution, Uniform};
use regex::Regex;
use crate::{
card::{Card, CardId, CardQueue, CardType},
collection::Collection,
config::StringKey,
deckconfig::INITIAL_EASE_FACTOR_THOUSANDS,
error::Result,
prelude::*,
};
impl Card {
/// Make card due in `days_from_today`.
/// If card is not a review card, convert it into one.
/// Review/relearning cards have their interval preserved unless
/// `force_reset` is true.
fn set_due_date(&mut self, today: u32, days_from_today: u32, force_reset: bool) {
let new_due = (today + days_from_today) as i32;
let new_interval =
if force_reset || !matches!(self.ctype, CardType::Review | CardType::Relearn) {
days_from_today
} else {
self.interval
};
self.schedule_as_review(new_interval, new_due);
}
fn schedule_as_review(&mut self, interval: u32, due: i32) {
self.remove_from_filtered_deck_before_reschedule();
self.interval = interval.max(1);
self.due = due;
self.ctype = CardType::Review;
self.queue = CardQueue::Review;
if self.ease_factor == 0 {
// unlike the old Python code, we leave the ease factor alone
// if it's already set
self.ease_factor = INITIAL_EASE_FACTOR_THOUSANDS;
}
}
}
#[derive(Debug, PartialEq)]
pub struct DueDateSpecifier {
min: u32,
max: u32,
force_reset: bool,
}
pub fn parse_due_date_str(s: &str) -> Result<DueDateSpecifier> {
lazy_static! {
static ref RE: Regex = Regex::new(
r#"(?x)^
# a number
(?P<min>\d+)
# an optional hypen and another number
(?:
-
(?P<max>\d+)
)?
# optional exclamation mark
(?P<bang>!)?
$
"#
)
.unwrap();
}
let caps = RE.captures(s).ok_or_else(|| AnkiError::invalid_input(s))?;
let min: u32 = caps.name("min").unwrap().as_str().parse()?;
let max = if let Some(max) = caps.name("max") {
max.as_str().parse()?
} else {
min
};
let force_reset = caps.name("bang").is_some();
Ok(DueDateSpecifier {
min: min.min(max),
max: max.max(min),
force_reset,
})
}
impl Collection {
/// `days` should be in a format parseable by `parse_due_date_str`.
/// If `context` is provided, provided key will be updated with the new
/// value of `days`.
pub fn set_due_date(
&mut self,
cids: &[CardId],
days: &str,
context: Option<StringKey>,
) -> Result<OpOutput<()>> {
let spec = parse_due_date_str(days)?;
let usn = self.usn()?;
let today = self.timing_today()?.days_elapsed;
let mut rng = rand::thread_rng();
let distribution = Uniform::from(spec.min..=spec.max);
self.transact(Op::SetDueDate, |col| {
col.storage.set_search_table_to_card_ids(cids, false)?;
for mut card in col.storage.all_searched_cards()? {
let original = card.clone();
let days_from_today = distribution.sample(&mut rng);
card.set_due_date(today, days_from_today, spec.force_reset);
col.log_manually_scheduled_review(&card, &original, usn)?;
col.update_card_inner(&mut card, original, usn)?;
}
col.storage.clear_searched_cards_table()?;
if let Some(key) = context {
col.set_config_string_inner(key, days)?;
}
Ok(())
})
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::prelude::*;
#[test]
fn parse() -> Result<()> {
type S = DueDateSpecifier;
assert!(parse_due_date_str("").is_err());
assert!(parse_due_date_str("x").is_err());
assert!(parse_due_date_str("-5").is_err());
assert_eq!(
parse_due_date_str("5")?,
S {
min: 5,
max: 5,
force_reset: false
}
);
assert_eq!(
parse_due_date_str("5!")?,
S {
min: 5,
max: 5,
force_reset: true
}
);
assert_eq!(
parse_due_date_str("50-70")?,
S {
min: 50,
max: 70,
force_reset: false
}
);
assert_eq!(
parse_due_date_str("70-50!")?,
S {
min: 50,
max: 70,
force_reset: true
}
);
Ok(())
}
#[test]
fn due_date() {
let mut c = Card::new(NoteId(0), 0, DeckId(0), 0);
// setting the due date of a new card will convert it
c.set_due_date(5, 2, false);
assert_eq!(c.ctype, CardType::Review);
assert_eq!(c.due, 7);
assert_eq!(c.interval, 2);
// reschedule it again the next day, shifting it from day 7 to day 9
c.set_due_date(6, 3, false);
assert_eq!(c.due, 9);
assert_eq!(c.interval, 2);
// we can bring cards forward too - return it to its original due date
c.set_due_date(6, 1, false);
assert_eq!(c.due, 7);
assert_eq!(c.interval, 2);
// we can force the interval to be reset instead of shifted
c.set_due_date(6, 3, true);
assert_eq!(c.due, 9);
assert_eq!(c.interval, 3);
// should work in a filtered deck
c.interval = 2;
c.original_due = 7;
c.original_deck_id = DeckId(1);
c.due = -10000;
c.queue = CardQueue::New;
c.set_due_date(6, 1, false);
assert_eq!(c.due, 7);
assert_eq!(c.interval, 2);
assert_eq!(c.queue, CardQueue::Review);
assert_eq!(c.original_due, 0);
assert_eq!(c.original_deck_id, DeckId(0));
// relearning treated like review
c.ctype = CardType::Relearn;
c.original_due = c.due;
c.due = 12345678;
c.set_due_date(6, 10, false);
assert_eq!(c.due, 16);
assert_eq!(c.interval, 2);
}
}