1use std::fmt;
8
9use chrono::{NaiveDate, NaiveDateTime, NaiveTime, Timelike};
10
11#[derive(Debug, Clone, PartialEq, Default)]
13pub enum CellValue {
14 #[default]
16 Empty,
17 Bool(bool),
19 Number(f64),
21 String(String),
23 Formula {
25 expr: String,
26 result: Option<Box<CellValue>>,
27 },
28 Date(f64),
32 Error(String),
34 RichString(Vec<crate::rich_text::RichTextRun>),
36}
37
38impl fmt::Display for CellValue {
39 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40 match self {
41 CellValue::Empty => write!(f, ""),
42 CellValue::Bool(b) => write!(f, "{}", if *b { "TRUE" } else { "FALSE" }),
43 CellValue::Number(n) => {
44 if n.fract() == 0.0 && n.is_finite() {
46 write!(f, "{}", *n as i64)
47 } else {
48 write!(f, "{n}")
49 }
50 }
51 CellValue::Date(serial) => {
52 if let Some(dt) = serial_to_datetime(*serial) {
53 if serial.fract() == 0.0 {
54 write!(f, "{}", dt.format("%Y-%m-%d"))
56 } else {
57 write!(f, "{}", dt.format("%Y-%m-%d %H:%M:%S"))
58 }
59 } else {
60 write!(f, "{serial}")
61 }
62 }
63 CellValue::String(s) => write!(f, "{s}"),
64 CellValue::Formula { result, expr, .. } => {
65 if let Some(result) = result {
66 write!(f, "{result}")
67 } else {
68 write!(f, "={expr}")
69 }
70 }
71 CellValue::Error(e) => write!(f, "{e}"),
72 CellValue::RichString(runs) => {
73 write!(f, "{}", crate::rich_text::rich_text_to_plain(runs))
74 }
75 }
76 }
77}
78
79impl From<&str> for CellValue {
80 fn from(s: &str) -> Self {
81 CellValue::String(s.to_string())
82 }
83}
84
85impl From<String> for CellValue {
86 fn from(s: String) -> Self {
87 CellValue::String(s)
88 }
89}
90
91impl From<f64> for CellValue {
92 fn from(n: f64) -> Self {
93 CellValue::Number(n)
94 }
95}
96
97impl From<i32> for CellValue {
98 fn from(n: i32) -> Self {
99 CellValue::Number(f64::from(n))
100 }
101}
102
103impl From<i64> for CellValue {
104 fn from(n: i64) -> Self {
105 CellValue::Number(n as f64)
106 }
107}
108
109impl From<bool> for CellValue {
110 fn from(b: bool) -> Self {
111 CellValue::Bool(b)
112 }
113}
114
115impl From<NaiveDate> for CellValue {
116 fn from(date: NaiveDate) -> Self {
117 CellValue::Date(date_to_serial(date))
118 }
119}
120
121impl From<NaiveDateTime> for CellValue {
122 fn from(datetime: NaiveDateTime) -> Self {
123 CellValue::Date(datetime_to_serial(datetime))
124 }
125}
126
127const SECONDS_PER_DAY: f64 = 86_400.0;
129
130pub fn date_to_serial(date: NaiveDate) -> f64 {
135 let epoch = NaiveDate::from_ymd_opt(1899, 12, 31).expect("valid epoch date 1899-12-31");
136 let days = (date - epoch).num_days() as f64;
137 if days >= 60.0 {
141 days + 1.0
142 } else {
143 days
144 }
145}
146
147pub fn datetime_to_serial(datetime: NaiveDateTime) -> f64 {
152 let date_part = date_to_serial(datetime.date());
153 let time = datetime.time();
154 let seconds_since_midnight =
155 time.hour() as f64 * 3600.0 + time.minute() as f64 * 60.0 + time.second() as f64;
156 date_part + seconds_since_midnight / SECONDS_PER_DAY
157}
158
159pub fn serial_to_date(serial: f64) -> Option<NaiveDate> {
163 let serial_int = serial.floor() as i64;
164 if serial_int < 1 {
165 return None;
166 }
167 let epoch = NaiveDate::from_ymd_opt(1899, 12, 31).expect("valid epoch date 1899-12-31");
168 if serial_int == 60 {
171 return NaiveDate::from_ymd_opt(1900, 2, 28);
172 }
173 let adjusted = if serial_int >= 61 {
175 serial_int - 1
176 } else {
177 serial_int
178 };
179 epoch.checked_add_signed(chrono::Duration::days(adjusted))
180}
181
182pub fn serial_to_datetime(serial: f64) -> Option<NaiveDateTime> {
186 let date = serial_to_date(serial)?;
187 let frac = serial.fract().abs();
188 let total_seconds = (frac * SECONDS_PER_DAY).round() as u32;
189 let hours = total_seconds / 3600;
190 let minutes = (total_seconds % 3600) / 60;
191 let seconds = total_seconds % 60;
192 let time = NaiveTime::from_hms_opt(hours, minutes, seconds)?;
193 Some(NaiveDateTime::new(date, time))
194}
195
196pub fn is_date_num_fmt(num_fmt_id: u32) -> bool {
200 matches!(num_fmt_id, 14..=22 | 45..=47)
201}
202
203pub fn is_date_format_code(code: &str) -> bool {
208 let mut in_quotes = false;
209 let mut prev_backslash = false;
210 for ch in code.chars() {
211 if prev_backslash {
212 prev_backslash = false;
213 continue;
214 }
215 if ch == '\\' {
216 prev_backslash = true;
217 continue;
218 }
219 if ch == '"' {
220 in_quotes = !in_quotes;
221 continue;
222 }
223 if in_quotes {
224 continue;
225 }
226 let lower = ch.to_ascii_lowercase();
227 if matches!(lower, 'y' | 'd' | 'h' | 's') {
228 return true;
229 }
230 if lower == 'm' {
234 return true;
235 }
236 }
237 false
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn test_cell_value_default_is_empty() {
246 let v = CellValue::default();
247 assert_eq!(v, CellValue::Empty);
248 }
249
250 #[test]
251 fn test_cell_value_from_str() {
252 let v: CellValue = "hello".into();
253 assert_eq!(v, CellValue::String("hello".to_string()));
254 }
255
256 #[test]
257 fn test_cell_value_from_string() {
258 let v: CellValue = String::from("world").into();
259 assert_eq!(v, CellValue::String("world".to_string()));
260 }
261
262 #[test]
263 fn test_cell_value_from_f64() {
264 let v: CellValue = 3.15.into();
265 assert_eq!(v, CellValue::Number(3.15));
266 }
267
268 #[test]
269 fn test_cell_value_from_i32() {
270 let v: CellValue = 42i32.into();
271 assert_eq!(v, CellValue::Number(42.0));
272 }
273
274 #[test]
275 fn test_cell_value_from_i64() {
276 let v: CellValue = 100i64.into();
277 assert_eq!(v, CellValue::Number(100.0));
278 }
279
280 #[test]
281 fn test_cell_value_from_bool() {
282 let v: CellValue = true.into();
283 assert_eq!(v, CellValue::Bool(true));
284
285 let v2: CellValue = false.into();
286 assert_eq!(v2, CellValue::Bool(false));
287 }
288
289 #[test]
290 fn test_cell_value_display() {
291 assert_eq!(CellValue::Empty.to_string(), "");
292 assert_eq!(CellValue::Bool(true).to_string(), "TRUE");
293 assert_eq!(CellValue::Bool(false).to_string(), "FALSE");
294 assert_eq!(CellValue::Number(42.0).to_string(), "42");
295 assert_eq!(CellValue::Number(3.15).to_string(), "3.15");
296 assert_eq!(CellValue::String("hello".to_string()).to_string(), "hello");
297 assert_eq!(
298 CellValue::Error("#DIV/0!".to_string()).to_string(),
299 "#DIV/0!"
300 );
301 assert_eq!(
302 CellValue::Formula {
303 expr: "A1+B1".to_string(),
304 result: Some(Box::new(CellValue::Number(42.0))),
305 }
306 .to_string(),
307 "42"
308 );
309 assert_eq!(
310 CellValue::Formula {
311 expr: "A1+B1".to_string(),
312 result: None,
313 }
314 .to_string(),
315 "=A1+B1"
316 );
317 }
318
319 #[test]
322 fn test_date_to_serial_jan_1_1900() {
323 let date = NaiveDate::from_ymd_opt(1900, 1, 1).unwrap();
324 assert_eq!(date_to_serial(date), 1.0);
325 }
326
327 #[test]
328 fn test_date_to_serial_feb_28_1900() {
329 let date = NaiveDate::from_ymd_opt(1900, 2, 28).unwrap();
330 assert_eq!(date_to_serial(date), 59.0);
331 }
332
333 #[test]
334 fn test_date_to_serial_mar_1_1900_accounts_for_leap_year_bug() {
335 let date = NaiveDate::from_ymd_opt(1900, 3, 1).unwrap();
337 assert_eq!(date_to_serial(date), 61.0);
338 }
339
340 #[test]
341 fn test_date_to_serial_jan_1_2000() {
342 let date = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap();
343 assert_eq!(date_to_serial(date), 36526.0);
345 }
346
347 #[test]
348 fn test_date_to_serial_jan_1_1970() {
349 let date = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap();
350 assert_eq!(date_to_serial(date), 25569.0);
351 }
352
353 #[test]
354 fn test_serial_to_date_jan_1_1900() {
355 let date = serial_to_date(1.0).unwrap();
356 assert_eq!(date, NaiveDate::from_ymd_opt(1900, 1, 1).unwrap());
357 }
358
359 #[test]
360 fn test_serial_to_date_feb_28_1900() {
361 let date = serial_to_date(59.0).unwrap();
362 assert_eq!(date, NaiveDate::from_ymd_opt(1900, 2, 28).unwrap());
363 }
364
365 #[test]
366 fn test_serial_to_date_60_phantom_leap_day() {
367 let date = serial_to_date(60.0).unwrap();
369 assert_eq!(date, NaiveDate::from_ymd_opt(1900, 2, 28).unwrap());
370 }
371
372 #[test]
373 fn test_serial_to_date_mar_1_1900() {
374 let date = serial_to_date(61.0).unwrap();
375 assert_eq!(date, NaiveDate::from_ymd_opt(1900, 3, 1).unwrap());
376 }
377
378 #[test]
379 fn test_serial_to_date_jan_1_2000() {
380 let date = serial_to_date(36526.0).unwrap();
381 assert_eq!(date, NaiveDate::from_ymd_opt(2000, 1, 1).unwrap());
382 }
383
384 #[test]
385 fn test_serial_to_date_invalid() {
386 assert!(serial_to_date(0.0).is_none());
387 assert!(serial_to_date(-1.0).is_none());
388 }
389
390 #[test]
391 fn test_date_roundtrip() {
392 let dates = vec![
395 NaiveDate::from_ymd_opt(1900, 3, 1).unwrap(),
396 NaiveDate::from_ymd_opt(2024, 6, 15).unwrap(),
397 NaiveDate::from_ymd_opt(1999, 12, 31).unwrap(),
398 NaiveDate::from_ymd_opt(2100, 1, 1).unwrap(),
399 ];
400 for date in dates {
401 let serial = date_to_serial(date);
402 let roundtripped = serial_to_date(serial).unwrap();
403 assert_eq!(roundtripped, date, "roundtrip failed for {date}");
404 }
405 }
406
407 #[test]
408 fn test_datetime_to_serial_noon() {
409 let dt = NaiveDate::from_ymd_opt(2000, 1, 1)
410 .unwrap()
411 .and_hms_opt(12, 0, 0)
412 .unwrap();
413 let serial = datetime_to_serial(dt);
414 assert!((serial - 36526.5).abs() < 1e-9);
416 }
417
418 #[test]
419 fn test_datetime_to_serial_with_time() {
420 let dt = NaiveDate::from_ymd_opt(2000, 1, 1)
421 .unwrap()
422 .and_hms_opt(6, 0, 0)
423 .unwrap();
424 let serial = datetime_to_serial(dt);
425 assert!((serial - 36526.25).abs() < 1e-9);
427 }
428
429 #[test]
430 fn test_serial_to_datetime_noon() {
431 let dt = serial_to_datetime(36526.5).unwrap();
432 assert_eq!(dt.date(), NaiveDate::from_ymd_opt(2000, 1, 1).unwrap());
433 assert_eq!(dt.time(), NaiveTime::from_hms_opt(12, 0, 0).unwrap());
434 }
435
436 #[test]
437 fn test_datetime_roundtrip() {
438 let dt = NaiveDate::from_ymd_opt(2024, 3, 15)
439 .unwrap()
440 .and_hms_opt(14, 30, 45)
441 .unwrap();
442 let serial = datetime_to_serial(dt);
443 let roundtripped = serial_to_datetime(serial).unwrap();
444 assert_eq!(roundtripped, dt);
445 }
446
447 #[test]
448 fn test_cell_value_date_display_date_only() {
449 let serial = date_to_serial(NaiveDate::from_ymd_opt(2024, 6, 15).unwrap());
450 let cv = CellValue::Date(serial);
451 assert_eq!(cv.to_string(), "2024-06-15");
452 }
453
454 #[test]
455 fn test_cell_value_date_display_with_time() {
456 let dt = NaiveDate::from_ymd_opt(2024, 6, 15)
457 .unwrap()
458 .and_hms_opt(14, 30, 0)
459 .unwrap();
460 let serial = datetime_to_serial(dt);
461 let cv = CellValue::Date(serial);
462 assert_eq!(cv.to_string(), "2024-06-15 14:30:00");
463 }
464
465 #[test]
466 fn test_cell_value_from_naive_date() {
467 let date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
468 let cv: CellValue = date.into();
469 match cv {
470 CellValue::Date(s) => assert_eq!(s, date_to_serial(date)),
471 _ => panic!("expected Date variant"),
472 }
473 }
474
475 #[test]
476 fn test_cell_value_from_naive_datetime() {
477 let dt = NaiveDate::from_ymd_opt(2024, 1, 1)
478 .unwrap()
479 .and_hms_opt(12, 0, 0)
480 .unwrap();
481 let cv: CellValue = dt.into();
482 match cv {
483 CellValue::Date(s) => assert_eq!(s, datetime_to_serial(dt)),
484 _ => panic!("expected Date variant"),
485 }
486 }
487
488 #[test]
489 fn test_is_date_num_fmt() {
490 for id in 14..=22 {
492 assert!(is_date_num_fmt(id), "expected {id} to be a date format");
493 }
494 for id in 45..=47 {
496 assert!(is_date_num_fmt(id), "expected {id} to be a date format");
497 }
498 assert!(!is_date_num_fmt(0));
500 assert!(!is_date_num_fmt(1));
501 assert!(!is_date_num_fmt(13));
502 assert!(!is_date_num_fmt(23));
503 assert!(!is_date_num_fmt(49));
504 }
505
506 #[test]
507 fn test_is_date_format_code() {
508 assert!(is_date_format_code("yyyy-mm-dd"));
509 assert!(is_date_format_code("m/d/yyyy"));
510 assert!(is_date_format_code("h:mm:ss"));
511 assert!(is_date_format_code("dd/mm/yyyy hh:mm"));
512 assert!(!is_date_format_code("0.00"));
514 assert!(!is_date_format_code("#,##0"));
515 assert!(!is_date_format_code("0%"));
516 assert!(!is_date_format_code("\"date\"0.00"));
518 assert!(!is_date_format_code("\\d0.00"));
520 }
521
522 #[test]
523 fn test_date_early_dates_before_leap_bug() {
524 let date = NaiveDate::from_ymd_opt(1900, 1, 2).unwrap();
526 assert_eq!(date_to_serial(date), 2.0);
527 assert_eq!(serial_to_date(2.0).unwrap(), date);
528 }
529}