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 ───────────────────
13Serial 0  = 1899-12-30  (Excel’s epoch; includes bogus 1900-02-29)
14Serial 60 = 1900-02-29  (non-existent – keep to preserve offsets)
15Serial 1  = 1899-12-31
16Serial 2  = 1900-01-01         …and so on.
17Time is stored as fractional days (no timezone).
18------------------------------------------------------------------- */
19
20pub fn datetime_to_serial(dt: &NaiveDateTime) -> f64 {
21    // Adjust for the fake 1900-02-29 gap
22    let mut days = (dt.date() - EXCEL_EPOCH).num_days();
23    if days >= 60 {
24        days += 1;
25    }
26
27    let secs_in_day = dt.time().num_seconds_from_midnight() as f64;
28    days as f64 + secs_in_day / 86_400.0
29}
30
31pub fn serial_to_datetime(serial: f64) -> NaiveDateTime {
32    // split at day boundary
33    let days = serial.trunc() as i64;
34    let frac_secs = (serial.fract() * 86_400.0).round() as i64; // 1 day = 86 400 s
35
36    // Serial 60 is bogus 1900-02-29; map it to 1900-03-01 for chrono,
37    // but preserve the exact day count for round-trip.
38    let base_date = if days < 60 {
39        EXCEL_EPOCH
40    } else {
41        EXCEL_EPOCH + ChronoDur::days(1)
42    };
43
44    let date = base_date + ChronoDur::days(days);
45    let time =
46        NaiveTime::from_num_seconds_from_midnight_opt((frac_secs.rem_euclid(86_400)) as u32, 0)
47            .unwrap();
48    date.and_time(time)
49}
50
51const EXCEL_EPOCH: NaiveDate = NaiveDate::from_ymd_opt(1899, 12, 30).unwrap();
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
54pub enum DateSystem {
55    Excel1900,
56    Excel1904,
57}
58
59impl Display for DateSystem {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        match self {
62            DateSystem::Excel1900 => write!(f, "1900"),
63            DateSystem::Excel1904 => write!(f, "1904"),
64        }
65    }
66}
67
68/// An **interpeter** LiteralValue. This is distinct
69/// from the possible types that can be stored in a cell.
70#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
71#[derive(Debug, Clone, PartialEq)]
72pub enum LiteralValue {
73    Int(i64),
74    Number(f64),
75    Text(String),
76    Boolean(bool),
77    Array(Vec<Vec<LiteralValue>>),   // For array results
78    Date(chrono::NaiveDate),         // For date values
79    DateTime(chrono::NaiveDateTime), // For date/time values
80    Time(chrono::NaiveTime),         // For time values
81    Duration(chrono::Duration),      // For durations
82    Empty,                           // For empty cells/optional arguments
83    Pending,                         // For pending values
84
85    Error(ExcelError),
86}
87
88impl Hash for LiteralValue {
89    fn hash<H: Hasher>(&self, state: &mut H) {
90        match self {
91            LiteralValue::Int(i) => i.hash(state),
92            LiteralValue::Number(n) => n.to_bits().hash(state),
93            LiteralValue::Text(s) => s.hash(state),
94            LiteralValue::Boolean(b) => b.hash(state),
95            LiteralValue::Array(a) => a.hash(state),
96            LiteralValue::Date(d) => d.hash(state),
97            LiteralValue::DateTime(dt) => dt.hash(state),
98            LiteralValue::Time(t) => t.hash(state),
99            LiteralValue::Duration(d) => d.hash(state),
100            LiteralValue::Empty => state.write_u8(0),
101            LiteralValue::Pending => state.write_u8(1),
102            LiteralValue::Error(e) => e.hash(state),
103        }
104    }
105}
106
107impl Eq for LiteralValue {}
108
109impl Display for LiteralValue {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        match self {
112            LiteralValue::Int(i) => write!(f, "{i}"),
113            LiteralValue::Number(n) => write!(f, "{n}"),
114            LiteralValue::Text(s) => write!(f, "{s}"),
115            LiteralValue::Boolean(b) => write!(f, "{b}"),
116            LiteralValue::Error(e) => write!(f, "{e}"),
117            LiteralValue::Array(a) => write!(f, "{a:?}"),
118            LiteralValue::Date(d) => write!(f, "{d}"),
119            LiteralValue::DateTime(dt) => write!(f, "{dt}"),
120            LiteralValue::Time(t) => write!(f, "{t}"),
121            LiteralValue::Duration(d) => write!(f, "{d}"),
122            LiteralValue::Empty => write!(f, ""),
123            LiteralValue::Pending => write!(f, "Pending"),
124        }
125    }
126}
127
128#[derive(Debug, Clone, PartialEq)]
129pub enum ValueError {
130    ImplicitIntersection(String),
131}
132
133impl LiteralValue {
134    /// Coerce
135    pub fn coerce_to_single_value(&self) -> Result<LiteralValue, ValueError> {
136        match self {
137            LiteralValue::Array(arr) => {
138                // Excel's implicit intersection or single LiteralValue coercion logic here
139                // Simplest: take top-left or return #LiteralValue! if not 1x1
140                if arr.len() == 1 && arr[0].len() == 1 {
141                    Ok(arr[0][0].clone())
142                } else if arr.is_empty() || arr[0].is_empty() {
143                    Ok(LiteralValue::Empty) // Or maybe error?
144                } else {
145                    Err(ValueError::ImplicitIntersection(
146                        "#LiteralValue! Implicit intersection failed".to_string(),
147                    ))
148                }
149            }
150            _ => Ok(self.clone()),
151        }
152    }
153
154    pub fn as_serial_number(&self) -> Option<f64> {
155        match self {
156            LiteralValue::Date(d) => {
157                let dt = d.and_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap());
158                Some(datetime_to_serial(&dt))
159            }
160            LiteralValue::DateTime(dt) => Some(datetime_to_serial(dt)),
161            LiteralValue::Time(t) => Some(t.num_seconds_from_midnight() as f64 / 86_400.0),
162            LiteralValue::Duration(d) => Some(d.num_seconds() as f64 / 86_400.0),
163            LiteralValue::Int(i) => Some(*i as f64),
164            LiteralValue::Number(n) => Some(*n),
165            LiteralValue::Boolean(b) => Some(if *b { 1.0 } else { 0.0 }),
166            _ => None,
167        }
168    }
169
170    /// Build the appropriate `LiteralValue` from an Excel serial number.
171    /// (Useful when a function returns a date/time).
172    pub fn from_serial_number(serial: f64) -> Self {
173        let dt = serial_to_datetime(serial);
174        if dt.time() == NaiveTime::from_hms_opt(0, 0, 0).unwrap() {
175            LiteralValue::Date(dt.date())
176        } else {
177            LiteralValue::DateTime(dt)
178        }
179    }
180
181    pub fn is_truthy(&self) -> bool {
182        match self {
183            LiteralValue::Boolean(b) => *b,
184            LiteralValue::Int(i) => *i != 0,
185            LiteralValue::Number(n) => *n != 0.0,
186            LiteralValue::Text(s) => !s.is_empty(),
187            LiteralValue::Array(arr) => !arr.is_empty(),
188            LiteralValue::Date(_) => true,
189            LiteralValue::DateTime(_) => true,
190            LiteralValue::Time(_) => true,
191            LiteralValue::Duration(_) => true,
192            LiteralValue::Error(_) => false,
193            LiteralValue::Empty => false,
194            LiteralValue::Pending => false,
195        }
196    }
197}