use chrono::{DateTime, Utc};
pub fn format_relative_date(iso: &str) -> String {
let Ok(dt) = iso.parse::<DateTime<Utc>>() else {
return iso.get(..19).unwrap_or(iso).replace('T', " ");
};
let now = Utc::now();
let diff = now.signed_duration_since(dt);
let secs = diff.num_seconds();
let minutes = diff.num_minutes();
let hours = diff.num_hours();
let days = diff.num_days();
if secs < 0 {
let future_diff = dt.signed_duration_since(now);
let future_days = future_diff.num_days();
let future_hours = future_diff.num_hours();
let future_minutes = future_diff.num_minutes();
let future_secs = future_diff.num_seconds();
let future_months = future_days / 30;
let future_years = future_days / 365;
return match (future_secs, future_minutes, future_hours, future_days) {
(s, _, _, _) if s < 60 => "just now".to_string(),
(_, m, _, _) if m < 60 => format!("in {} min", m),
(_, _, h, _) if h < 24 => format!("in {}h", h),
(_, _, _, 1) => "tomorrow".to_string(),
(_, _, _, d) if d < 7 => format!("in {} days", d),
(_, _, _, d) if d < 14 => "next week".to_string(),
(_, _, _, d) if d < 30 => format!("in {} weeks", d / 7),
(_, _, _, d) if d < 60 => "in about a month".to_string(),
_ if future_years >= 1 => format!(
"in {} year{}",
future_years,
if future_years > 1 { "s" } else { "" }
),
_ => format!("in {} months", future_months),
};
}
let months = days / 30;
let years = days / 365;
match (secs, minutes, hours, days) {
(s, _, _, _) if s < 60 => "just now".to_string(),
(_, m, _, _) if m < 60 => format!("{} min ago", m),
(_, _, h, _) if h < 24 => format!("{}h ago", h),
(_, _, _, 1) => "yesterday".to_string(),
(_, _, _, d) if d < 7 => format!("{} days ago", d),
(_, _, _, d) if d < 14 => "last week".to_string(),
(_, _, _, d) if d < 30 => format!("{} weeks ago", d / 7),
(_, _, _, d) if d < 60 => "about a month ago".to_string(),
_ if years >= 1 => format!("{} year{} ago", years, if years > 1 { "s" } else { "" }),
_ => format!("{} months ago", months),
}
}
pub fn parse_duration_to_hours(input: &str) -> Result<f32, String> {
let s = input.trim().to_lowercase().replace(',', ".");
if let Some(h_pos) = s.find('h') {
let hours_part = &s[..h_pos];
let minutes_part = s[h_pos + 1..].trim_end_matches('m');
let hours: f32 = hours_part
.parse()
.map_err(|_| format!("Invalid hours in \"{}\"", input))?;
let minutes: f32 = if minutes_part.is_empty() {
0.0
} else {
minutes_part
.parse()
.map_err(|_| format!("Invalid minutes in \"{}\"", input))?
};
let total = hours + minutes / 60.0;
if total <= 0.0 {
return Err("Duration must be greater than zero".into());
}
return Ok(total);
}
if let Some(stripped) = s.strip_suffix('m') {
let minutes: f32 = stripped
.parse()
.map_err(|_| format!("Invalid minutes in \"{}\"", input))?;
let total = minutes / 60.0;
if total <= 0.0 {
return Err("Duration must be greater than zero".into());
}
return Ok(total);
}
if let Ok(minutes) = s.parse::<f32>() {
if minutes <= 0.0 {
return Err("Duration must be greater than zero".into());
}
return Ok(minutes / 60.0);
}
Err(format!(
"Unrecognised format \"{}\". Try: 1h30, 90m, 1.5h",
input
))
}