use std::time::Duration;
struct DurationParts {
days: u64,
hours: u64,
minutes: u64,
seconds: u64,
}
impl DurationParts {
fn from_seconds(seconds: u64) -> Self {
Self {
days: seconds / 86400,
hours: (seconds % 86400) / 3600,
minutes: (seconds % 3600) / 60,
seconds: seconds % 60,
}
}
}
pub fn format_duration(seconds: u64) -> String {
if seconds == 0 {
return "0 seconds".to_string();
}
let p = DurationParts::from_seconds(seconds);
let mut parts = Vec::new();
if p.days > 0 {
parts.push(format!(
"{} {}",
p.days,
if p.days == 1 { "day" } else { "days" }
));
}
if p.hours > 0 {
parts.push(format!(
"{} {}",
p.hours,
if p.hours == 1 { "hour" } else { "hours" }
));
}
if p.minutes > 0 {
parts.push(format!(
"{} {}",
p.minutes,
if p.minutes == 1 { "minute" } else { "minutes" }
));
}
if p.seconds > 0 || parts.is_empty() {
parts.push(format!(
"{} {}",
p.seconds,
if p.seconds == 1 { "second" } else { "seconds" }
));
}
parts.join(", ")
}
pub fn format_duration_short(seconds: u64) -> String {
if seconds == 0 {
return "0s".to_string();
}
let p = DurationParts::from_seconds(seconds);
let mut parts = Vec::new();
if p.days > 0 {
parts.push(format!("{}d", p.days));
}
if p.hours > 0 {
parts.push(format!("{}h", p.hours));
}
if p.minutes > 0 {
parts.push(format!("{}m", p.minutes));
}
if p.seconds > 0 || parts.is_empty() {
parts.push(format!("{}s", p.seconds));
}
parts.join(" ")
}
pub fn format_duration_compact(seconds: u64) -> String {
if seconds == 0 {
return "0s".to_string();
}
let p = DurationParts::from_seconds(seconds);
if p.days > 0 {
format!("{}d", p.days)
} else if p.hours > 0 {
format!("{}h", p.hours)
} else if p.minutes > 0 {
format!("{}m", p.minutes)
} else {
format!("{}s", p.seconds)
}
}
pub fn format_std_duration(duration: Duration) -> String {
format_duration(duration.as_secs())
}
pub fn format_std_duration_short(duration: Duration) -> String {
format_duration_short(duration.as_secs())
}
pub fn format_relative_time(seconds_ago: u64) -> String {
if seconds_ago < 60 {
return "just now".to_string();
}
let minutes = seconds_ago / 60;
if minutes < 60 {
return format!(
"{} {} ago",
minutes,
if minutes == 1 { "minute" } else { "minutes" }
);
}
let hours = seconds_ago / 3600;
if hours < 24 {
return format!(
"{} {} ago",
hours,
if hours == 1 { "hour" } else { "hours" }
);
}
let days = seconds_ago / 86400;
if days < 7 {
return format!("{} {} ago", days, if days == 1 { "day" } else { "days" });
}
let weeks = days / 7;
if weeks < 4 {
return format!(
"{} {} ago",
weeks,
if weeks == 1 { "week" } else { "weeks" }
);
}
let months = days / 30;
if months < 12 {
return format!(
"{} {} ago",
months,
if months == 1 { "month" } else { "months" }
);
}
let years = days / 365;
format!(
"{} {} ago",
years,
if years == 1 { "year" } else { "years" }
)
}
pub fn format_relative_time_short(seconds_ago: u64) -> String {
if seconds_ago < 60 {
return "now".to_string();
}
let minutes = seconds_ago / 60;
if minutes < 60 {
return format!("{}m ago", minutes);
}
let hours = seconds_ago / 3600;
if hours < 24 {
return format!("{}h ago", hours);
}
let days = seconds_ago / 86400;
if days < 7 {
return format!("{}d ago", days);
}
let weeks = days / 7;
if weeks < 4 {
return format!("{}w ago", weeks);
}
let months = days / 30;
if months < 12 {
return format!("{}mo ago", months);
}
let years = days / 365;
format!("{}y ago", years)
}
pub fn format_size(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
const TB: u64 = GB * 1024;
const PB: u64 = TB * 1024;
if bytes < KB {
format!("{} B", bytes)
} else if bytes < MB {
format!("{:.1} KB", bytes as f64 / KB as f64)
} else if bytes < GB {
format!("{:.1} MB", bytes as f64 / MB as f64)
} else if bytes < TB {
format!("{:.1} GB", bytes as f64 / GB as f64)
} else if bytes < PB {
format!("{:.1} TB", bytes as f64 / TB as f64)
} else {
format!("{:.1} PB", bytes as f64 / PB as f64)
}
}
pub fn format_size_si(bytes: u64) -> String {
const KB: u64 = 1000;
const MB: u64 = KB * 1000;
const GB: u64 = MB * 1000;
const TB: u64 = GB * 1000;
const PB: u64 = TB * 1000;
if bytes < KB {
format!("{} B", bytes)
} else if bytes < MB {
format!("{:.1} kB", bytes as f64 / KB as f64)
} else if bytes < GB {
format!("{:.1} MB", bytes as f64 / MB as f64)
} else if bytes < TB {
format!("{:.1} GB", bytes as f64 / GB as f64)
} else if bytes < PB {
format!("{:.1} TB", bytes as f64 / TB as f64)
} else {
format!("{:.1} PB", bytes as f64 / PB as f64)
}
}
pub fn format_size_compact(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
const TB: u64 = GB * 1024;
if bytes < KB {
format!("{}", bytes)
} else if bytes < MB {
let val = bytes as f64 / KB as f64;
if val == val.trunc() {
format!("{}K", val as u64)
} else {
format!("{:.1}K", val)
}
} else if bytes < GB {
let val = bytes as f64 / MB as f64;
if val == val.trunc() {
format!("{}M", val as u64)
} else {
format!("{:.1}M", val)
}
} else if bytes < TB {
let val = bytes as f64 / GB as f64;
if val == val.trunc() {
format!("{}G", val as u64)
} else {
format!("{:.1}G", val)
}
} else {
let val = bytes as f64 / TB as f64;
if val == val.trunc() {
format!("{}T", val as u64)
} else {
format!("{:.1}T", val)
}
}
}
pub fn format_rate(bytes_per_second: u64) -> String {
format!("{}/s", format_size(bytes_per_second))
}
pub fn format_rate_compact(bytes_per_second: u64) -> String {
format!("{}/s", format_size_compact(bytes_per_second))
}
pub fn format_number(n: u64) -> String {
let s = n.to_string();
let mut result = String::new();
let chars: Vec<char> = s.chars().collect();
for (i, ch) in chars.iter().enumerate() {
if i > 0 && (chars.len() - i).is_multiple_of(3) {
result.push(',');
}
result.push(*ch);
}
result
}
pub fn format_number_short(n: u64) -> String {
if n < 1000 {
n.to_string()
} else if n < 1_000_000 {
let val = n as f64 / 1000.0;
if val == val.trunc() {
format!("{}K", val as u64)
} else {
format!("{:.1}K", val)
}
} else if n < 1_000_000_000 {
let val = n as f64 / 1_000_000.0;
if val == val.trunc() {
format!("{}M", val as u64)
} else {
format!("{:.1}M", val)
}
} else {
let val = n as f64 / 1_000_000_000.0;
if val == val.trunc() {
format!("{}B", val as u64)
} else {
format!("{:.1}B", val)
}
}
}
pub fn format_percent(ratio: f64) -> String {
let percent = (ratio * 100.0).round();
let clamped = percent.clamp(i32::MIN as f64, i32::MAX as f64) as i32;
format!("{}%", clamped)
}
pub fn format_percent_precise(ratio: f64, decimals: usize) -> String {
format!("{:.1$}%", ratio * 100.0, decimals)
}
pub fn pluralize(count: u64, singular: &str, plural: &str) -> String {
if count == 1 {
format!("{} {}", count, singular)
} else {
format!("{} {}", count, plural)
}
}
pub fn pluralize_s(count: u64, word: &str) -> String {
if count == 1 {
format!("{} {}", count, word)
} else {
format!("{} {}s", count, word)
}
}
pub fn ordinal(n: u64) -> String {
let suffix = match (n % 10, n % 100) {
(1, 11) => "th",
(2, 12) => "th",
(3, 13) => "th",
(1, _) => "st",
(2, _) => "nd",
(3, _) => "rd",
_ => "th",
};
format!("{}{}", n, suffix)
}