52 lines
1.5 KiB
Rust
52 lines
1.5 KiB
Rust
|
|
//! Human time strings from ISO 8601 UTC timestamps.
|
||
|
|
//!
|
||
|
|
//! Used by the pretty renderer for "resets in 3h12m" and "in 47m". Parses are
|
||
|
|
//! lenient. A parse failure returns the raw string unchanged.
|
||
|
|
|
||
|
|
use chrono::{DateTime, Utc};
|
||
|
|
|
||
|
|
/// Relative phrasing of a future or past timestamp. Example: "in 3h12m" or
|
||
|
|
/// "47m ago". Falls back to the input on parse failure.
|
||
|
|
pub fn relative(iso: &str) -> String {
|
||
|
|
let Some(t) = parse(iso) else {
|
||
|
|
return iso.to_string();
|
||
|
|
};
|
||
|
|
let now = Utc::now();
|
||
|
|
let d = t.signed_duration_since(now);
|
||
|
|
let secs = d.num_seconds();
|
||
|
|
|
||
|
|
if secs.abs() < 60 {
|
||
|
|
if secs >= 0 {
|
||
|
|
"in <1m".to_string()
|
||
|
|
} else {
|
||
|
|
"<1m ago".to_string()
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
let h = secs / 3600;
|
||
|
|
let m = (secs % 3600) / 60;
|
||
|
|
let core = match (h.abs() > 0, m != 0) {
|
||
|
|
(true, true) => format!("{}h{}m", h.abs(), m.abs()),
|
||
|
|
(true, false) => format!("{}h", h.abs()),
|
||
|
|
(false, _) => format!("{}m", m.abs()),
|
||
|
|
};
|
||
|
|
if secs >= 0 {
|
||
|
|
format!("in {core}")
|
||
|
|
} else {
|
||
|
|
format!("{core} ago")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Absolute UTC string like "2025-08-31 17:00 UTC".
|
||
|
|
pub fn absolute(iso: &str) -> String {
|
||
|
|
let Some(t) = parse(iso) else {
|
||
|
|
return iso.to_string();
|
||
|
|
};
|
||
|
|
t.format("%Y-%m-%d %H:%M UTC").to_string()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse(iso: &str) -> Option<DateTime<Utc>> {
|
||
|
|
DateTime::parse_from_rfc3339(iso)
|
||
|
|
.ok()
|
||
|
|
.map(|dt| dt.with_timezone(&Utc))
|
||
|
|
}
|