Anki/rslib/src/scheduler/timespan.rs
Damien Elmes ded805b504
Switch Rust import style (#2330)
* Prepare to switch Rust import style

* Run nightly format

Closes #2320

* Clean up a few imports

* Enable comment wrapping

* Wrap comments
2023-01-18 21:39:55 +10:00

192 lines
6.2 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::I18n;
/// Short string like '4d' to place above answer buttons.
pub fn answer_button_time(seconds: f32, tr: &I18n) -> String {
let span = Timespan::from_secs(seconds).natural_span();
let amount = span.as_rounded_unit_for_answer_buttons();
match span.unit() {
TimespanUnit::Seconds => tr.scheduling_answer_button_time_seconds(amount),
TimespanUnit::Minutes => tr.scheduling_answer_button_time_minutes(amount),
TimespanUnit::Hours => tr.scheduling_answer_button_time_hours(amount),
TimespanUnit::Days => tr.scheduling_answer_button_time_days(amount),
TimespanUnit::Months => tr.scheduling_answer_button_time_months(amount),
TimespanUnit::Years => tr.scheduling_answer_button_time_years(amount),
}
.into()
}
/// 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, tr: &I18n) -> String {
let string = answer_button_time(seconds as f32, tr);
if seconds == 0 {
tr.scheduling_end().into()
} else 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, tr: &I18n, precise: bool) -> String {
let span = Timespan::from_secs(seconds).natural_span();
let amount = if precise {
span.as_unit()
} else {
span.as_rounded_unit()
};
match span.unit() {
TimespanUnit::Seconds => tr.scheduling_time_span_seconds(amount),
TimespanUnit::Minutes => tr.scheduling_time_span_minutes(amount),
TimespanUnit::Hours => tr.scheduling_time_span_hours(amount),
TimespanUnit::Days => tr.scheduling_time_span_days(amount),
TimespanUnit::Months => tr.scheduling_time_span_months(amount),
TimespanUnit::Years => tr.scheduling_time_span_years(amount),
}
.into()
}
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 = 365.0 * DAY;
#[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::scheduler::timespan::answer_button_time;
use crate::scheduler::timespan::time_span;
use crate::scheduler::timespan::MONTH;
#[test]
fn answer_buttons() {
let tr = I18n::template_only();
assert_eq!(answer_button_time(30.0, &tr), "30s");
assert_eq!(answer_button_time(70.0, &tr), "1m");
assert_eq!(answer_button_time(1.1 * MONTH, &tr), "1.1mo");
}
#[test]
fn time_spans() {
let tr = I18n::template_only();
assert_eq!(time_span(1.0, &tr, false), "1 second");
assert_eq!(time_span(30.3, &tr, false), "30 seconds");
assert_eq!(time_span(30.3, &tr, true), "30.3 seconds");
assert_eq!(time_span(90.0, &tr, false), "1.5 minutes");
assert_eq!(time_span(45.0 * 86_400.0, &tr, false), "1.5 months");
assert_eq!(time_span(365.0 * 86_400.0 * 1.5, &tr, false), "1.5 years");
}
}