use std::process::Command;
use std::sync::OnceLock;
fn local_utc_offset_secs() -> i64 {
static OFFSET: OnceLock<i64> = OnceLock::new();
*OFFSET.get_or_init(|| {
Command::new("date")
.arg("+%z")
.output()
.ok()
.and_then(|output| {
let value = String::from_utf8_lossy(&output.stdout).trim().to_string();
if value.len() >= 5 {
let sign: i64 = if value.starts_with('-') { -1 } else { 1 };
let hours: i64 = value[1..3].parse().ok()?;
let mins: i64 = value[3..5].parse().ok()?;
Some(sign * (hours * 3600 + mins * 60))
} else {
None
}
})
.unwrap_or(0)
})
}
const fn days_in_month(year: i64, month: i64) -> i64 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
2 => {
if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) {
29
} else {
28
}
},
_ => 30,
}
}
pub(in super::super) fn format_date(iso: &str) -> String {
let stripped = iso.trim_end_matches('Z');
if let Some((date, after_t)) = stripped.split_once('T') {
let has_offset = after_t.rfind(['+', '-']).is_some_and(|p| p > 0);
if has_offset {
return date.to_string();
}
}
let full = format_timestamp(iso);
full.split(' ').next().unwrap_or(&full).to_string()
}
pub(in super::super) fn format_time(iso: &str) -> String {
let is_utc = iso.ends_with('Z');
let stripped = iso.trim_end_matches('Z');
let Some((_, time_and_offset)) = stripped.split_once('T') else {
return "—".to_string();
};
let (time_str, has_embedded_offset) =
time_and_offset
.rfind(['+', '-'])
.map_or((time_and_offset, false), |pos| {
if pos > 0 {
(&time_and_offset[..pos], true)
} else {
(time_and_offset, false)
}
});
let time_parts: Vec<&str> = time_str.split(':').collect();
if time_parts.len() < 3 {
return time_str.to_string();
}
let (Ok(hour), Ok(minute), Ok(second)) = (
time_parts[0].parse::<i64>(),
time_parts[1].parse::<i64>(),
time_parts[2]
.split('.')
.next()
.unwrap_or("0")
.parse::<i64>(),
) else {
return time_str.to_string();
};
let offset = if has_embedded_offset || !is_utc {
0
} else {
local_utc_offset_secs()
};
let total_secs = hour * 3600 + minute * 60 + second + offset;
let mut adj = total_secs % (24 * 3600);
if adj < 0 {
adj += 24 * 3600;
}
let h = adj / 3600;
let m = (adj % 3600) / 60;
let s = adj % 60;
format!("{h:02}:{m:02}:{s:02}")
}
pub(in super::super) fn format_duration(duration_ms: Option<u64>) -> String {
let Some(ms) = duration_ms else {
return "—".to_string();
};
let total_secs = ms / 1000;
let minutes = total_secs / 60;
let seconds = total_secs % 60;
if minutes > 0 {
format!("{minutes}m {seconds:02}s")
} else {
format!("{seconds}s")
}
}
pub(in super::super) fn format_timestamp(iso: &str) -> String {
let utc_offset_secs = local_utc_offset_secs();
let stripped = iso.trim_end_matches('Z');
match stripped.split_once('T') {
Some((date, time)) => {
let date_parts: Vec<&str> = date.split('-').collect();
let time_parts: Vec<&str> = time.split(':').collect();
if date_parts.len() >= 3
&& time_parts.len() >= 2
&& let (Ok(y), Ok(month), Ok(day), Ok(hour), Ok(minute)) = (
date_parts[0].parse::<i64>(),
date_parts[1].parse::<i64>(),
date_parts[2].parse::<i64>(),
time_parts[0].parse::<i64>(),
time_parts[1].parse::<i64>(),
)
{
let total_mins = hour * 60 + minute + utc_offset_secs / 60;
let mut day = day;
let mut month = month;
let mut year = y;
let mut adj_mins = total_mins % (24 * 60);
if adj_mins < 0 {
adj_mins += 24 * 60;
day -= 1;
if day < 1 {
month -= 1;
if month < 1 {
month = 12;
year -= 1;
}
day = days_in_month(year, month);
}
} else if adj_mins >= 24 * 60 {
adj_mins -= 24 * 60;
day += 1;
if day > days_in_month(year, month) {
day = 1;
month += 1;
if month > 12 {
month = 1;
year += 1;
}
}
}
let local_h = adj_mins / 60;
let local_m = adj_mins % 60;
return format!("{year:04}-{month:02}-{day:02} {local_h:02}:{local_m:02}");
}
let short_time = if time.len() >= 5 { &time[..5] } else { time };
format!("{date} {short_time}")
},
None => stripped.to_string(),
}
}