use time::{Duration, OffsetDateTime};
pub fn thousands(n: impl Into<i64>) -> String {
let n: i64 = n.into();
let digits = n.unsigned_abs().to_string();
let mut out = String::with_capacity(digits.len() + digits.len() / 3 + 1);
if n < 0 {
out.push('-');
}
for (i, c) in digits.chars().enumerate() {
if i > 0 && (digits.len() - i).is_multiple_of(3) {
out.push(',');
}
out.push(c);
}
out
}
pub fn ago(at: OffsetDateTime) -> String {
ago_from(at, OffsetDateTime::now_utc())
}
pub fn ago_from(at: OffsetDateTime, now: OffsetDateTime) -> String {
let secs = (now - at).whole_seconds();
match secs {
s if s < 45 => "just now".to_owned(),
s if s < 3600 => format!("{}m ago", (s + 30) / 60),
s if s < 86_400 => format!("{}h ago", s / 3600),
s if s < 7 * 86_400 => format!("{}d ago", s / 86_400),
_ => date(at),
}
}
pub fn date(at: OffsetDateTime) -> String {
const MONTHS: [&str; 12] = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
format!("{} {}", MONTHS[at.month() as usize - 1], at.day())
}
pub fn datetime(at: OffsetDateTime) -> String {
format!("{}, {:02}:{:02} UTC", date(at), at.hour(), at.minute())
}
pub fn duration(d: Duration) -> String {
let s = d.whole_seconds().max(0);
match s {
s if s < 60 => format!("{s}s"),
s if s < 3600 => format!("{}m {:02}s", s / 60, s % 60),
s => format!("{}h {:02}m", s / 3600, (s % 3600) / 60),
}
}
pub fn millis(ms: u64) -> String {
duration(Duration::milliseconds(ms.min(i64::MAX as u64) as i64))
}
pub fn pct1(part: u64, whole: u64) -> String {
if whole == 0 {
return "0".to_owned();
}
let tenths = (part * 1000 + whole / 2) / whole;
format!("{}.{}", tenths / 10, tenths % 10)
}
pub fn percent(part: i64, whole: i64) -> String {
let p = pct1(part.max(0) as u64, whole.max(0) as u64);
format!("{}%", p.strip_suffix(".0").unwrap_or(&p))
}
pub fn pages(n: u32) -> String {
format!("{} page{}", thousands(n), if n == 1 { "" } else { "s" })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn thousands_separators() {
assert_eq!(thousands(0), "0");
assert_eq!(thousands(999), "999");
assert_eq!(thousands(1284), "1,284");
assert_eq!(thousands(1_234_567), "1,234,567");
assert_eq!(thousands(-50_000), "-50,000");
}
#[test]
fn ago_buckets() {
let now = OffsetDateTime::UNIX_EPOCH + Duration::days(400);
assert_eq!(ago_from(now - Duration::seconds(10), now), "just now");
assert_eq!(ago_from(now - Duration::minutes(12), now), "12m ago");
assert_eq!(ago_from(now - Duration::hours(3), now), "3h ago");
assert_eq!(ago_from(now - Duration::days(2), now), "2d ago");
assert!(ago_from(now - Duration::days(30), now).contains(' '));
}
#[test]
fn durations() {
assert_eq!(duration(Duration::seconds(42)), "42s");
assert_eq!(duration(Duration::seconds(102)), "1m 42s");
assert_eq!(duration(Duration::seconds(7500)), "2h 05m");
assert_eq!(pct1(1158, 1284), "90.2");
assert_eq!(percent(2, 3), "66.7%");
assert_eq!(percent(5, 5), "100%");
assert_eq!(percent(0, 0), "0%");
assert_eq!(pages(1), "1 page");
assert_eq!(pages(1284), "1,284 pages");
assert_eq!(pct1(1, 0), "0");
}
}