time: more time-ago granularity in months/years

before: 1y
after:  1y 8mo

etc

Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
William Casarin
2025-09-10 16:40:13 -07:00
parent 02a90eccd1
commit 9278c90802

View File

@@ -10,73 +10,75 @@ const ONE_WEEK_IN_SECONDS: u64 = 604_800;
const ONE_MONTH_IN_SECONDS: u64 = 2_592_000; // 30 days
const ONE_YEAR_IN_SECONDS: u64 = 31_536_000; // 365 days
// Range boundary constants for match patterns
const MAX_SECONDS: u64 = ONE_MINUTE_IN_SECONDS - 1;
const MAX_SECONDS_FOR_MINUTES: u64 = ONE_HOUR_IN_SECONDS - 1;
const MAX_SECONDS_FOR_HOURS: u64 = ONE_DAY_IN_SECONDS - 1;
const MAX_SECONDS_FOR_DAYS: u64 = ONE_WEEK_IN_SECONDS - 1;
const MAX_SECONDS_FOR_WEEKS: u64 = ONE_MONTH_IN_SECONDS - 1;
const MAX_SECONDS_FOR_MONTHS: u64 = ONE_YEAR_IN_SECONDS - 1;
/// Calculate relative time between two timestamps
/// Calculate relative time between two timestamps, with two units only
/// when the scale is large enough (e.g., "1y 6m", "5d 4h"),
/// but not for hours/minutes/seconds.
fn time_ago_between(i18n: &mut Localization, timestamp: u64, now: u64) -> String {
// Determine if the timestamp is in the future or the past
let duration = if now >= timestamp {
now.saturating_sub(timestamp)
} else {
timestamp.saturating_sub(now)
};
let time_str = match duration {
0..=2 => tr!(
// Special-case: "now" for < 3 seconds
if duration <= 2 {
let s = tr!(
i18n,
"now",
"Relative time for very recent events (less than 3 seconds)"
),
3..=MAX_SECONDS => tr!(
i18n,
"{count}s",
"Relative time in seconds",
count = duration
),
ONE_MINUTE_IN_SECONDS..=MAX_SECONDS_FOR_MINUTES => tr!(
i18n,
"{count}m",
"Relative time in minutes",
count = duration / ONE_MINUTE_IN_SECONDS
),
ONE_HOUR_IN_SECONDS..=MAX_SECONDS_FOR_HOURS => tr!(
i18n,
"{count}h",
"Relative time in hours",
count = duration / ONE_HOUR_IN_SECONDS
),
ONE_DAY_IN_SECONDS..=MAX_SECONDS_FOR_DAYS => tr!(
i18n,
"{count}d",
"Relative time in days",
count = duration / ONE_DAY_IN_SECONDS
),
ONE_WEEK_IN_SECONDS..=MAX_SECONDS_FOR_WEEKS => tr!(
i18n,
"{count}w",
"Relative time in weeks",
count = duration / ONE_WEEK_IN_SECONDS
),
ONE_MONTH_IN_SECONDS..=MAX_SECONDS_FOR_MONTHS => tr!(
i18n,
"{count}mo",
"Relative time in months",
count = duration / ONE_MONTH_IN_SECONDS
),
_ => tr!(
i18n,
"{count}y",
"Relative time in years",
count = duration / ONE_YEAR_IN_SECONDS
),
);
return if timestamp > now { format!("+{s}") } else { s };
}
// Break into buckets
let years = duration / ONE_YEAR_IN_SECONDS;
let rem_y = duration % ONE_YEAR_IN_SECONDS;
let months = rem_y / ONE_MONTH_IN_SECONDS;
let rem_m = rem_y % ONE_MONTH_IN_SECONDS;
let weeks = rem_m / ONE_WEEK_IN_SECONDS;
let rem_w = rem_m % ONE_WEEK_IN_SECONDS;
let days = rem_w / ONE_DAY_IN_SECONDS;
let rem_d = rem_w % ONE_DAY_IN_SECONDS;
let hours = rem_d / ONE_HOUR_IN_SECONDS;
let rem_h = rem_d % ONE_HOUR_IN_SECONDS;
let mins = rem_h / ONE_MINUTE_IN_SECONDS;
let secs = rem_h % ONE_MINUTE_IN_SECONDS;
let mut parts: Vec<String> = Vec::with_capacity(2);
let mut push_part = |count: u64, key: &str, desc: &str| {
if count > 0 && parts.len() < 2 {
parts.push(tr!(i18n, key, desc, count = count));
}
};
if years > 0 {
push_part(years, "{count}y", "Relative time in years");
push_part(months, "{count}mo", "Relative time in months");
} else if months > 0 {
push_part(months, "{count}mo", "Relative time in months");
push_part(weeks, "{count}w", "Relative time in weeks");
} else if weeks > 0 {
push_part(weeks, "{count}w", "Relative time in weeks");
push_part(days, "{count}d", "Relative time in days");
} else if days > 0 {
push_part(days, "{count}d", "Relative time in days");
push_part(hours, "{count}h", "Relative time in hours");
} else if hours > 0 {
push_part(hours, "{count}h", "Relative time in hours");
} else if mins > 0 {
push_part(mins, "{count}m", "Relative time in minutes");
} else {
push_part(secs.max(1), "{count}s", "Relative time in seconds");
}
let time_str = parts.join(" ");
if timestamp > now {
format!("+{time_str}")
} else {