1use jiff::Timestamp;
9
10const MINUTE: i64 = 60;
11const HOUR: i64 = 60 * MINUTE;
12const DAY: i64 = 24 * HOUR;
13const MONTH: i64 = 30 * DAY;
14const YEAR: i64 = 365 * DAY;
15
16pub fn iso8601(unix_seconds: i64) -> String {
18 match Timestamp::from_second(unix_seconds) {
19 Ok(ts) => ts.strftime("%Y-%m-%dT%H:%M:%SZ").to_string(),
20 Err(_) => "(invalid time)".to_string(),
21 }
22}
23
24pub fn relative(now_unix: i64, then_unix: i64) -> String {
27 let secs = now_unix - then_unix;
28 if secs < MINUTE {
29 return "just now".to_string();
30 }
31 let (value, unit) = if secs < HOUR {
32 (secs / MINUTE, "m")
33 } else if secs < DAY {
34 (secs / HOUR, "h")
35 } else if secs < MONTH {
36 (secs / DAY, "d")
37 } else if secs < YEAR {
38 (secs / MONTH, "mo")
39 } else {
40 (secs / YEAR, "y")
41 };
42 format!("{value}{unit} ago")
43}
44
45pub fn now_unix() -> i64 {
47 Timestamp::now().as_second()
48}
49
50pub fn parse_iso8601(text: &str) -> Option<i64> {
53 text.parse::<Timestamp>().ok().map(|t| t.as_second())
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59
60 #[test]
61 fn iso8601_formats_known_instant() {
62 assert_eq!(iso8601(1_705_314_600), "2024-01-15T10:30:00Z");
64 assert_eq!(iso8601(0), "1970-01-01T00:00:00Z");
65 }
66
67 #[test]
68 fn relative_units_pick_largest() {
69 assert_eq!(relative(1_000, 1_000), "just now");
70 assert_eq!(relative(1_000 + 30, 1_000), "just now");
71 assert_eq!(relative(1_000 + 5 * MINUTE, 1_000), "5m ago");
72 assert_eq!(relative(1_000 + 3 * HOUR, 1_000), "3h ago");
73 assert_eq!(relative(1_000 + 2 * DAY, 1_000), "2d ago");
74 assert_eq!(relative(1_000 + 4 * MONTH, 1_000), "4mo ago");
75 assert_eq!(relative(1_000 + YEAR, 1_000), "1y ago");
76 assert_eq!(relative(1_000 + 3 * YEAR, 1_000), "3y ago");
77 }
78
79 #[test]
80 fn relative_boundaries() {
81 assert_eq!(relative(59, 0), "just now");
82 assert_eq!(relative(60, 0), "1m ago");
83 assert_eq!(relative(HOUR - 1, 0), "59m ago");
84 assert_eq!(relative(HOUR, 0), "1h ago");
85 assert_eq!(relative(DAY - 1, 0), "23h ago");
86 assert_eq!(relative(DAY, 0), "1d ago");
87 assert_eq!(relative(MONTH - 1, 0), "29d ago");
88 assert_eq!(relative(MONTH, 0), "1mo ago");
89 assert_eq!(relative(YEAR, 0), "1y ago");
90 }
91
92 #[test]
93 fn future_times_are_just_now() {
94 assert_eq!(relative(0, 1_000), "just now");
95 }
96
97 #[test]
98 fn now_unix_is_after_2020() {
99 assert!(now_unix() > 1_600_000_000);
100 }
101
102 #[test]
103 fn iso8601_round_trips_through_parse() {
104 for unix in [0, 1_705_314_600, 1_600_000_000] {
105 assert_eq!(parse_iso8601(&iso8601(unix)), Some(unix));
106 }
107 assert_eq!(parse_iso8601("not a timestamp"), None);
108 }
109}