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:
parent
b9635ce936
commit
a8ddb65e1c
3
ftl/core/errors.ftl
Normal file
3
ftl/core/errors.ftl
Normal 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.
|
@ -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.
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
@ -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> {
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user