use chrono::{NaiveDate, NaiveDateTime, NaiveTime, SubsecRound, Timelike};
use crate::errors::{ErrInvalidDate, Result};
const NANOS_IN_A_DAY: f64 = 24.0 * 60.0 * 60.0 * 1_000_000_000.0;
const ROUND_EPSILON: f64 = 1e-9;
pub const EXCEL_EPOCH_1900: fn() -> NaiveDateTime = || {
NaiveDate::from_ymd_opt(1899, 12, 31)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap()
};
pub const EXCEL_REVERSE_EPOCH_1900: fn() -> NaiveDateTime = || {
NaiveDate::from_ymd_opt(1899, 12, 30)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap()
};
pub const EXCEL_EPOCH_1904: fn() -> NaiveDateTime = || {
NaiveDate::from_ymd_opt(1904, 1, 1)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap()
};
pub const EXCEL_MIN_TIME_1900: fn() -> NaiveDateTime = || {
NaiveDate::from_ymd_opt(1899, 12, 31)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap()
};
pub const EXCEL_BUGGY_PERIOD_START: fn() -> NaiveDateTime = || {
NaiveDate::from_ymd_opt(1900, 3, 1)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap()
};
const DAYS_IN_MONTH: [u32; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
pub fn is_leap_year(y: i32) -> bool {
(y % 4 == 0 && y % 100 != 0) || (y % 400 == 0)
}
pub fn get_days_in_month(y: i32, m: u32) -> u32 {
if m == 2 && is_leap_year(y) {
29
} else {
DAYS_IN_MONTH[m as usize - 1]
}
}
pub fn validate_date(y: i32, m: u32, d: u32) -> bool {
if m < 1 || m > 12 || d < 1 {
return false;
}
d <= get_days_in_month(y, m)
}
pub fn format_year(y: i32) -> i32 {
if y < 1900 {
if y < 30 { y + 2000 } else { y + 1900 }
} else {
y
}
}
pub fn datetime_to_excel_serial(dt: NaiveDateTime, date1904: bool) -> f64 {
let min_date = if date1904 {
EXCEL_EPOCH_1904()
} else {
EXCEL_MIN_TIME_1900()
};
if dt < min_date {
return 0.0;
}
let base = if date1904 {
EXCEL_EPOCH_1904()
} else {
EXCEL_EPOCH_1900()
};
let duration = dt.signed_duration_since(base);
let mut days = duration.num_seconds() as f64 / 86400.0;
if !date1904 && dt >= EXCEL_BUGGY_PERIOD_START() {
days += 1.0;
}
days
}
pub fn date_to_excel_serial(d: NaiveDate, date1904: bool) -> f64 {
datetime_to_excel_serial(d.and_hms_opt(0, 0, 0).unwrap_or_default(), date1904)
}
pub fn time_to_excel_serial(t: NaiveTime) -> f64 {
let seconds = t.num_seconds_from_midnight() as f64;
let nano = t.nanosecond() as f64 / 1_000_000_000.0;
(seconds + nano) / 86400.0
}
pub fn excel_serial_to_datetime(excel_time: f64, date1904: bool) -> Result<NaiveDateTime> {
if excel_time < 0.0 {
return Err(Box::new(ErrInvalidDate));
}
Ok(time_from_excel_time(excel_time, date1904))
}
fn time_from_excel_time(excel_time: f64, date1904: bool) -> NaiveDateTime {
let whole_days_part = excel_time as i64;
if whole_days_part <= 61 {
const OFFSET1900: f64 = 15018.0;
const OFFSET1904: f64 = 16480.0;
const MJD0: f64 = 2400000.5;
let offset = if date1904 { OFFSET1904 } else { OFFSET1900 };
return julian_date_to_gregorian_time(MJD0, excel_time + offset);
}
let float_part = excel_time - (whole_days_part as f64) + ROUND_EPSILON;
let base = if date1904 {
EXCEL_EPOCH_1904()
} else {
EXCEL_REVERSE_EPOCH_1900()
};
let duration = chrono::Duration::nanoseconds((NANOS_IN_A_DAY * float_part) as i64);
let mut date = base
.checked_add_signed(chrono::Duration::days(whole_days_part))
.unwrap_or(base)
.checked_add_signed(duration)
.unwrap_or(base);
if date.nanosecond() / 1_000_000 > 500 {
date = date.round_subsecs(0);
} else {
date = date.trunc_subsecs(0);
}
date
}
fn shift_julian_to_noon(julian_days: f64, julian_fraction: f64) -> (f64, f64) {
match julian_fraction {
f if (-0.5..0.5).contains(&f) => (julian_days, julian_fraction + 0.5),
f if f >= 0.5 => (julian_days + 1.0, julian_fraction - 0.5),
f if f <= -0.5 => (julian_days - 1.0, julian_fraction + 1.5),
_ => (julian_days, julian_fraction),
}
}
fn fraction_of_a_day(fraction: f64) -> (i32, i32, i32, i32) {
const C1US: i64 = 1_000;
const C1S: i64 = 1_000_000_000;
const C1DAY: i64 = 24 * 60 * 60 * C1S;
let mut frac = (C1DAY as f64 * fraction + C1US as f64 / 2.0) as i64;
let nanoseconds = ((frac % C1S) / C1US) as i32 * C1US as i32;
frac /= C1S;
let seconds = (frac % 60) as i32;
frac /= 60;
let minutes = (frac % 60) as i32;
let hours = (frac / 60) as i32;
(hours, minutes, seconds, nanoseconds)
}
fn julian_date_to_gregorian_time(part1: f64, part2: f64) -> NaiveDateTime {
let part1_i = part1.trunc();
let part1_f = part1.fract();
let part2_i = part2.trunc();
let part2_f = part2.fract();
let mut julian_days = part1_i + part2_i;
let mut julian_fraction = part1_f + part2_f;
(julian_days, julian_fraction) = shift_julian_to_noon(julian_days, julian_fraction);
let (day, month, year) = fliegel_van_flandern(julian_days as i32);
let (hours, minutes, seconds, nanoseconds) = fraction_of_a_day(julian_fraction);
NaiveDate::from_ymd_opt(year, month as u32, day as u32)
.unwrap_or_else(|| NaiveDate::from_ymd_opt(1899, 12, 30).unwrap())
.and_hms_nano_opt(
hours as u32,
minutes as u32,
seconds as u32,
nanoseconds as u32,
)
.unwrap_or_default()
}
fn fliegel_van_flandern(jd: i32) -> (i32, i32, i32) {
let mut l = jd + 68569;
let n = (4 * l) / 146097;
l = l - (146097 * n + 3) / 4;
let i = (4000 * (l + 1)) / 1461001;
l = l - (1461 * i) / 4 + 31;
let j = (80 * l) / 2447;
let d = l - (2447 * j) / 80;
l = j / 11;
let m = j + 2 - (12 * l);
let y = 100 * (n - 49) + i + l;
(d, m, y)
}
pub struct Date;
impl Date {
pub fn datetime_to_serial(dt: NaiveDateTime, date1904: bool) -> f64 {
datetime_to_excel_serial(dt, date1904)
}
pub fn date_to_serial(d: NaiveDate, date1904: bool) -> f64 {
date_to_excel_serial(d, date1904)
}
pub fn time_to_serial(t: NaiveTime) -> f64 {
time_to_excel_serial(t)
}
pub fn serial_to_datetime(serial: f64, date1904: bool) -> Result<NaiveDateTime> {
excel_serial_to_datetime(serial, date1904)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_datetime_to_excel_serial_1900() {
let dt = NaiveDate::from_ymd_opt(2024, 7, 13)
.unwrap()
.and_hms_opt(12, 0, 0)
.unwrap();
assert!((datetime_to_excel_serial(dt, false) - 45486.5).abs() < 1e-9);
}
#[test]
fn test_datetime_to_excel_serial_1904() {
let dt = NaiveDate::from_ymd_opt(2024, 7, 13)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap();
assert!((datetime_to_excel_serial(dt, true) - (45486.0 - 1462.0)).abs() < 1e-9);
}
#[test]
fn test_1900_leap_year_bug() {
let dt = NaiveDate::from_ymd_opt(1900, 3, 1)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap();
assert_eq!(datetime_to_excel_serial(dt, false), 61.0);
}
#[test]
fn test_excel_serial_to_datetime_1900() {
let dt = excel_serial_to_datetime(45486.5, false).unwrap();
assert_eq!(dt.date(), NaiveDate::from_ymd_opt(2024, 7, 13).unwrap());
assert_eq!(dt.time(), NaiveTime::from_hms_opt(12, 0, 0).unwrap());
}
#[test]
fn test_excel_serial_to_datetime_1904() {
let dt = excel_serial_to_datetime(44024.5, true).unwrap();
assert_eq!(dt.date(), NaiveDate::from_ymd_opt(2024, 7, 13).unwrap());
assert_eq!(dt.time(), NaiveTime::from_hms_opt(12, 0, 0).unwrap());
}
#[test]
fn test_time_to_excel_serial() {
let t = NaiveTime::from_hms_opt(12, 0, 0).unwrap();
assert!((time_to_excel_serial(t) - 0.5).abs() < 1e-9);
}
#[test]
fn test_negative_serial_is_error() {
assert!(excel_serial_to_datetime(-1.0, false).is_err());
}
#[test]
fn test_is_leap_year() {
assert!(is_leap_year(2024));
assert!(!is_leap_year(2023));
assert!(!is_leap_year(1900));
assert!(is_leap_year(2000));
}
#[test]
fn test_get_days_in_month() {
assert_eq!(get_days_in_month(2024, 2), 29);
assert_eq!(get_days_in_month(2023, 2), 28);
assert_eq!(get_days_in_month(2024, 1), 31);
assert_eq!(get_days_in_month(2024, 4), 30);
}
}