formualizer_eval/builtins/datetime/
serial.rs1use chrono::{Days, NaiveDate, NaiveDateTime, NaiveTime};
10use formualizer_common::{DateSystem, ExcelError};
11
12const EXCEL_1900_EPOCH: NaiveDate = NaiveDate::from_ymd_opt(1899, 12, 31).unwrap();
13const EXCEL_1904_EPOCH: NaiveDate = NaiveDate::from_ymd_opt(1904, 1, 1).unwrap();
14const SECONDS_PER_DAY: f64 = 86_400.0;
15
16fn checked_add_days(date: NaiveDate, days: i64) -> Result<NaiveDate, ExcelError> {
17 if days >= 0 {
18 date.checked_add_days(Days::new(days as u64))
19 } else {
20 date.checked_sub_days(Days::new(days.unsigned_abs()))
21 }
22 .ok_or_else(ExcelError::new_num)
23}
24
25fn legacy_time(serial: f64) -> Result<NaiveTime, ExcelError> {
26 let total_seconds = (serial.fract() * SECONDS_PER_DAY).round() as u32;
29 let hours = total_seconds / 3600;
30 let minutes = (total_seconds % 3600) / 60;
31 let seconds = total_seconds % 60;
32 NaiveTime::from_hms_opt(hours.min(23), minutes.min(59), seconds.min(59))
33 .ok_or_else(ExcelError::new_num)
34}
35
36fn legacy_excel_1900_date(serial: f64) -> Result<NaiveDate, ExcelError> {
37 if serial.is_infinite() {
38 return Err(ExcelError::new_num());
39 }
40
41 let serial_int = serial.trunc();
45 if serial_int < 0.0 {
46 return Err(ExcelError::new_num());
47 }
48 let serial_int = serial_int as i64;
49 if serial_int == 60 {
50 return Ok(NaiveDate::from_ymd_opt(1900, 2, 28).unwrap());
51 }
52 let offset = if serial_int < 60 {
53 serial_int
54 } else {
55 serial_int - 1
56 };
57 checked_add_days(EXCEL_1900_EPOCH, offset)
58}
59
60pub fn serial_to_date(serial: f64) -> Result<NaiveDate, ExcelError> {
62 legacy_excel_1900_date(serial)
63}
64
65pub fn date_to_serial(date: &NaiveDate) -> f64 {
67 formualizer_common::date_to_serial_for(DateSystem::Excel1900, date)
68}
69
70pub fn date_to_serial_for(system: DateSystem, date: &NaiveDate) -> f64 {
72 formualizer_common::date_to_serial_for(system, date)
73}
74
75pub fn datetime_to_serial(datetime: &NaiveDateTime) -> f64 {
77 formualizer_common::datetime_to_serial_for(DateSystem::Excel1900, datetime)
78}
79
80pub fn datetime_to_serial_for(system: DateSystem, datetime: &NaiveDateTime) -> f64 {
82 formualizer_common::datetime_to_serial_for(system, datetime)
83}
84
85pub fn serial_to_datetime(serial: f64) -> Result<NaiveDateTime, ExcelError> {
91 let date = legacy_excel_1900_date(serial)?;
92 Ok(NaiveDateTime::new(date, legacy_time(serial)?))
93}
94
95pub fn serial_to_datetime_for(
101 system: DateSystem,
102 serial: f64,
103) -> Result<NaiveDateTime, ExcelError> {
104 if serial.is_infinite() {
105 return Err(ExcelError::new_num());
106 }
107 match system {
108 DateSystem::Excel1900 => serial_to_datetime(serial),
109 DateSystem::Excel1904 => {
110 if serial.is_nan() {
111 return Err(ExcelError::new_num());
112 }
113 let days = serial.trunc() as i64;
114 let date = checked_add_days(EXCEL_1904_EPOCH, days)?;
115 Ok(NaiveDateTime::new(date, legacy_time(serial)?))
116 }
117 }
118}
119
120pub fn time_to_fraction(time: &NaiveTime) -> f64 {
122 formualizer_common::time_to_fraction(time)
123}
124
125pub fn create_date_normalized(year: i32, month: i32, day: i32) -> Result<NaiveDate, ExcelError> {
127 let total_months = (year * 12) + month - 1;
128 let normalized_year = total_months / 12;
129 let normalized_month = (total_months % 12) + 1;
130 let first_of_month = NaiveDate::from_ymd_opt(normalized_year, normalized_month as u32, 1)
131 .ok_or_else(ExcelError::new_num)?;
132 first_of_month
133 .checked_add_signed(chrono::TimeDelta::days((day - 1) as i64))
134 .ok_or_else(ExcelError::new_num)
135}