mirror of
https://github.com/ankitects/anki.git
synced 2025-09-23 00:12:25 -04:00

Notes: - The fuzz seed is now derived from the card id and # of reps, so if a card is undone and done again, the same fuzz will be used. - The intervals shown on the answer buttons now include the fuzz, instead of hiding it from the user. This will prevent questions about due dates being different to what was shown on the buttons, but will create questions about due dates being different for cards with the same interval, and some people may find it distracting for learning cards. The new approach is easier to reason about, but time will tell whether it's a net gain or not. - The env var we were using to shift the clock away from rollover for unit tests has been repurposed to also disable fuzzing, which simplifies the tests. - Cards in filtered decks without scheduling now have the preview delay fuzzed. - Sub-day learning cards are mostly fuzzed like before, but will apply the up-to-5-minutes of fuzz regardless of the time of day. - The answer buttons now round minute values, as the fuzz on short intervals is distracting.
192 lines
6.1 KiB
Rust
192 lines
6.1 KiB
Rust
// Copyright: Ankitects Pty Ltd and contributors
|
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
use crate::i18n::{tr_args, I18n, TR};
|
|
|
|
/// Short string like '4d' to place above answer buttons.
|
|
pub fn answer_button_time(seconds: f32, i18n: &I18n) -> String {
|
|
let span = Timespan::from_secs(seconds).natural_span();
|
|
let args = tr_args!["amount" => span.as_rounded_unit_for_answer_buttons()];
|
|
let key = match span.unit() {
|
|
TimespanUnit::Seconds => TR::SchedulingAnswerButtonTimeSeconds,
|
|
TimespanUnit::Minutes => TR::SchedulingAnswerButtonTimeMinutes,
|
|
TimespanUnit::Hours => TR::SchedulingAnswerButtonTimeHours,
|
|
TimespanUnit::Days => TR::SchedulingAnswerButtonTimeDays,
|
|
TimespanUnit::Months => TR::SchedulingAnswerButtonTimeMonths,
|
|
TimespanUnit::Years => TR::SchedulingAnswerButtonTimeYears,
|
|
};
|
|
i18n.trn(key, args)
|
|
}
|
|
|
|
/// Short string like '4d' to place above answer buttons.
|
|
/// Times within the collapse time are represented like '<10m'
|
|
pub fn answer_button_time_collapsible(seconds: u32, collapse_secs: u32, i18n: &I18n) -> String {
|
|
let string = answer_button_time(seconds as f32, i18n);
|
|
if seconds < collapse_secs {
|
|
format!("<{}", string)
|
|
} else {
|
|
string
|
|
}
|
|
}
|
|
|
|
/// Describe the given seconds using the largest appropriate unit.
|
|
/// If precise is true, show to two decimal places, eg
|
|
/// eg 70 seconds -> "1.17 minutes"
|
|
/// If false, seconds and days are shown without decimals.
|
|
pub fn time_span(seconds: f32, i18n: &I18n, precise: bool) -> String {
|
|
let span = Timespan::from_secs(seconds).natural_span();
|
|
let amount = if precise {
|
|
span.as_unit()
|
|
} else {
|
|
span.as_rounded_unit()
|
|
};
|
|
let args = tr_args!["amount" => amount];
|
|
let key = match span.unit() {
|
|
TimespanUnit::Seconds => TR::SchedulingTimeSpanSeconds,
|
|
TimespanUnit::Minutes => TR::SchedulingTimeSpanMinutes,
|
|
TimespanUnit::Hours => TR::SchedulingTimeSpanHours,
|
|
TimespanUnit::Days => TR::SchedulingTimeSpanDays,
|
|
TimespanUnit::Months => TR::SchedulingTimeSpanMonths,
|
|
TimespanUnit::Years => TR::SchedulingTimeSpanYears,
|
|
};
|
|
i18n.trn(key, args)
|
|
}
|
|
|
|
const SECOND: f32 = 1.0;
|
|
const MINUTE: f32 = 60.0 * SECOND;
|
|
const HOUR: f32 = 60.0 * MINUTE;
|
|
const DAY: f32 = 24.0 * HOUR;
|
|
const MONTH: f32 = 30.0 * DAY;
|
|
const YEAR: f32 = 12.0 * MONTH;
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub(crate) enum TimespanUnit {
|
|
Seconds,
|
|
Minutes,
|
|
Hours,
|
|
Days,
|
|
Months,
|
|
Years,
|
|
}
|
|
|
|
impl TimespanUnit {
|
|
pub fn as_str(self) -> &'static str {
|
|
match self {
|
|
TimespanUnit::Seconds => "seconds",
|
|
TimespanUnit::Minutes => "minutes",
|
|
TimespanUnit::Hours => "hours",
|
|
TimespanUnit::Days => "days",
|
|
TimespanUnit::Months => "months",
|
|
TimespanUnit::Years => "years",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub(crate) struct Timespan {
|
|
seconds: f32,
|
|
unit: TimespanUnit,
|
|
}
|
|
|
|
impl Timespan {
|
|
pub fn from_secs(seconds: f32) -> Self {
|
|
Timespan {
|
|
seconds,
|
|
unit: TimespanUnit::Seconds,
|
|
}
|
|
}
|
|
|
|
/// Return the value as the configured unit, eg seconds=70/unit=Minutes
|
|
/// returns 1.17
|
|
pub fn as_unit(self) -> f32 {
|
|
let s = self.seconds;
|
|
match self.unit {
|
|
TimespanUnit::Seconds => s,
|
|
TimespanUnit::Minutes => s / MINUTE,
|
|
TimespanUnit::Hours => s / HOUR,
|
|
TimespanUnit::Days => s / DAY,
|
|
TimespanUnit::Months => s / MONTH,
|
|
TimespanUnit::Years => s / YEAR,
|
|
}
|
|
}
|
|
|
|
/// Round seconds and days to integers, otherwise
|
|
/// truncates to one decimal place.
|
|
pub fn as_rounded_unit(self) -> f32 {
|
|
match self.unit {
|
|
// seconds/minutes/days as integer
|
|
TimespanUnit::Seconds | TimespanUnit::Days => self.as_unit().round(),
|
|
// other values shown to 1 decimal place
|
|
_ => (self.as_unit() * 10.0).round() / 10.0,
|
|
}
|
|
}
|
|
|
|
/// Round seconds, minutes and days to integers, otherwise
|
|
/// truncates to one decimal place.
|
|
pub fn as_rounded_unit_for_answer_buttons(self) -> f32 {
|
|
match self.unit {
|
|
// seconds/minutes/days as integer
|
|
TimespanUnit::Seconds | TimespanUnit::Minutes | TimespanUnit::Days => {
|
|
self.as_unit().round()
|
|
}
|
|
// other values shown to 1 decimal place
|
|
_ => (self.as_unit() * 10.0).round() / 10.0,
|
|
}
|
|
}
|
|
|
|
pub fn unit(self) -> TimespanUnit {
|
|
self.unit
|
|
}
|
|
|
|
/// Return a new timespan in the most appropriate unit, eg
|
|
/// 70 secs -> timespan in minutes
|
|
pub fn natural_span(self) -> Timespan {
|
|
let secs = self.seconds.abs();
|
|
let unit = if secs < MINUTE {
|
|
TimespanUnit::Seconds
|
|
} else if secs < HOUR {
|
|
TimespanUnit::Minutes
|
|
} else if secs < DAY {
|
|
TimespanUnit::Hours
|
|
} else if secs < MONTH {
|
|
TimespanUnit::Days
|
|
} else if secs < YEAR {
|
|
TimespanUnit::Months
|
|
} else {
|
|
TimespanUnit::Years
|
|
};
|
|
|
|
Timespan {
|
|
seconds: self.seconds,
|
|
unit,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use crate::i18n::I18n;
|
|
use crate::log;
|
|
use crate::sched::timespan::{answer_button_time, time_span, MONTH};
|
|
|
|
#[test]
|
|
fn answer_buttons() {
|
|
let log = log::terminal();
|
|
let i18n = I18n::new(&["zz"], "", log);
|
|
assert_eq!(answer_button_time(30.0, &i18n), "30s");
|
|
assert_eq!(answer_button_time(70.0, &i18n), "1m");
|
|
assert_eq!(answer_button_time(1.1 * MONTH, &i18n), "1.1mo");
|
|
}
|
|
|
|
#[test]
|
|
fn time_spans() {
|
|
let log = log::terminal();
|
|
let i18n = I18n::new(&["zz"], "", log);
|
|
assert_eq!(time_span(1.0, &i18n, false), "1 second");
|
|
assert_eq!(time_span(30.3, &i18n, false), "30 seconds");
|
|
assert_eq!(time_span(30.3, &i18n, true), "30.3 seconds");
|
|
assert_eq!(time_span(90.0, &i18n, false), "1.5 minutes");
|
|
assert_eq!(time_span(45.0 * 86_400.0, &i18n, false), "1.5 months");
|
|
assert_eq!(time_span(365.0 * 86_400.0 * 1.5, &i18n, false), "1.5 years");
|
|
}
|
|
}
|