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 =
{ $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.
}

View File

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

View File

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

View File

@ -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,
)

View File

@ -676,8 +676,8 @@ impl BackendService for Backend {
fn set_due_date(&self, input: pb::SetDueDateIn) -> BackendResult<pb::Empty> {
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<Empty> {

View File

@ -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),
}
}

View File

@ -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<DueDateSpecifier> {
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<min>\d+)
# an optional hypen and another number
(?:
-
(?P<max>\d+)
)?
# optional exclamation mark
(?P<bang>!)?
$
"#
)
.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);
}