add ability to force interval reset

- use trailing ! to force a reset
- use - instead of ..
- tweak i18n messages and error handling
This commit is contained in:
Damien Elmes 2021-02-08 22:33:27 +10:00
parent b9635ce936
commit a8ddb65e1c
8 changed files with 118 additions and 55 deletions

3
ftl/core/errors.ftl Normal file
View file

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

View file

@ -146,16 +146,19 @@ scheduling-deck-updated =
scheduling-set-due-date-prompt = scheduling-set-due-date-prompt =
{ $cards -> { $cards ->
[one] Show card in how many days? [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 -> { $cards ->
[one] Changed card's due date. [one] Set due date of { $cards } card.
*[other] Changed due date of { $cards } cards. *[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 = scheduling-forgot-cards =
{ $cards -> { $cards ->
[one] { $cards } card placed at the end of the new card queue. [one] Forgot { $card } card.
*[other] { $cards } cards placed at the end of the new card queue. *[other] Forgot { $cards } cards.
} }

View file

@ -1430,7 +1430,7 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""",
def reschedCards( def reschedCards(
self, card_ids: List[int], min_interval: int, max_interval: int self, card_ids: List[int], min_interval: int, max_interval: int
) -> None: ) -> 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 forgetCards = schedule_cards_as_new

View file

@ -1124,13 +1124,7 @@ def test_resched():
assert c.due == col.sched.today assert c.due == col.sched.today
assert c.ivl == 1 assert c.ivl == 1
assert c.queue == QUEUE_TYPE_REV and c.type == CARD_TYPE_REV assert c.queue == QUEUE_TYPE_REV and c.type == CARD_TYPE_REV
# make it due tomorrow, which increases its interval by a day # make it due tomorrow
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])
col.sched.reschedCards([c.id], 1, 1) col.sched.reschedCards([c.id], 1, 1)
c.load() c.load()
assert c.due == col.sched.today + 1 assert c.due == col.sched.today + 1

View file

@ -8,7 +8,6 @@ from typing import List
import aqt import aqt
from anki.collection import Config from anki.collection import Config
from anki.errors import InvalidInput
from anki.lang import TR from anki.lang import TR
from aqt.qt import * from aqt.qt import *
from aqt.utils import getText, showWarning, tooltip, tr from aqt.utils import getText, showWarning, tooltip, tr
@ -26,9 +25,15 @@ def set_due_date_dialog(
return return
default = mw.col.get_config_string(default_key) 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( (days, success) = getText(
prompt=tr(TR.SCHEDULING_SET_DUE_DATE_PROMPT, cards=len(card_ids)), prompt=prompt,
parent=parent, parent=parent,
default=default, default=default,
title=tr(TR.ACTIONS_SET_DUE_DATE), title=tr(TR.ACTIONS_SET_DUE_DATE),
@ -45,16 +50,12 @@ def set_due_date_dialog(
try: try:
fut.result() fut.result()
except Exception as e: except Exception as e:
if isinstance(e, InvalidInput): showWarning(str(e))
err = tr(TR.SCHEDULING_SET_DUE_DATE_INVALID_INPUT)
else:
err = str(e)
showWarning(err)
on_done() on_done()
return return
tooltip( 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, parent=parent,
) )

View file

@ -676,8 +676,8 @@ impl BackendService for Backend {
fn set_due_date(&self, input: pb::SetDueDateIn) -> BackendResult<pb::Empty> { fn set_due_date(&self, input: pb::SetDueDateIn) -> BackendResult<pb::Empty> {
let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect(); let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect();
let (min, max) = parse_due_date_str(&input.days)?; let spec = parse_due_date_str(&input.days)?;
self.with_col(|col| col.set_due_date(&cids, min, max).map(Into::into)) self.with_col(|col| col.set_due_date(&cids, spec).map(Into::into))
} }
fn sort_cards(&self, input: pb::SortCardsIn) -> BackendResult<Empty> { fn sort_cards(&self, input: pb::SortCardsIn) -> BackendResult<Empty> {

View file

@ -197,6 +197,17 @@ impl AnkiError {
tr_args!("reason" => reason.into_owned()), 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), _ => format!("{:?}", self),
} }
} }

View file

@ -18,9 +18,11 @@ impl Card {
/// Relearning cards have their interval preserved. Normal review /// Relearning cards have their interval preserved. Normal review
/// cards have their interval adjusted based on change between the /// cards have their interval adjusted based on change between the
/// previous and new due date. /// 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_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 // review cards have their interval shifted based on actual elapsed time
let days_early = old_due - new_due; let days_early = old_due - new_due;
((self.interval as i32) - days_early).max(0) as u32 ((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. #[derive(Debug, PartialEq)]
pub fn parse_due_date_str(s: &str) -> Result<(u32, u32)> { pub struct DueDateSpecifier {
min: u32,
max: u32,
force_reset: bool,
}
pub fn parse_due_date_str(s: &str) -> Result<DueDateSpecifier> {
lazy_static! { lazy_static! {
static ref SINGLE: Regex = Regex::new(r#"^\d+$"#).unwrap(); static ref RE: Regex = Regex::new(
static ref RANGE: Regex = Regex::new(
r#"(?x)^ r#"(?x)^
(\d+) # a number
\.\. (?P<min>\d+)
(\d+) # an optional hypen and another number
(?:
-
(?P<max>\d+)
)?
# optional exclamation mark
(?P<bang>!)?
$ $
"# "#
) )
.unwrap(); .unwrap();
} }
if SINGLE.is_match(s) { let caps = RE.captures(s).ok_or_else(|| AnkiError::invalid_input(s))?;
let num: u32 = s.parse()?; let min: u32 = caps.name("min").unwrap().as_str().parse()?;
Ok((num, num)) let max = if let Some(max) = caps.name("max") {
} else if let Some(cap) = RANGE.captures_iter(s).next() { max.as_str().parse()?
let one: u32 = cap[1].parse()?;
let two: u32 = cap[2].parse()?;
Ok((one.min(two), two.max(one)))
} else { } 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 { 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 usn = self.usn()?;
let today = self.timing_today()?.days_elapsed; let today = self.timing_today()?.days_elapsed;
let mut rng = rand::thread_rng(); 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| { self.transact(None, |col| {
col.storage.set_search_table_to_card_ids(cids, false)?; col.storage.set_search_table_to_card_ids(cids, false)?;
for mut card in col.storage.all_searched_cards()? { for mut card in col.storage.all_searched_cards()? {
let original = card.clone(); let original = card.clone();
let days_from_today = distribution.sample(&mut rng); 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.log_manually_scheduled_review(&card, &original, usn)?;
col.update_card(&mut card, &original, usn)?; col.update_card(&mut card, &original, usn)?;
} }
@ -116,12 +132,42 @@ mod test {
#[test] #[test]
fn parse() -> Result<()> { fn parse() -> Result<()> {
type S = DueDateSpecifier;
assert!(parse_due_date_str("").is_err()); assert!(parse_due_date_str("").is_err());
assert!(parse_due_date_str("x").is_err()); assert!(parse_due_date_str("x").is_err());
assert!(parse_due_date_str("-5").is_err()); assert!(parse_due_date_str("-5").is_err());
assert_eq!(parse_due_date_str("5")?, (5, 5)); assert_eq!(
assert_eq!(parse_due_date_str("50..70")?, (50, 70)); parse_due_date_str("5")?,
assert_eq!(parse_due_date_str("70..50")?, (50, 70)); 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(()) Ok(())
} }
@ -130,29 +176,34 @@ mod test {
let mut c = Card::new(NoteID(0), 0, DeckID(0), 0); let mut c = Card::new(NoteID(0), 0, DeckID(0), 0);
// setting the due date of a new card will convert it // 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.ctype, CardType::Review);
assert_eq!(c.due, 7); assert_eq!(c.due, 7);
assert_eq!(c.interval, 2); assert_eq!(c.interval, 2);
// reschedule it again the next day, shifting it from day 7 to day 9 // 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); assert_eq!(c.due, 9);
// we moved it 2 days forward from its original 2 day interval, and the // we moved it 2 days forward from its original 2 day interval, and the
// interval should match the new delay // interval should match the new delay
assert_eq!(c.interval, 4); assert_eq!(c.interval, 4);
// we can bring cards forward too - return it to its original due date // 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.due, 7);
assert_eq!(c.interval, 2); 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 // should work in a filtered deck
c.original_due = 7; c.original_due = 7;
c.original_deck_id = DeckID(1); c.original_deck_id = DeckID(1);
c.due = -10000; c.due = -10000;
c.queue = CardQueue::New; 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.due, 7);
assert_eq!(c.interval, 2); assert_eq!(c.interval, 2);
assert_eq!(c.queue, CardQueue::Review); assert_eq!(c.queue, CardQueue::Review);
@ -163,7 +214,7 @@ mod test {
c.ctype = CardType::Relearn; c.ctype = CardType::Relearn;
c.original_due = c.due; c.original_due = c.due;
c.due = 12345678; c.due = 12345678;
c.set_due_date(6, 10); c.set_due_date(6, 10, false);
assert_eq!(c.due, 16); assert_eq!(c.due, 16);
assert_eq!(c.interval, 10); assert_eq!(c.interval, 10);
@ -171,7 +222,7 @@ mod test {
c.ctype = CardType::Relearn; c.ctype = CardType::Relearn;
c.original_due = c.due; c.original_due = c.due;
c.due = 12345678; c.due = 12345678;
c.set_due_date(6, 1); c.set_due_date(6, 1, false);
assert_eq!(c.due, 7); assert_eq!(c.due, 7);
assert_eq!(c.interval, 10); assert_eq!(c.interval, 10);
} }