Skip to main content

codoseo_web/
fmt.rs

1//! Display formatting shared by every screen: thousands separators, "12m ago", durations.
2
3use time::{Duration, OffsetDateTime};
4
5/// `1284` -> `1,284`
6pub fn thousands(n: impl Into<i64>) -> String {
7    let n: i64 = n.into();
8    let digits = n.unsigned_abs().to_string();
9    let mut out = String::with_capacity(digits.len() + digits.len() / 3 + 1);
10    if n < 0 {
11        out.push('-');
12    }
13    for (i, c) in digits.chars().enumerate() {
14        if i > 0 && (digits.len() - i).is_multiple_of(3) {
15            out.push(',');
16        }
17        out.push(c);
18    }
19    out
20}
21
22/// A short "how long ago": `just now`, `12m ago`, `3h ago`, `2d ago`, then a date.
23pub fn ago(at: OffsetDateTime) -> String {
24    ago_from(at, OffsetDateTime::now_utc())
25}
26
27pub fn ago_from(at: OffsetDateTime, now: OffsetDateTime) -> String {
28    let secs = (now - at).whole_seconds();
29    match secs {
30        s if s < 45 => "just now".to_owned(),
31        s if s < 3600 => format!("{}m ago", (s + 30) / 60),
32        s if s < 86_400 => format!("{}h ago", s / 3600),
33        s if s < 7 * 86_400 => format!("{}d ago", s / 86_400),
34        _ => date(at),
35    }
36}
37
38/// `Oct 3`
39pub fn date(at: OffsetDateTime) -> String {
40    const MONTHS: [&str; 12] = [
41        "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
42    ];
43    format!("{} {}", MONTHS[at.month() as usize - 1], at.day())
44}
45
46/// `Oct 3, 14:05 UTC`
47pub fn datetime(at: OffsetDateTime) -> String {
48    format!("{}, {:02}:{:02} UTC", date(at), at.hour(), at.minute())
49}
50
51/// `42s`, `1m 42s`, `2h 05m`
52pub fn duration(d: Duration) -> String {
53    let s = d.whole_seconds().max(0);
54    match s {
55        s if s < 60 => format!("{s}s"),
56        s if s < 3600 => format!("{}m {:02}s", s / 60, s % 60),
57        s => format!("{}h {:02}m", s / 3600, (s % 3600) / 60),
58    }
59}
60
61/// `duration` for a millisecond count.
62pub fn millis(ms: u64) -> String {
63    duration(Duration::milliseconds(ms.min(i64::MAX as u64) as i64))
64}
65
66/// One decimal place percentage of `part / whole`: `90.2`.
67pub fn pct1(part: u64, whole: u64) -> String {
68    if whole == 0 {
69        return "0".to_owned();
70    }
71    let tenths = (part * 1000 + whole / 2) / whole;
72    format!("{}.{}", tenths / 10, tenths % 10)
73}
74
75/// `66.7%`, or `100%` without a pointless `.0`.
76pub fn percent(part: i64, whole: i64) -> String {
77    let p = pct1(part.max(0) as u64, whole.max(0) as u64);
78    format!("{}%", p.strip_suffix(".0").unwrap_or(&p))
79}
80
81/// `1 page`, `1,284 pages`.
82pub fn pages(n: u32) -> String {
83    format!("{} page{}", thousands(n), if n == 1 { "" } else { "s" })
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn thousands_separators() {
92        assert_eq!(thousands(0), "0");
93        assert_eq!(thousands(999), "999");
94        assert_eq!(thousands(1284), "1,284");
95        assert_eq!(thousands(1_234_567), "1,234,567");
96        assert_eq!(thousands(-50_000), "-50,000");
97    }
98
99    #[test]
100    fn ago_buckets() {
101        let now = OffsetDateTime::UNIX_EPOCH + Duration::days(400);
102        assert_eq!(ago_from(now - Duration::seconds(10), now), "just now");
103        assert_eq!(ago_from(now - Duration::minutes(12), now), "12m ago");
104        assert_eq!(ago_from(now - Duration::hours(3), now), "3h ago");
105        assert_eq!(ago_from(now - Duration::days(2), now), "2d ago");
106        assert!(ago_from(now - Duration::days(30), now).contains(' '));
107    }
108
109    #[test]
110    fn durations() {
111        assert_eq!(duration(Duration::seconds(42)), "42s");
112        assert_eq!(duration(Duration::seconds(102)), "1m 42s");
113        assert_eq!(duration(Duration::seconds(7500)), "2h 05m");
114        assert_eq!(pct1(1158, 1284), "90.2");
115        assert_eq!(percent(2, 3), "66.7%");
116        assert_eq!(percent(5, 5), "100%");
117        assert_eq!(percent(0, 0), "0%");
118        assert_eq!(pages(1), "1 page");
119        assert_eq!(pages(1284), "1,284 pages");
120        assert_eq!(pct1(1, 0), "0");
121    }
122}