Anki/rslib/src/sched/timespan.rs
Damien Elmes 97300a16bf implement fuzzing
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.
2021-02-22 21:31:53 +10:00

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");
}
}