diff --git a/proto/backend.proto b/proto/backend.proto
index 445aa233b..fd17b2f3a 100644
--- a/proto/backend.proto
+++ b/proto/backend.proto
@@ -308,7 +308,8 @@ message TranslateArgValue {
message FormatTimeSpanIn {
enum Context {
- ANSWER_BUTTONS = 0;
+ NORMAL = 0;
+ ANSWER_BUTTONS = 1;
}
float seconds = 1;
diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py
index a9154cb73..290b63e3a 100644
--- a/pylib/anki/rsbackend.py
+++ b/pylib/anki/rsbackend.py
@@ -345,7 +345,11 @@ class RustBackend:
)
).translate_string
- def format_time_span(self, seconds: float, context: FormatTimeSpanContext) -> str:
+ def format_time_span(
+ self,
+ seconds: float,
+ context: FormatTimeSpanContext = FormatTimeSpanContext.NORMAL,
+ ) -> str:
return self._run_command(
pb.BackendInput(
format_time_span=pb.FormatTimeSpanIn(seconds=seconds, context=context)
diff --git a/pylib/anki/stats.py b/pylib/anki/stats.py
index 38eb62818..d7fcf285c 100644
--- a/pylib/anki/stats.py
+++ b/pylib/anki/stats.py
@@ -28,8 +28,6 @@ class CardStats:
def report(self) -> str:
c = self.card
- # pylint: disable=unnecessary-lambda
- fmt = lambda x, **kwargs: fmtTimeSpan(x, short=True, **kwargs)
self.txt = "
"
self.addLine(_("Added"), self.date(c.id / 1000))
first = self.col.db.scalar("select min(id) from revlog where cid = ?", c.id)
@@ -52,7 +50,9 @@ class CardStats:
next,
)
if c.queue == QUEUE_TYPE_REV:
- self.addLine(_("Interval"), fmt(c.ivl * 86400))
+ self.addLine(
+ _("Interval"), self.col.backend.format_time_span(c.ivl * 86400)
+ )
self.addLine(_("Ease"), "%d%%" % (c.factor / 10.0))
self.addLine(_("Reviews"), "%d" % c.reps)
self.addLine(_("Lapses"), "%d" % c.lapses)
@@ -83,13 +83,8 @@ class CardStats:
def date(self, tm) -> str:
return time.strftime("%Y-%m-%d", time.localtime(tm))
- def time(self, tm) -> str:
- s = ""
- if tm >= 60:
- s = fmtTimeSpan((tm / 60) * 60, short=True, point=-1, unit=1)
- if tm % 60 != 0 or not s:
- s += fmtTimeSpan(tm % 60, point=2 if not s else -1, short=True)
- return s
+ def time(self, tm: float) -> str:
+ return self.col.backend.format_time_span(tm)
# Collection stats
diff --git a/pylib/anki/utils.py b/pylib/anki/utils.py
index 76cc31610..e2c1ed78f 100644
--- a/pylib/anki/utils.py
+++ b/pylib/anki/utils.py
@@ -24,7 +24,7 @@ from html.entities import name2codepoint
from typing import Iterable, Iterator, List, Optional, Tuple, Union
from anki.db import DB
-from anki.lang import _, ngettext
+from anki.lang import ngettext
_tmpdir: Optional[str]
@@ -56,28 +56,10 @@ inTimeTable = {
}
-def shortTimeFmt(type: str) -> str:
- return {
- # T: year is an abbreviation for year. %s is a number of years
- "years": _("%sy"),
- # T: m is an abbreviation for month. %s is a number of months
- "months": _("%smo"),
- # T: d is an abbreviation for day. %s is a number of days
- "days": _("%sd"),
- # T: h is an abbreviation for hour. %s is a number of hours
- "hours": _("%sh"),
- # T: m is an abbreviation for minute. %s is a number of minutes
- "minutes": _("%sm"),
- # T: s is an abbreviation for second. %s is a number of seconds
- "seconds": _("%ss"),
- }[type]
-
-
def fmtTimeSpan(
time: Union[int, float],
pad: int = 0,
point: int = 0,
- short: bool = False,
inTime: bool = False,
unit: int = 99,
) -> str:
@@ -86,13 +68,10 @@ def fmtTimeSpan(
time = convertSecondsTo(time, type)
if not point:
time = int(round(time))
- if short:
- fmt = shortTimeFmt(type)
+ if inTime:
+ fmt = inTimeTable[type](_pluralCount(time, point))
else:
- if inTime:
- fmt = inTimeTable[type](_pluralCount(time, point))
- else:
- fmt = timeTable[type](_pluralCount(time, point))
+ fmt = timeTable[type](_pluralCount(time, point))
timestr = "%%%(a)d.%(b)df" % {"a": pad, "b": point}
return locale.format_string(fmt % timestr, time)
diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py
index 301683d95..8a3806954 100644
--- a/qt/aqt/browser.py
+++ b/qt/aqt/browser.py
@@ -1451,13 +1451,10 @@ border: 1px solid #000; padding: 3px; '>%s"""
if not entries:
return ""
s = "%s | " % _("Date")
- s += ("%s | " * 5) % (
- _("Type"),
- _("Rating"),
- _("Interval"),
- _("Ease"),
- _("Time"),
- )
+ s += "%s | " % _("Type")
+ s += "%s | " % _("Rating")
+ s += "%s | " % _("Interval")
+ s += ("%s | " * 2) % (_("Ease"), _("Time"),)
cnt = 0
for (date, ease, ivl, factor, taken, type) in reversed(entries):
cnt += 1
@@ -1485,13 +1482,14 @@ border: 1px solid #000; padding: 3px; '>%s"""
if ivl == 0:
ivl = _("0d")
elif ivl > 0:
- ivl = fmtTimeSpan(ivl * 86400, short=True)
+ ivl = fmtTimeSpan(ivl * 86400)
else:
ivl = cs.time(-ivl)
- s += ("%s | " * 5) % (
- tstr,
- ease,
- ivl,
+ s += "%s | " % tstr
+ s += "%s | " % ease
+ s += "%s | " % ivl
+
+ s += ("%s | " * 2) % (
"%d%%" % (factor / 10) if factor else "",
cs.time(taken),
) + "
"
diff --git a/rslib/src/backend.rs b/rslib/src/backend.rs
index e4c1420fc..3aee1239e 100644
--- a/rslib/src/backend.rs
+++ b/rslib/src/backend.rs
@@ -11,7 +11,7 @@ use crate::media::check::MediaChecker;
use crate::media::sync::MediaSyncProgress;
use crate::media::MediaManager;
use crate::sched::cutoff::{local_minutes_west_for_stamp, sched_timing_today};
-use crate::sched::timespan::answer_button_time;
+use crate::sched::timespan::{answer_button_time, time_span};
use crate::template::{
render_card, without_legacy_template_directives, FieldMap, FieldRequirements, ParsedTemplate,
RenderedNode,
@@ -406,6 +406,7 @@ 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::AnswerButtons => {
answer_button_time(input.seconds, &self.i18n)
}
diff --git a/rslib/src/i18n/scheduling.ftl b/rslib/src/i18n/scheduling.ftl
index 5f735228f..2220368e3 100644
--- a/rslib/src/i18n/scheduling.ftl
+++ b/rslib/src/i18n/scheduling.ftl
@@ -9,3 +9,31 @@ answer-button-time-hours = {$amount}h
answer-button-time-days = {$amount}d
answer-button-time-months = {$amount}mo
answer-button-time-years = {$amount}y
+
+## A span of time, such as the delay until a card is shown again, the
+## amount of time taken to answer a card, and so on.
+
+time-span-seconds = { $amount ->
+ [one] {$amount} second
+ *[other] {$amount} seconds
+ }
+time-span-minutes = { $amount ->
+ [one] {$amount} minute
+ *[other] {$amount} minutes
+ }
+time-span-hours = { $amount ->
+ [one] {$amount} hour
+ *[other] {$amount} hours
+ }
+time-span-days = { $amount ->
+ [one] {$amount} day
+ *[other] {$amount} days
+ }
+time-span-months = { $amount ->
+ [one] {$amount} month
+ *[other] {$amount} months
+ }
+time-span-years = { $amount ->
+ [one] {$amount} year
+ *[other] {$amount} years
+ }
diff --git a/rslib/src/sched/timespan.rs b/rslib/src/sched/timespan.rs
index d85f67fb0..b70458c99 100644
--- a/rslib/src/sched/timespan.rs
+++ b/rslib/src/sched/timespan.rs
@@ -3,11 +3,12 @@
use crate::i18n::{tr_args, I18n, StringsGroup};
+/// 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() {
TimespanUnit::Months | TimespanUnit::Years => span.as_unit(),
- // we don't show fractional value except for months/years
+ // we don't show fractional values except for months/years
_ => span.as_unit().round(),
};
let unit = span.unit().as_str();
@@ -16,6 +17,17 @@ pub fn answer_button_time(seconds: f32, i18n: &I18n) -> String {
.trn(&format!("answer-button-time-{}", unit), args)
}
+/// Describe the given seconds using the largest appropriate unit
+/// eg 70 seconds -> "1.17 minutes"
+pub fn time_span(seconds: f32, i18n: &I18n) -> String {
+ let span = Timespan::from_secs(seconds).natural_span();
+ let amount = span.as_unit();
+ let unit = span.unit().as_str();
+ let args = tr_args!["amount" => amount];
+ i18n.get(StringsGroup::Scheduling)
+ .trn(&format!("time-span-{}", unit), args)
+}
+
const SECOND: f32 = 1.0;
const MINUTE: f32 = 60.0 * SECOND;
const HOUR: f32 = 60.0 * MINUTE;