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]