| 1 | |
|
| 2 | |
|
| 3 | |
|
| 4 | | use time::format_description::well_known::Rfc3339; |
| 5 | | use time::OffsetDateTime; |
| 6 | |
|
| 7 | |
|
| 8 | 1488× | pub fn ago(when_ms: u64, now_ms: u64) -> String { |
| 9 | 1488× | if when_ms == 0 { |
| 10 | 24× | return "unknown".into(); |
| 11 | 1464× | } |
| 12 | 1464× | if when_ms > now_ms { |
| 13 | 3× | return "in the future".into(); |
| 14 | 1461× | } |
| 15 | 1461× | let s = (now_ms - when_ms) / 1000; |
| 16 | 1461× | let (n, unit) = if s < 45 { |
| 17 | 1431× | return "just now".into(); |
| 18 | 30× | } else if s < 90 { |
| 19 | 1× | (1, "minute") |
| 20 | 29× | } else if s < 3600 { |
| 21 | 1× | (s / 60, "minute") |
| 22 | 28× | } else if s < 86_400 { |
| 23 | 1× | (s / 3600, "hour") |
| 24 | 27× | } else if s < 86_400 * 30 { |
| 25 | 7× | (s / 86_400, "day") |
| 26 | 20× | } else if s < 86_400 * 365 { |
| 27 | 1× | (s / (86_400 * 30), "month") |
| 28 | | } else { |
| 29 | 19× | (s / (86_400 * 365), "year") |
| 30 | | }; |
| 31 | 30× | format!("{n} {unit}{} ago", if n == 1 { "" } else { "s" }) |
| 32 | 1488× | } |
| 33 | |
|
| 34 | |
|
| 35 | 1463× | pub fn absolute(when_ms: u64) -> String { |
| 36 | 1463× | match OffsetDateTime::from_unix_timestamp((when_ms / 1000) as i64) { |
| 37 | 1463× | Ok(t) => { |
| 38 | 1463× | let f = time::macros::format_description!("[year]-[month]-[day] [hour]:[minute] UTC"); |
| 39 | 1463× | t.format(&f).unwrap_or_default() |
| 40 | | } |
| 41 | 0× | Err(_) => String::new(), |
| 42 | | } |
| 43 | 1463× | } |
| 44 | |
|
| 45 | |
|
| 46 | 3× | pub fn day(when_ms: u64) -> String { |
| 47 | 3× | match OffsetDateTime::from_unix_timestamp((when_ms / 1000) as i64) { |
| 48 | 3× | Ok(t) => { |
| 49 | 3× | let f = time::macros::format_description!("[day padding:none] [month repr:short] [year]"); |
| 50 | 3× | t.format(&f).unwrap_or_default() |
| 51 | | } |
| 52 | 0× | Err(_) => String::new(), |
| 53 | | } |
| 54 | 3× | } |
| 55 | |
|
| 56 | 8× | pub fn rfc3339(when_ms: u64) -> String { |
| 57 | 8× | OffsetDateTime::from_unix_timestamp((when_ms / 1000) as i64).ok().and_then(|t| t.format(&Rfc3339).ok()).unwrap_or_default() |
| 58 | 8× | } |
| 59 | |
|
| 60 | | #[cfg(test)] |
| 61 | | mod tests { |
| 62 | | use super::*; |
| 63 | |
|
| 64 | | #[test] |
| 65 | 1× | fn days() { |
| 66 | 1× | assert_eq!(day(1_792_540_800_000), "21 Oct 2026"); |
| 67 | 1× | } |
| 68 | |
|
| 69 | | #[test] |
| 70 | 1× | fn relative() { |
| 71 | 1× | let now = 1_800_000_000_000; |
| 72 | 1× | assert_eq!(ago(now - 10_000, now), "just now"); |
| 73 | 1× | assert_eq!(ago(now - 60_000, now), "1 minute ago"); |
| 74 | 1× | assert_eq!(ago(now - 5 * 60_000, now), "5 minutes ago"); |
| 75 | 1× | assert_eq!(ago(now - 3 * 3_600_000, now), "3 hours ago"); |
| 76 | 1× | assert_eq!(ago(now - 2 * 86_400_000, now), "2 days ago"); |
| 77 | 1× | assert_eq!(ago(now - 400 * 86_400_000, now), "1 year ago"); |
| 78 | 1× | assert_eq!(ago(0, now), "unknown"); |
| 79 | 1× | assert_eq!(absolute(1_758_990_120_000), "2025-09-27 16:22 UTC"); |
| 80 | 1× | } |
| 81 | | } |