formualizer_eval/builtins/datetime/
serial.rs1use chrono::{NaiveDate, NaiveDateTime, NaiveTime, Timelike};
4use formualizer_common::ExcelError;
5
6use crate::engine::DateSystem;
7
8const EXCEL_BASE_YEAR: i32 = 1899;
24const EXCEL_BASE_MONTH: u32 = 12;
25const EXCEL_BASE_DAY: u32 = 31;
26
27pub fn serial_to_date(serial: f64) -> Result<NaiveDate, ExcelError> {
30 let serial_int = serial.trunc();
31 if serial_int < 0.0 {
32 return Err(ExcelError::new_num());
33 }
34 let serial_int = serial_int as i64; if serial_int == 60 {
38 return Ok(NaiveDate::from_ymd_opt(1900, 2, 28).unwrap());
39 }
40
41 let base = NaiveDate::from_ymd_opt(EXCEL_BASE_YEAR, EXCEL_BASE_MONTH, EXCEL_BASE_DAY)
42 .ok_or_else(ExcelError::new_num)?;
43
44 let offset = if serial_int < 60 {
47 serial_int
48 } else {
49 serial_int - 1
50 };
51
52 base.checked_add_signed(chrono::TimeDelta::days(offset))
53 .ok_or_else(ExcelError::new_num)
54}
55
56pub fn date_to_serial(date: &NaiveDate) -> f64 {
59 let base = NaiveDate::from_ymd_opt(EXCEL_BASE_YEAR, EXCEL_BASE_MONTH, EXCEL_BASE_DAY).unwrap();
60 let diff = (*date - base).num_days(); let serial = if *date >= NaiveDate::from_ymd_opt(1900, 3, 1).unwrap() {
62 diff + 1 } else {
64 diff
65 };
66 serial as f64
67}
68
69pub fn serial_to_datetime(serial: f64) -> Result<NaiveDateTime, ExcelError> {
72 let date = serial_to_date(serial)?;
73 let time_fraction = serial.fract();
74
75 let total_seconds = (time_fraction * 86400.0).round() as u32;
77 let hours = total_seconds / 3600;
78 let minutes = (total_seconds % 3600) / 60;
79 let seconds = total_seconds % 60;
80
81 let time = NaiveTime::from_hms_opt(hours.min(23), minutes.min(59), seconds.min(59))
82 .ok_or_else(ExcelError::new_num)?;
83
84 Ok(NaiveDateTime::new(date, time))
85}
86
87pub fn datetime_to_serial(datetime: &NaiveDateTime) -> f64 {
89 let date_serial = date_to_serial(&datetime.date());
90 let time_fraction = time_to_fraction(&datetime.time());
91 date_serial + time_fraction
92}
93
94const EXCEL_1904_EPOCH: NaiveDate = NaiveDate::from_ymd_opt(1904, 1, 1).unwrap();
97
98pub fn date_to_serial_for(system: DateSystem, date: &NaiveDate) -> f64 {
100 match system {
101 DateSystem::Excel1900 => date_to_serial(date),
102 DateSystem::Excel1904 => (*date - EXCEL_1904_EPOCH).num_days() as f64,
103 }
104}
105
106pub fn datetime_to_serial_for(system: DateSystem, dt: &NaiveDateTime) -> f64 {
108 match system {
109 DateSystem::Excel1900 => datetime_to_serial(dt),
110 DateSystem::Excel1904 => {
111 let days = (dt.date() - EXCEL_1904_EPOCH).num_days() as f64;
112 let frac = time_to_fraction(&dt.time());
113 days + frac
114 }
115 }
116}
117
118pub fn serial_to_datetime_for(
120 system: DateSystem,
121 serial: f64,
122) -> Result<NaiveDateTime, ExcelError> {
123 match system {
124 DateSystem::Excel1900 => serial_to_datetime(serial),
125 DateSystem::Excel1904 => {
126 if serial.is_nan() || serial.is_infinite() {
127 return Err(ExcelError::new_num());
128 }
129 let days = serial.trunc() as i64;
130 let date = EXCEL_1904_EPOCH
131 .checked_add_signed(chrono::TimeDelta::days(days))
132 .ok_or_else(ExcelError::new_num)?;
133 let time_fraction = serial.fract();
134 let total_seconds = (time_fraction * 86400.0).round() as u32;
135 let hours = total_seconds / 3600;
136 let minutes = (total_seconds % 3600) / 60;
137 let seconds = total_seconds % 60;
138 let time = NaiveTime::from_hms_opt(hours.min(23), minutes.min(59), seconds.min(59))
139 .ok_or_else(ExcelError::new_num)?;
140 Ok(NaiveDateTime::new(date, time))
141 }
142 }
143}
144
145pub fn time_to_fraction(time: &NaiveTime) -> f64 {
147 let total_seconds =
148 time.hour() as f64 * 3600.0 + time.minute() as f64 * 60.0 + time.second() as f64;
149 total_seconds / 86400.0
150}
151
152pub fn create_date_normalized(year: i32, month: i32, day: i32) -> Result<NaiveDate, ExcelError> {
155 let total_months = (year * 12) + month - 1;
157 let normalized_year = total_months / 12;
158 let normalized_month = (total_months % 12) + 1;
159
160 let temp_date = NaiveDate::from_ymd_opt(normalized_year, normalized_month as u32, 1)
162 .ok_or_else(ExcelError::new_num)?;
163
164 temp_date
166 .checked_add_signed(chrono::TimeDelta::days((day - 1) as i64))
167 .ok_or_else(ExcelError::new_num)
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn test_serial_to_date_basic() {
176 let date = serial_to_date(1.0).unwrap();
178 assert_eq!(date, NaiveDate::from_ymd_opt(1900, 1, 1).unwrap());
179
180 let date = serial_to_date(2.0).unwrap();
182 assert_eq!(date, NaiveDate::from_ymd_opt(1900, 1, 2).unwrap());
183 }
184
185 #[test]
186 fn test_leap_year_bug() {
187 let date = serial_to_date(59.0).unwrap();
189 assert_eq!(date, NaiveDate::from_ymd_opt(1900, 2, 28).unwrap());
190
191 let date = serial_to_date(60.0).unwrap();
193 assert_eq!(date, NaiveDate::from_ymd_opt(1900, 2, 28).unwrap());
194
195 let date = serial_to_date(61.0).unwrap();
197 assert_eq!(date, NaiveDate::from_ymd_opt(1900, 3, 1).unwrap());
198 }
199
200 #[test]
201 fn test_date_to_serial() {
202 let date = NaiveDate::from_ymd_opt(1900, 1, 1).unwrap();
204 assert_eq!(date_to_serial(&date), 1.0);
205
206 let date = NaiveDate::from_ymd_opt(1900, 2, 28).unwrap();
208 assert_eq!(date_to_serial(&date), 59.0);
209
210 let date = NaiveDate::from_ymd_opt(1900, 3, 1).unwrap();
212 assert_eq!(date_to_serial(&date), 61.0);
213 }
214
215 #[test]
216 fn test_time_fraction() {
217 let time = NaiveTime::from_hms_opt(12, 0, 0).unwrap();
219 assert!((time_to_fraction(&time) - 0.5).abs() < 1e-10);
220
221 let time = NaiveTime::from_hms_opt(6, 0, 0).unwrap();
223 assert!((time_to_fraction(&time) - 0.25).abs() < 1e-10);
224 }
225
226 #[test]
227 fn test_date_normalization() {
228 let date = create_date_normalized(2024, 13, 5).unwrap();
230 assert_eq!(date, NaiveDate::from_ymd_opt(2025, 1, 5).unwrap());
231
232 let date = create_date_normalized(2024, 0, 15).unwrap();
234 assert_eq!(date, NaiveDate::from_ymd_opt(2023, 12, 15).unwrap());
235 }
236}