Skip to main content

formualizer_common/
value.rs

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
12/* ───────────────────── Excel date-serial utilities ───────────────────
13Excel 1900 system with the leap-year bug.
14
15Serial 1  = 1900-01-01
16Serial 59 = 1900-02-28
17Serial 60 = 1900-02-29 (phantom day; does not exist)
18Serial 61 = 1900-03-01
19
20Time is stored as fractional days (no timezone).
21
22Implementation notes:
23- For date -> serial: compute days since base 1899-12-31. If date >= 1900-03-01, add 1.
24- For serial -> date: if serial == 60, map to 1900-02-28. If serial > 60, subtract 1 day.
25-------------------------------------------------------------------- */
26
27pub 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    // Account for Excel's phantom 1900-02-29.
32    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    // Excel base: serial 1 = 1900-01-01
45    let base = NaiveDate::from_ymd_opt(1899, 12, 31).unwrap();
46
47    // Handle phantom day explicitly.
48    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// Historical: EXCEL_EPOCH previously used 1899-12-30 in this crate. Keep all conversions going
64// through the functions above, which are aligned with `formualizer-eval`'s Excel1900 mapping.
65
66#[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/// An **interpeter** LiteralValue. This is distinct
82/// from the possible types that can be stored in a cell.
83#[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>>),   // For array results
91    Date(chrono::NaiveDate),         // For date values
92    DateTime(chrono::NaiveDateTime), // For date/time values
93    Time(chrono::NaiveTime),         // For time values
94    Duration(chrono::Duration),      // For durations
95    Empty,                           // For empty cells/optional arguments
96    Pending,                         // For pending values
97
98    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    /// Coerce
148    pub fn coerce_to_single_value(&self) -> Result<LiteralValue, ValueError> {
149        match self {
150            LiteralValue::Array(arr) => {
151                // Excel's implicit intersection or single LiteralValue coercion logic here
152                // Simplest: take top-left or return #LiteralValue! if not 1x1
153                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) // Or maybe error?
157                } 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    /// Build the appropriate `LiteralValue` from an Excel serial number.
184    /// (Useful when a function returns a date/time).
185    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        // Excel treats serial 60 as 1900-02-29. We map it to 1900-02-28.
227        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        // Regression: Excel serial for 2023-03-01 should decode to 2023-03-01.
238        let d = serial_to_datetime(44986.0).date();
239        assert_eq!(d, NaiveDate::from_ymd_opt(2023, 3, 1).unwrap());
240    }
241}