use chrono::{Days, NaiveDate, NaiveDateTime, NaiveTime};
use formualizer_common::{DateSystem, ExcelError};
const EXCEL_1900_EPOCH: NaiveDate = NaiveDate::from_ymd_opt(1899, 12, 31).unwrap();
const EXCEL_1904_EPOCH: NaiveDate = NaiveDate::from_ymd_opt(1904, 1, 1).unwrap();
const SECONDS_PER_DAY: f64 = 86_400.0;
fn checked_add_days(date: NaiveDate, days: i64) -> Result<NaiveDate, ExcelError> {
if days >= 0 {
date.checked_add_days(Days::new(days as u64))
} else {
date.checked_sub_days(Days::new(days.unsigned_abs()))
}
.ok_or_else(ExcelError::new_num)
}
fn legacy_time(serial: f64) -> Result<NaiveTime, ExcelError> {
let total_seconds = (serial.fract() * SECONDS_PER_DAY).round() as u32;
let hours = total_seconds / 3600;
let minutes = (total_seconds % 3600) / 60;
let seconds = total_seconds % 60;
NaiveTime::from_hms_opt(hours.min(23), minutes.min(59), seconds.min(59))
.ok_or_else(ExcelError::new_num)
}
fn legacy_excel_1900_date(serial: f64) -> Result<NaiveDate, ExcelError> {
if serial.is_infinite() {
return Err(ExcelError::new_num());
}
let serial_int = serial.trunc();
if serial_int < 0.0 {
return Err(ExcelError::new_num());
}
let serial_int = serial_int as i64;
if serial_int == 60 {
return Ok(NaiveDate::from_ymd_opt(1900, 2, 28).unwrap());
}
let offset = if serial_int < 60 {
serial_int
} else {
serial_int - 1
};
checked_add_days(EXCEL_1900_EPOCH, offset)
}
pub fn serial_to_date(serial: f64) -> Result<NaiveDate, ExcelError> {
legacy_excel_1900_date(serial)
}
pub fn date_to_serial(date: &NaiveDate) -> f64 {
formualizer_common::date_to_serial_for(DateSystem::Excel1900, date)
}
pub fn date_to_serial_for(system: DateSystem, date: &NaiveDate) -> f64 {
formualizer_common::date_to_serial_for(system, date)
}
pub fn datetime_to_serial(datetime: &NaiveDateTime) -> f64 {
formualizer_common::datetime_to_serial_for(DateSystem::Excel1900, datetime)
}
pub fn datetime_to_serial_for(system: DateSystem, datetime: &NaiveDateTime) -> f64 {
formualizer_common::datetime_to_serial_for(system, datetime)
}
pub fn serial_to_datetime(serial: f64) -> Result<NaiveDateTime, ExcelError> {
let date = legacy_excel_1900_date(serial)?;
Ok(NaiveDateTime::new(date, legacy_time(serial)?))
}
pub fn serial_to_datetime_for(
system: DateSystem,
serial: f64,
) -> Result<NaiveDateTime, ExcelError> {
if serial.is_infinite() {
return Err(ExcelError::new_num());
}
match system {
DateSystem::Excel1900 => serial_to_datetime(serial),
DateSystem::Excel1904 => {
if serial.is_nan() {
return Err(ExcelError::new_num());
}
let days = serial.trunc() as i64;
let date = checked_add_days(EXCEL_1904_EPOCH, days)?;
Ok(NaiveDateTime::new(date, legacy_time(serial)?))
}
}
}
pub fn time_to_fraction(time: &NaiveTime) -> f64 {
formualizer_common::time_to_fraction(time)
}
pub fn create_date_normalized(year: i32, month: i32, day: i32) -> Result<NaiveDate, ExcelError> {
let total_months = (year * 12) + month - 1;
let normalized_year = total_months / 12;
let normalized_month = (total_months % 12) + 1;
let first_of_month = NaiveDate::from_ymd_opt(normalized_year, normalized_month as u32, 1)
.ok_or_else(ExcelError::new_num)?;
first_of_month
.checked_add_signed(chrono::TimeDelta::days((day - 1) as i64))
.ok_or_else(ExcelError::new_num)
}