use std::time::Duration;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
pub fn rel_time(d: Duration) -> String {
let s = d.as_secs();
if s < 60 {
format!("{s}s")
} else if s < 3600 {
format!("{}m", s / 60)
} else if s < 86_400 {
format!("{}h", s / 3600)
} else {
format!("{}d", s / 86_400)
}
}
pub fn bytes(n: usize) -> String {
if n < 1024 {
format!("{n} B")
} else if n < 1024 * 1024 {
format!("{} KiB", n / 1024)
} else {
format!("{} MiB", n / (1024 * 1024))
}
}
pub fn truncate(s: &str, max: usize) -> String {
if max == 0 {
return String::new();
}
let clean: String = s
.chars()
.map(|c| if c.is_control() { ' ' } else { c })
.collect();
if clean.width() <= max {
return clean;
}
let budget = max - 1;
let mut used = 0;
let mut out = String::new();
for c in clean.chars() {
let w = c.width().unwrap_or(0);
if used + w > budget {
break;
}
used += w;
out.push(c);
}
out.push('…');
out
}
pub fn pad(s: &str, width: usize) -> String {
let mut t = truncate(s, width);
let w = t.width();
if w < width {
t.extend(std::iter::repeat_n(' ', width - w));
}
t
}
pub(crate) fn civil_from_days(days: i64) -> (i64, u32, u32) {
let z = days + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - era * 146_097; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365; let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = (doy - (153 * mp + 2) / 5 + 1) as u32; let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32; (yoe + era * 400 + i64::from(m <= 2), m, d)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn truncate_ascii_is_unchanged() {
assert_eq!(truncate("hello", 5), "hello");
assert_eq!(truncate("hello world", 8), "hello w…");
assert_eq!(truncate("hi", 0), "");
}
#[test]
fn truncate_flattens_control_chars() {
assert_eq!(truncate("a\x1b[31mb\nc", 20), "a [31mb c");
assert_eq!(truncate("x\ty", 3), "x y");
}
#[test]
fn truncate_counts_cjk_as_two_columns() {
let t = truncate("日本語のテスト", 8);
assert_eq!(t, "日本語…");
assert!(t.width() <= 8, "width {} overflows", t.width());
assert!(t.ends_with('…'));
}
#[test]
fn truncate_drops_a_straddling_wide_glyph() {
let t = truncate("ab日本", 4);
assert_eq!(t, "ab…");
assert_eq!(t.width(), 3); }
#[test]
fn truncate_counts_emoji_as_two_columns() {
assert_eq!(truncate("😀", 2), "😀"); assert_eq!(truncate("😀😀😀", 4), "😀…"); }
#[test]
fn truncate_counts_combining_marks_as_zero() {
assert_eq!(truncate("e\u{0301}", 1), "e\u{0301}");
}
#[test]
fn pad_measures_display_width() {
assert_eq!(pad("hi", 4), "hi ");
let p = pad("日本", 5);
assert_eq!(p, "日本 ");
assert_eq!(p.width(), 5);
let p = pad("日本語のテスト", 8);
assert_eq!(p, "日本語… ");
assert_eq!(p.width(), 8);
assert_eq!(pad("e\u{0301}", 3), "e\u{0301} ");
}
#[test]
fn civil_from_days_matches_known_dates() {
assert_eq!(civil_from_days(0), (1970, 1, 1));
assert_eq!(civil_from_days(19_723), (2024, 1, 1)); assert_eq!(civil_from_days(19_782), (2024, 2, 29)); assert_eq!(civil_from_days(20_648), (2026, 7, 14));
assert_eq!(civil_from_days(-1), (1969, 12, 31));
}
}