1use chrono::{Duration as ChronoDur, NaiveDate, NaiveDateTime, NaiveTime, Timelike};
2use std::{
3 fmt::{self, Display},
4 hash::{Hash, Hasher},
5};
6
7use crate::ExcelError;
8
9#[cfg(feature = "serde")]
10use serde::{Deserialize, Serialize};
11
12pub fn datetime_to_serial(dt: &NaiveDateTime) -> f64 {
28 let base = NaiveDate::from_ymd_opt(1899, 12, 31).unwrap();
29 let mut days = (dt.date() - base).num_days();
30
31 if dt.date() >= NaiveDate::from_ymd_opt(1900, 3, 1).unwrap() {
33 days += 1;
34 }
35
36 let secs_in_day = dt.time().num_seconds_from_midnight() as f64;
37 days as f64 + secs_in_day / 86_400.0
38}
39
40pub fn serial_to_datetime(serial: f64) -> NaiveDateTime {
41 let days = serial.trunc() as i64;
42 let frac_secs = (serial.fract() * 86_400.0).round() as i64;
43
44 let base = NaiveDate::from_ymd_opt(1899, 12, 31).unwrap();
46
47 let offset_days = if days == 60 {
49 59
50 } else if days < 60 {
51 days
52 } else {
53 days - 1
54 };
55
56 let date = base + ChronoDur::days(offset_days);
57 let time =
58 NaiveTime::from_num_seconds_from_midnight_opt((frac_secs.rem_euclid(86_400)) as u32, 0)
59 .unwrap();
60 date.and_time(time)
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
67pub enum DateSystem {
68 Excel1900,
69 Excel1904,
70}
71
72impl Display for DateSystem {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 match self {
75 DateSystem::Excel1900 => write!(f, "1900"),
76 DateSystem::Excel1904 => write!(f, "1904"),
77 }
78 }
79}
80
81#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
84#[derive(Debug, Clone, PartialEq)]
85pub enum LiteralValue {
86 Int(i64),
87 Number(f64),
88 Text(String),
89 Boolean(bool),
90 Array(Vec<Vec<LiteralValue>>), Date(chrono::NaiveDate), DateTime(chrono::NaiveDateTime), Time(chrono::NaiveTime), Duration(chrono::Duration), Empty, Pending, Error(ExcelError),
99}
100
101impl Hash for LiteralValue {
102 fn hash<H: Hasher>(&self, state: &mut H) {
103 match self {
104 LiteralValue::Int(i) => i.hash(state),
105 LiteralValue::Number(n) => n.to_bits().hash(state),
106 LiteralValue::Text(s) => s.hash(state),
107 LiteralValue::Boolean(b) => b.hash(state),
108 LiteralValue::Array(a) => a.hash(state),
109 LiteralValue::Date(d) => d.hash(state),
110 LiteralValue::DateTime(dt) => dt.hash(state),
111 LiteralValue::Time(t) => t.hash(state),
112 LiteralValue::Duration(d) => d.hash(state),
113 LiteralValue::Empty => state.write_u8(0),
114 LiteralValue::Pending => state.write_u8(1),
115 LiteralValue::Error(e) => e.hash(state),
116 }
117 }
118}
119
120impl Eq for LiteralValue {}
121
122impl Display for LiteralValue {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 match self {
125 LiteralValue::Int(i) => write!(f, "{i}"),
126 LiteralValue::Number(n) => write!(f, "{n}"),
127 LiteralValue::Text(s) => write!(f, "{s}"),
128 LiteralValue::Boolean(b) => write!(f, "{b}"),
129 LiteralValue::Error(e) => write!(f, "{e}"),
130 LiteralValue::Array(a) => write!(f, "{a:?}"),
131 LiteralValue::Date(d) => write!(f, "{d}"),
132 LiteralValue::DateTime(dt) => write!(f, "{dt}"),
133 LiteralValue::Time(t) => write!(f, "{t}"),
134 LiteralValue::Duration(d) => write!(f, "{d}"),
135 LiteralValue::Empty => write!(f, ""),
136 LiteralValue::Pending => write!(f, "Pending"),
137 }
138 }
139}
140
141#[derive(Debug, Clone, PartialEq)]
142pub enum ValueError {
143 ImplicitIntersection(String),
144}
145
146impl LiteralValue {
147 pub fn coerce_to_single_value(&self) -> Result<LiteralValue, ValueError> {
149 match self {
150 LiteralValue::Array(arr) => {
151 if arr.len() == 1 && arr[0].len() == 1 {
154 Ok(arr[0][0].clone())
155 } else if arr.is_empty() || arr[0].is_empty() {
156 Ok(LiteralValue::Empty) } else {
158 Err(ValueError::ImplicitIntersection(
159 "#LiteralValue! Implicit intersection failed".to_string(),
160 ))
161 }
162 }
163 _ => Ok(self.clone()),
164 }
165 }
166
167 pub fn as_serial_number(&self) -> Option<f64> {
168 match self {
169 LiteralValue::Date(d) => {
170 let dt = d.and_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap());
171 Some(datetime_to_serial(&dt))
172 }
173 LiteralValue::DateTime(dt) => Some(datetime_to_serial(dt)),
174 LiteralValue::Time(t) => Some(t.num_seconds_from_midnight() as f64 / 86_400.0),
175 LiteralValue::Duration(d) => Some(d.num_seconds() as f64 / 86_400.0),
176 LiteralValue::Int(i) => Some(*i as f64),
177 LiteralValue::Number(n) => Some(*n),
178 LiteralValue::Boolean(b) => Some(if *b { 1.0 } else { 0.0 }),
179 _ => None,
180 }
181 }
182
183 pub fn from_serial_number(serial: f64) -> Self {
186 let dt = serial_to_datetime(serial);
187 if dt.time() == NaiveTime::from_hms_opt(0, 0, 0).unwrap() {
188 LiteralValue::Date(dt.date())
189 } else {
190 LiteralValue::DateTime(dt)
191 }
192 }
193
194 pub fn is_truthy(&self) -> bool {
195 match self {
196 LiteralValue::Boolean(b) => *b,
197 LiteralValue::Int(i) => *i != 0,
198 LiteralValue::Number(n) => *n != 0.0,
199 LiteralValue::Text(s) => !s.is_empty(),
200 LiteralValue::Array(arr) => !arr.is_empty(),
201 LiteralValue::Date(_) => true,
202 LiteralValue::DateTime(_) => true,
203 LiteralValue::Time(_) => true,
204 LiteralValue::Duration(_) => true,
205 LiteralValue::Error(_) => false,
206 LiteralValue::Empty => false,
207 LiteralValue::Pending => false,
208 }
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn excel_1900_serial_roundtrip_basic() {
218 let base = NaiveDate::from_ymd_opt(1900, 1, 1).unwrap();
219 let dt = base.and_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap());
220 assert!((datetime_to_serial(&dt) - 1.0).abs() < 1e-12);
221 assert_eq!(serial_to_datetime(1.0).date(), base);
222 }
223
224 #[test]
225 fn excel_1900_phantom_day_behavior() {
226 let d59 = serial_to_datetime(59.0).date();
228 let d60 = serial_to_datetime(60.0).date();
229 let d61 = serial_to_datetime(61.0).date();
230 assert_eq!(d59, NaiveDate::from_ymd_opt(1900, 2, 28).unwrap());
231 assert_eq!(d60, NaiveDate::from_ymd_opt(1900, 2, 28).unwrap());
232 assert_eq!(d61, NaiveDate::from_ymd_opt(1900, 3, 1).unwrap());
233 }
234
235 #[test]
236 fn excel_1900_modern_date_regression() {
237 let d = serial_to_datetime(44986.0).date();
239 assert_eq!(d, NaiveDate::from_ymd_opt(2023, 3, 1).unwrap());
240 }
241}