Krolyxon
Tag v0.2.0

ferreniumsrcrendertime.rs

93 lines · 3.1 KB · Rust

1//! Human-readable times.
2
3use std::time::{SystemTime, UNIX_EPOCH};
4
5pub fn now() -> i64 {
6 SystemTime::now()
7 .duration_since(UNIX_EPOCH)
8 .map_or(0, |d| d.as_secs() as i64)
9}
10
11/// "3 hours ago", relative to `now` (both unix seconds). Future times
12/// (clock skew) read as "just now".
13pub fn relative(then: i64, now: i64) -> String {
14 const MIN: i64 = 60;
15 const HOUR: i64 = 60 * MIN;
16 const DAY: i64 = 24 * HOUR;
17 const WEEK: i64 = 7 * DAY;
18 const MONTH: i64 = 30 * DAY;
19 const YEAR: i64 = 365 * DAY;
20
21 let delta = now - then;
22 let (n, unit) = match delta {
23 d if d < MIN => return "just now".into(),
24 d if d < HOUR => (d / MIN, "minute"),
25 d if d < DAY => (d / HOUR, "hour"),
26 d if d < 2 * WEEK => (d / DAY, "day"),
27 d if d < 2 * MONTH => (d / WEEK, "week"),
28 d if d < YEAR => (d / MONTH, "month"),
29 d => (d / YEAR, "year"),
30 };
31 format!("{n} {unit}{} ago", if n == 1 { "" } else { "s" })
32}
33
34/// RFC 3339 UTC timestamp, for `<time datetime>` and tooltips.
35pub fn iso(then: i64) -> String {
36 jiff::Timestamp::from_second(then)
37 .map(|t| t.strftime("%Y-%m-%dT%H:%M:%SZ").to_string())
38 .unwrap_or_default()
39}
40
41fn zoned(then: i64, offset: i32) -> Option<jiff::Zoned> {
42 let offset = jiff::tz::Offset::from_seconds(offset).unwrap_or(jiff::tz::Offset::UTC);
43 let ts = jiff::Timestamp::from_second(then).ok()?;
44 Some(ts.to_zoned(jiff::tz::TimeZone::fixed(offset)))
45}
46
47/// `Thu, 5 Nov 2026 14:03:00 +0100` in the recorded offset (patch headers).
48pub fn rfc2822(then: i64, offset: i32) -> String {
49 zoned(then, offset)
50 .map(|z| z.strftime("%a, %-d %b %Y %H:%M:%S %z").to_string())
51 .unwrap_or_default()
52}
53
54/// `2026-11-05 14:03 +0100` in the recorded offset.
55pub fn local(then: i64, offset: i32) -> String {
56 zoned(then, offset)
57 .map(|z| z.strftime("%Y-%m-%d %H:%M %z").to_string())
58 .unwrap_or_default()
59}
60
61/// `Nov 5, 2026` (UTC), for grouping commits by day.
62pub fn day(then: i64) -> String {
63 jiff::Timestamp::from_second(then)
64 .map(|t| t.strftime("%b %-d, %Y").to_string())
65 .unwrap_or_default()
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn relative_times() {
74 let now = 1_000_000_000;
75 assert_eq!(relative(now + 5, now), "just now");
76 assert_eq!(relative(now - 59, now), "just now");
77 assert_eq!(relative(now - 60, now), "1 minute ago");
78 assert_eq!(relative(now - 2 * 3600, now), "2 hours ago");
79 assert_eq!(relative(now - 86400, now), "1 day ago");
80 assert_eq!(relative(now - 13 * 86400, now), "13 days ago");
81 assert_eq!(relative(now - 15 * 86400, now), "2 weeks ago");
82 assert_eq!(relative(now - 90 * 86400, now), "3 months ago");
83 assert_eq!(relative(now - 800 * 86400, now), "2 years ago");
84 }
85
86 #[test]
87 fn iso_format() {
88 assert_eq!(iso(0), "1970-01-01T00:00:00Z");
89 assert_eq!(rfc2822(0, 3600), "Thu, 1 Jan 1970 01:00:00 +0100");
90 assert_eq!(local(0, -7200), "1969-12-31 22:00 -0200");
91 assert_eq!(day(86400 * 5), "Jan 6, 1970");
92 }
93}