diff --git a/proto/backend.proto b/proto/backend.proto index 2fcf913a4..cd4c0affb 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -307,8 +307,9 @@ message TranslateArgValue { message FormatTimeSpanIn { enum Context { - NORMAL = 0; + PRECISE = 0; ANSWER_BUTTONS = 1; + INTERVALS = 2; } float seconds = 1; diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index 62ee71bff..6395a184f 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -342,7 +342,7 @@ class RustBackend: def format_time_span( self, seconds: float, - context: FormatTimeSpanContext = FormatTimeSpanContext.NORMAL, + context: FormatTimeSpanContext = FormatTimeSpanContext.INTERVALS, ) -> str: return self._run_command( pb.BackendInput( diff --git a/pylib/anki/stats.py b/pylib/anki/stats.py index 133b7cf72..5be72f04a 100644 --- a/pylib/anki/stats.py +++ b/pylib/anki/stats.py @@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional, Tuple import anki from anki.consts import * from anki.lang import _, ngettext -from anki.rsbackend import FString +from anki.rsbackend import FormatTimeSpanContext, FString from anki.utils import ids2str # Card stats @@ -85,7 +85,9 @@ class CardStats: return time.strftime("%Y-%m-%d", time.localtime(tm)) def time(self, tm: float) -> str: - return self.col.backend.format_time_span(tm) + return self.col.backend.format_time_span( + tm, context=FormatTimeSpanContext.PRECISE + ) # Collection stats diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 832a22017..d8ecb9fe3 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -1503,7 +1503,7 @@ border: 1px solid #000; padding: 3px; '>%s""" s += ("%s" * 2) % ( "%d%%" % (factor / 10) if factor else "", - cs.time(taken), + self.col.backend.format_time_span(taken) ) + "" s += "" if cnt < self.card.reps: diff --git a/rslib/src/backend.rs b/rslib/src/backend.rs index 493471b6c..6a7f8d033 100644 --- a/rslib/src/backend.rs +++ b/rslib/src/backend.rs @@ -422,7 +422,10 @@ impl Backend { None => return "".to_string(), }; match context { - pb::format_time_span_in::Context::Normal => time_span(input.seconds, &self.i18n), + pb::format_time_span_in::Context::Precise => time_span(input.seconds, &self.i18n, true), + pb::format_time_span_in::Context::Intervals => { + time_span(input.seconds, &self.i18n, false) + } pb::format_time_span_in::Context::AnswerButtons => { answer_button_time(input.seconds, &self.i18n) } diff --git a/rslib/src/sched/timespan.rs b/rslib/src/sched/timespan.rs index 9c496811e..f78156ad9 100644 --- a/rslib/src/sched/timespan.rs +++ b/rslib/src/sched/timespan.rs @@ -6,13 +6,7 @@ use crate::i18n::{tr_args, FString, I18n}; /// 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 amount = match span.unit() { - // months/years shown with 1 decimal place - TimespanUnit::Months | TimespanUnit::Years => (span.as_unit() * 10.0).round() / 10.0, - // other values shown without decimals - _ => span.as_unit().round(), - }; - let args = tr_args!["amount" => amount]; + let args = tr_args!["amount" => span.as_rounded_unit()]; let key = match span.unit() { TimespanUnit::Seconds => FString::SchedulingAnswerButtonTimeSeconds, TimespanUnit::Minutes => FString::SchedulingAnswerButtonTimeMinutes, @@ -24,11 +18,17 @@ pub fn answer_button_time(seconds: f32, i18n: &I18n) -> String { i18n.trn(key, args) } -/// Describe the given seconds using the largest appropriate unit +/// 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" -pub fn time_span(seconds: f32, i18n: &I18n) -> String { +/// 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 = span.as_unit(); + 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 => FString::SchedulingTimeSpanSeconds, @@ -133,6 +133,17 @@ impl Timespan { } } + /// Round seconds and days to integers, otherwise + /// truncates to one decimal place. + fn as_rounded_unit(self) -> f32 { + match self.unit { + // seconds/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, + } + } + fn unit(self) -> TimespanUnit { self.unit } @@ -173,16 +184,18 @@ mod test { fn answer_buttons() { let i18n = I18n::new(&["zz"], ""); assert_eq!(answer_button_time(30.0, &i18n), "30s"); - assert_eq!(answer_button_time(70.0, &i18n), "1m"); + assert_eq!(answer_button_time(70.0, &i18n), "1.2m"); assert_eq!(answer_button_time(1.1 * MONTH, &i18n), "1.1mo"); } #[test] fn time_spans() { let i18n = I18n::new(&["zz"], ""); - assert_eq!(time_span(1.0, &i18n), "1 second"); - assert_eq!(time_span(30.0, &i18n), "30 seconds"); - assert_eq!(time_span(90.0, &i18n), "1.5 minutes"); + 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"); } #[test]