use chrono::{Datelike, 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
}
fn has_two_digit_slash_year(s: &str) -> bool {
let parts: Vec<&str> = s.split('/').collect();
if parts.len() == 3 {
let y = parts[2].trim();
y.len() <= 2 && y.chars().all(|c| c.is_ascii_digit())
} else {
false
}
}
pub fn text_to_date_serial(text: &str) -> Option<f64> {
if text.is_empty() {
return Some(0.0);
}
let date_part: &str = if let Some(idx) = text.find(' ') {
let after = &text[idx + 1..];
let looks_like_time = after
.chars()
.next()
.map(|c| c.is_ascii_digit())
.unwrap_or(false)
&& after.contains(':');
if looks_like_time { &text[..idx] } else { text }
} else {
text
};
let formats_4y = [
"%m/%d/%Y", "%Y-%m-%d", "%Y/%m/%d",
"%d-%b-%Y",
"%B %d, %Y", "%b %d, %Y",
"%B %d %Y", "%b %d %Y",
];
for fmt in &formats_4y {
if *fmt == "%m/%d/%Y" && has_two_digit_slash_year(date_part) {
continue;
}
if let Ok(date) = NaiveDate::parse_from_str(date_part, fmt) {
return Some(date_to_serial(date));
}
}
let formats_2y: &[&str] = &["%m/%d/%y", "%d-%b-%y"];
for fmt in formats_2y {
if let Ok(date) = NaiveDate::parse_from_str(date_part, fmt) {
let y = date.year();
let century_year = if y <= 29 { y + 2000 } else if y <= 99 { y + 1900 } else { y };
if let Some(remapped) = NaiveDate::from_ymd_opt(century_year, date.month(), date.day()) {
return Some(date_to_serial(remapped));
}
}
}
None
}
pub fn text_to_time_serial(text: &str) -> Option<f64> {
let time_part = if let Some(idx) = text.find(' ') {
let before = &text[..idx];
if before.contains(':') {
text
} else {
&text[idx + 1..]
}
} else {
text
};
let stripped = strip_fractional_seconds(time_part);
let candidates: &[&str] = if stripped.as_deref() != Some(time_part) {
&[stripped.as_deref().unwrap_or(time_part), time_part]
} else {
&[time_part]
};
let formats = [
"%H:%M:%S",
"%H:%M",
"%I:%M %p",
"%I:%M:%S %p",
"%I:%M%p",
"%I:%M:%S%p",
];
if let Some(ref stripped_str) = stripped {
if stripped_str.as_str() != time_part {
let sub_second = extract_sub_second(time_part).unwrap_or(0.0);
for fmt in &formats {
if let Ok(t) = NaiveTime::parse_from_str(stripped_str, fmt) {
let serial = time_to_serial(t.hour(), t.minute(), t.second());
return Some(serial + sub_second / 86400.0);
}
}
}
}
for candidate in candidates {
for fmt in &formats {
if let Ok(t) = NaiveTime::parse_from_str(candidate, fmt) {
return Some(time_to_serial(t.hour(), t.minute(), t.second()));
}
}
}
None
}
fn strip_fractional_seconds(s: &str) -> Option<String> {
if let Some(dot_pos) = s.find('.') {
if dot_pos >= 2 {
let after_dot = &s[dot_pos + 1..];
let frac_len = after_dot.chars().take_while(|c| c.is_ascii_digit()).count();
let end = dot_pos + 1 + frac_len;
let result = format!("{}{}", &s[..dot_pos], &s[end..]);
return Some(result);
}
}
None
}
fn extract_sub_second(s: &str) -> Option<f64> {
if let Some(dot_pos) = s.find('.') {
let after_dot = &s[dot_pos..]; let frac_end = 1 + after_dot[1..].chars().take_while(|c| c.is_ascii_digit()).count();
let frac_str = &after_dot[..frac_end]; frac_str.parse::<f64>().ok()
} else {
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)> {
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()))
}