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