use chrono::{Duration, NaiveDate, NaiveTime, Timelike};
fn base() -> NaiveDate {
NaiveDate::from_ymd_opt(1899, 12, 30).unwrap()
}
pub fn serial_to_date(serial: f64) -> Option<NaiveDate> {
let days = serial.floor() as i64;
base().checked_add_signed(Duration::days(days))
}
pub fn date_to_serial(date: NaiveDate) -> f64 {
date.signed_duration_since(base()).num_days() as f64
}
pub fn serial_to_time(serial: f64) -> (u32, u32, u32) {
let frac = serial.fract().abs();
let total_secs = (frac * 86400.0).round() as u32;
let h = total_secs / 3600;
let m = (total_secs % 3600) / 60;
let s = total_secs % 60;
(h, m, s)
}
pub fn time_to_serial(h: u32, m: u32, s: u32) -> f64 {
(h as f64 * 3600.0 + m as f64 * 60.0 + s as f64) / 86400.0
}
pub fn text_to_date_serial(text: &str) -> Option<f64> {
let formats = [
"%m/%d/%Y", "%m/%d/%y", "%Y-%m-%d",
"%d-%b-%Y", "%d-%b-%y",
"%B %d, %Y", "%b %d, %Y",
"%B %d %Y", "%b %d %Y",
];
for fmt in &formats {
if let Ok(date) = NaiveDate::parse_from_str(text, fmt) {
return Some(date_to_serial(date));
}
}
None
}
pub fn text_to_time_serial(text: &str) -> Option<f64> {
let formats = [
"%H:%M:%S", "%H:%M",
"%I:%M %p", "%I:%M:%S %p",
"%I:%M%p", "%I:%M:%S%p",
];
for fmt in &formats {
if let Ok(t) = NaiveTime::parse_from_str(text, fmt) {
return Some(time_to_serial(t.hour(), t.minute(), t.second()));
}
}
None
}
pub fn excel_ymd_to_serial(year: i32, month: u32, day: u32) -> Option<f64> {
if (year, month, day) == (1900, 2, 29) {
return Some(60.0);
}
let date = NaiveDate::from_ymd_opt(year, month, day)?;
let sheets_serial = date_to_serial(date);
if sheets_serial < 2.0 {
return None;
}
Some(if sheets_serial < 61.0 { sheets_serial - 1.0 } else { sheets_serial })
}
pub fn excel_serial_to_ymd(serial: f64) -> Option<(i32, u32, u32)> {
use chrono::Datelike;
let days = serial.floor();
if days < 1.0 {
return None;
}
if days == 60.0 {
return Some((1900, 2, 29));
}
let sheets_serial = if days < 60.0 { days + 1.0 } else { days };
let date = serial_to_date(sheets_serial)?;
Some((date.year(), date.month(), date.day()))
}