diff --git a/ftl/core/errors.ftl b/ftl/core/errors.ftl new file mode 100644 index 000000000..5f5ea6eb5 --- /dev/null +++ b/ftl/core/errors.ftl @@ -0,0 +1,3 @@ +errors-invalid-input-empty = Invalid input. +errors-invalid-input-details = Invalid input: { $details } +errors-parse-number-fail = A number was invalid or out of range. diff --git a/ftl/core/scheduling.ftl b/ftl/core/scheduling.ftl index 453246bca..dc6549cc6 100644 --- a/ftl/core/scheduling.ftl +++ b/ftl/core/scheduling.ftl @@ -146,16 +146,19 @@ scheduling-deck-updated = scheduling-set-due-date-prompt = { $cards -> [one] Show card in how many days? - *[other] Show cards in how many days? (eg 1, or 1..7) + *[other] Show cards in how many days? } -scheduling-set-due-date-changed-cards = +scheduling-set-due-date-prompt-hint = + 0 = today + 1! = tomorrow+reset review interval + 3-7 = random choice of 3-7 days +scheduling-set-due-date-done = { $cards -> - [one] Changed card's due date. - *[other] Changed due date of { $cards } cards. + [one] Set due date of { $cards } card. + *[other] Set due date of { $cards } cards. } -scheduling-set-due-date-invalid-input = Expected a number or range (eg 1, or 1..7) scheduling-forgot-cards = { $cards -> - [one] { $cards } card placed at the end of the new card queue. - *[other] { $cards } cards placed at the end of the new card queue. + [one] Forgot { $card } card. + *[other] Forgot { $cards } cards. } diff --git a/pylib/anki/schedv2.py b/pylib/anki/schedv2.py index 06f5d4748..c2f5cdd72 100644 --- a/pylib/anki/schedv2.py +++ b/pylib/anki/schedv2.py @@ -1430,7 +1430,7 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""", def reschedCards( self, card_ids: List[int], min_interval: int, max_interval: int ) -> None: - self.set_due_date(card_ids, f"{min_interval}..{max_interval}") + self.set_due_date(card_ids, f"{min_interval}-{max_interval}!") forgetCards = schedule_cards_as_new diff --git a/pylib/tests/test_schedv2.py b/pylib/tests/test_schedv2.py index 45a8997b7..070d688a2 100644 --- a/pylib/tests/test_schedv2.py +++ b/pylib/tests/test_schedv2.py @@ -1124,13 +1124,7 @@ def test_resched(): assert c.due == col.sched.today assert c.ivl == 1 assert c.queue == QUEUE_TYPE_REV and c.type == CARD_TYPE_REV - # make it due tomorrow, which increases its interval by a day - col.sched.reschedCards([c.id], 1, 1) - c.load() - assert c.due == col.sched.today + 1 - assert c.ivl == 2 - # but if it was new, that would not happen - col.sched.forgetCards([c.id]) + # make it due tomorrow col.sched.reschedCards([c.id], 1, 1) c.load() assert c.due == col.sched.today + 1 diff --git a/qt/aqt/scheduling.py b/qt/aqt/scheduling.py index bc753d21c..7366fa02e 100644 --- a/qt/aqt/scheduling.py +++ b/qt/aqt/scheduling.py @@ -8,7 +8,6 @@ from typing import List import aqt from anki.collection import Config -from anki.errors import InvalidInput from anki.lang import TR from aqt.qt import * from aqt.utils import getText, showWarning, tooltip, tr @@ -26,9 +25,15 @@ def set_due_date_dialog( return default = mw.col.get_config_string(default_key) + prompt = "\n".join( + [ + tr(TR.SCHEDULING_SET_DUE_DATE_PROMPT, cards=len(card_ids)), + tr(TR.SCHEDULING_SET_DUE_DATE_PROMPT_HINT), + ] + ) (days, success) = getText( - prompt=tr(TR.SCHEDULING_SET_DUE_DATE_PROMPT, cards=len(card_ids)), + prompt=prompt, parent=parent, default=default, title=tr(TR.ACTIONS_SET_DUE_DATE), @@ -45,16 +50,12 @@ def set_due_date_dialog( try: fut.result() except Exception as e: - if isinstance(e, InvalidInput): - err = tr(TR.SCHEDULING_SET_DUE_DATE_INVALID_INPUT) - else: - err = str(e) - showWarning(err) + showWarning(str(e)) on_done() return tooltip( - tr(TR.SCHEDULING_SET_DUE_DATE_CHANGED_CARDS, cards=len(card_ids)), + tr(TR.SCHEDULING_SET_DUE_DATE_DONE, cards=len(card_ids)), parent=parent, ) diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 905e2aec7..535d4e251 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -676,8 +676,8 @@ impl BackendService for Backend { fn set_due_date(&self, input: pb::SetDueDateIn) -> BackendResult { let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect(); - let (min, max) = parse_due_date_str(&input.days)?; - self.with_col(|col| col.set_due_date(&cids, min, max).map(Into::into)) + let spec = parse_due_date_str(&input.days)?; + self.with_col(|col| col.set_due_date(&cids, spec).map(Into::into)) } fn sort_cards(&self, input: pb::SortCardsIn) -> BackendResult { diff --git a/rslib/src/err.rs b/rslib/src/err.rs index 886270eda..86d7266bb 100644 --- a/rslib/src/err.rs +++ b/rslib/src/err.rs @@ -197,6 +197,17 @@ impl AnkiError { tr_args!("reason" => reason.into_owned()), ) } + AnkiError::InvalidInput { info } => { + if info.is_empty() { + i18n.tr(TR::ErrorsInvalidInputEmpty).into() + } else { + i18n.trn( + TR::ErrorsInvalidInputDetails, + tr_args!("details" => info.to_owned()), + ) + } + } + AnkiError::ParseNumError => i18n.tr(TR::ErrorsParseNumberFail).into(), _ => format!("{:?}", self), } } diff --git a/rslib/src/sched/reviews.rs b/rslib/src/sched/reviews.rs index 48ed884c7..3571c7ee2 100644 --- a/rslib/src/sched/reviews.rs +++ b/rslib/src/sched/reviews.rs @@ -18,9 +18,11 @@ impl Card { /// Relearning cards have their interval preserved. Normal review /// cards have their interval adjusted based on change between the /// previous and new due date. - fn set_due_date(&mut self, today: u32, days_from_today: u32) { + 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 let Some(old_due) = self.current_review_due_day() { + let new_interval = if force_reset { + days_from_today + } else if let Some(old_due) = self.current_review_due_day() { // review cards have their interval shifted based on actual elapsed time let days_early = old_due - new_due; ((self.interval as i32) - days_early).max(0) as u32 @@ -62,44 +64,58 @@ impl Card { } } -/// Parse a number or range (eg '4' or '4..7') into min and max. -pub fn parse_due_date_str(s: &str) -> Result<(u32, u32)> { +#[derive(Debug, PartialEq)] +pub struct DueDateSpecifier { + min: u32, + max: u32, + force_reset: bool, +} + +pub fn parse_due_date_str(s: &str) -> Result { lazy_static! { - static ref SINGLE: Regex = Regex::new(r#"^\d+$"#).unwrap(); - static ref RANGE: Regex = Regex::new( + static ref RE: Regex = Regex::new( r#"(?x)^ - (\d+) - \.\. - (\d+) + # a number + (?P\d+) + # an optional hypen and another number + (?: + - + (?P\d+) + )? + # optional exclamation mark + (?P!)? $ "# ) .unwrap(); } - if SINGLE.is_match(s) { - let num: u32 = s.parse()?; - Ok((num, num)) - } else if let Some(cap) = RANGE.captures_iter(s).next() { - let one: u32 = cap[1].parse()?; - let two: u32 = cap[2].parse()?; - Ok((one.min(two), two.max(one))) + 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 { - Err(AnkiError::ParseNumError) - } + min + }; + let force_reset = caps.name("bang").is_some(); + Ok(DueDateSpecifier { + min: min.min(max), + max: max.max(min), + force_reset, + }) } impl Collection { - pub fn set_due_date(&mut self, cids: &[CardID], min_days: u32, max_days: u32) -> Result<()> { + pub fn set_due_date(&mut self, cids: &[CardID], spec: DueDateSpecifier) -> Result<()> { let usn = self.usn()?; let today = self.timing_today()?.days_elapsed; let mut rng = rand::thread_rng(); - let distribution = Uniform::from(min_days..=max_days); + let distribution = Uniform::from(spec.min..=spec.max); self.transact(None, |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); + card.set_due_date(today, days_from_today, spec.force_reset); col.log_manually_scheduled_review(&card, &original, usn)?; col.update_card(&mut card, &original, usn)?; } @@ -116,12 +132,42 @@ mod test { #[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")?, (5, 5)); - assert_eq!(parse_due_date_str("50..70")?, (50, 70)); - assert_eq!(parse_due_date_str("70..50")?, (50, 70)); + 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(()) } @@ -130,29 +176,34 @@ mod test { 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); + 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); + c.set_due_date(6, 3, false); assert_eq!(c.due, 9); // we moved it 2 days forward from its original 2 day interval, and the // interval should match the new delay assert_eq!(c.interval, 4); // we can bring cards forward too - return it to its original due date - c.set_due_date(6, 1); + 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, 2, true); + assert_eq!(c.due, 8); + assert_eq!(c.interval, 2); + // should work in a filtered deck c.original_due = 7; c.original_deck_id = DeckID(1); c.due = -10000; c.queue = CardQueue::New; - c.set_due_date(6, 1); + c.set_due_date(6, 1, false); assert_eq!(c.due, 7); assert_eq!(c.interval, 2); assert_eq!(c.queue, CardQueue::Review); @@ -163,7 +214,7 @@ mod test { c.ctype = CardType::Relearn; c.original_due = c.due; c.due = 12345678; - c.set_due_date(6, 10); + c.set_due_date(6, 10, false); assert_eq!(c.due, 16); assert_eq!(c.interval, 10); @@ -171,7 +222,7 @@ mod test { c.ctype = CardType::Relearn; c.original_due = c.due; c.due = 12345678; - c.set_due_date(6, 1); + c.set_due_date(6, 1, false); assert_eq!(c.due, 7); assert_eq!(c.interval, 10); }