formualizer-common 1.1.3

Core value, reference, and error types shared across the Formualizer parser and engine
Documentation
use chrono::{Duration as ChronoDur, NaiveDate, NaiveDateTime, NaiveTime, Timelike};
use std::{
    fmt::{self, Display},
    hash::{Hash, Hasher},
};

use crate::ExcelError;

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

/* ───────────────────── Excel date-serial utilities ───────────────────
Excel 1900 system with the leap-year bug.

Serial 1  = 1900-01-01
Serial 59 = 1900-02-28
Serial 60 = 1900-02-29 (phantom day; does not exist)
Serial 61 = 1900-03-01

Time is stored as fractional days (no timezone).

Implementation notes:
- For date -> serial: compute days since base 1899-12-31. If date >= 1900-03-01, add 1.
- For serial -> date: if serial == 60, map to 1900-02-28. If serial > 60, subtract 1 day.
-------------------------------------------------------------------- */

pub fn datetime_to_serial(dt: &NaiveDateTime) -> f64 {
    let base = NaiveDate::from_ymd_opt(1899, 12, 31).unwrap();
    let mut days = (dt.date() - base).num_days();

    // Account for Excel's phantom 1900-02-29.
    if dt.date() >= NaiveDate::from_ymd_opt(1900, 3, 1).unwrap() {
        days += 1;
    }

    let secs_in_day = dt.time().num_seconds_from_midnight() as f64;
    days as f64 + secs_in_day / 86_400.0
}

pub fn serial_to_datetime(serial: f64) -> NaiveDateTime {
    let days = serial.trunc() as i64;
    let frac_secs = (serial.fract() * 86_400.0).round() as i64;

    // Excel base: serial 1 = 1900-01-01
    let base = NaiveDate::from_ymd_opt(1899, 12, 31).unwrap();

    // Handle phantom day explicitly.
    let offset_days = if days == 60 {
        59
    } else if days < 60 {
        days
    } else {
        days - 1
    };

    let date = base + ChronoDur::days(offset_days);
    let time =
        NaiveTime::from_num_seconds_from_midnight_opt((frac_secs.rem_euclid(86_400)) as u32, 0)
            .unwrap();
    date.and_time(time)
}

// Historical: EXCEL_EPOCH previously used 1899-12-30 in this crate. Keep all conversions going
// through the functions above, which are aligned with `formualizer-eval`'s Excel1900 mapping.

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DateSystem {
    Excel1900,
    Excel1904,
}

impl Display for DateSystem {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            DateSystem::Excel1900 => write!(f, "1900"),
            DateSystem::Excel1904 => write!(f, "1904"),
        }
    }
}

/// An **interpeter** LiteralValue. This is distinct
/// from the possible types that can be stored in a cell.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub enum LiteralValue {
    Int(i64),
    Number(f64),
    Text(String),
    Boolean(bool),
    Array(Vec<Vec<LiteralValue>>),   // For array results
    Date(chrono::NaiveDate),         // For date values
    DateTime(chrono::NaiveDateTime), // For date/time values
    Time(chrono::NaiveTime),         // For time values
    Duration(chrono::Duration),      // For durations
    Empty,                           // For empty cells/optional arguments
    Pending,                         // For pending values

    Error(ExcelError),
}

impl Hash for LiteralValue {
    fn hash<H: Hasher>(&self, state: &mut H) {
        match self {
            LiteralValue::Int(i) => i.hash(state),
            LiteralValue::Number(n) => n.to_bits().hash(state),
            LiteralValue::Text(s) => s.hash(state),
            LiteralValue::Boolean(b) => b.hash(state),
            LiteralValue::Array(a) => a.hash(state),
            LiteralValue::Date(d) => d.hash(state),
            LiteralValue::DateTime(dt) => dt.hash(state),
            LiteralValue::Time(t) => t.hash(state),
            LiteralValue::Duration(d) => d.hash(state),
            LiteralValue::Empty => state.write_u8(0),
            LiteralValue::Pending => state.write_u8(1),
            LiteralValue::Error(e) => e.hash(state),
        }
    }
}

impl Eq for LiteralValue {}

impl Display for LiteralValue {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            LiteralValue::Int(i) => write!(f, "{i}"),
            LiteralValue::Number(n) => write!(f, "{n}"),
            LiteralValue::Text(s) => write!(f, "{s}"),
            LiteralValue::Boolean(b) => write!(f, "{b}"),
            LiteralValue::Error(e) => write!(f, "{e}"),
            LiteralValue::Array(a) => write!(f, "{a:?}"),
            LiteralValue::Date(d) => write!(f, "{d}"),
            LiteralValue::DateTime(dt) => write!(f, "{dt}"),
            LiteralValue::Time(t) => write!(f, "{t}"),
            LiteralValue::Duration(d) => write!(f, "{d}"),
            LiteralValue::Empty => write!(f, ""),
            LiteralValue::Pending => write!(f, "Pending"),
        }
    }
}

#[derive(Debug, Clone, PartialEq)]
pub enum ValueError {
    ImplicitIntersection(String),
}

impl LiteralValue {
    /// Coerce
    pub fn coerce_to_single_value(&self) -> Result<LiteralValue, ValueError> {
        match self {
            LiteralValue::Array(arr) => {
                // Excel's implicit intersection or single LiteralValue coercion logic here
                // Simplest: take top-left or return #LiteralValue! if not 1x1
                if arr.len() == 1 && arr[0].len() == 1 {
                    Ok(arr[0][0].clone())
                } else if arr.is_empty() || arr[0].is_empty() {
                    Ok(LiteralValue::Empty) // Or maybe error?
                } else {
                    Err(ValueError::ImplicitIntersection(
                        "#LiteralValue! Implicit intersection failed".to_string(),
                    ))
                }
            }
            _ => Ok(self.clone()),
        }
    }

    pub fn as_serial_number(&self) -> Option<f64> {
        match self {
            LiteralValue::Date(d) => {
                let dt = d.and_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap());
                Some(datetime_to_serial(&dt))
            }
            LiteralValue::DateTime(dt) => Some(datetime_to_serial(dt)),
            LiteralValue::Time(t) => Some(t.num_seconds_from_midnight() as f64 / 86_400.0),
            LiteralValue::Duration(d) => Some(d.num_seconds() as f64 / 86_400.0),
            LiteralValue::Int(i) => Some(*i as f64),
            LiteralValue::Number(n) => Some(*n),
            LiteralValue::Boolean(b) => Some(if *b { 1.0 } else { 0.0 }),
            _ => None,
        }
    }

    /// Build the appropriate `LiteralValue` from an Excel serial number.
    /// (Useful when a function returns a date/time).
    pub fn from_serial_number(serial: f64) -> Self {
        let dt = serial_to_datetime(serial);
        if dt.time() == NaiveTime::from_hms_opt(0, 0, 0).unwrap() {
            LiteralValue::Date(dt.date())
        } else {
            LiteralValue::DateTime(dt)
        }
    }

    pub fn is_truthy(&self) -> bool {
        match self {
            LiteralValue::Boolean(b) => *b,
            LiteralValue::Int(i) => *i != 0,
            LiteralValue::Number(n) => *n != 0.0,
            LiteralValue::Text(s) => !s.is_empty(),
            LiteralValue::Array(arr) => !arr.is_empty(),
            LiteralValue::Date(_) => true,
            LiteralValue::DateTime(_) => true,
            LiteralValue::Time(_) => true,
            LiteralValue::Duration(_) => true,
            LiteralValue::Error(_) => false,
            LiteralValue::Empty => false,
            LiteralValue::Pending => false,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn excel_1900_serial_roundtrip_basic() {
        let base = NaiveDate::from_ymd_opt(1900, 1, 1).unwrap();
        let dt = base.and_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap());
        assert!((datetime_to_serial(&dt) - 1.0).abs() < 1e-12);
        assert_eq!(serial_to_datetime(1.0).date(), base);
    }

    #[test]
    fn excel_1900_phantom_day_behavior() {
        // Excel treats serial 60 as 1900-02-29. We map it to 1900-02-28.
        let d59 = serial_to_datetime(59.0).date();
        let d60 = serial_to_datetime(60.0).date();
        let d61 = serial_to_datetime(61.0).date();
        assert_eq!(d59, NaiveDate::from_ymd_opt(1900, 2, 28).unwrap());
        assert_eq!(d60, NaiveDate::from_ymd_opt(1900, 2, 28).unwrap());
        assert_eq!(d61, NaiveDate::from_ymd_opt(1900, 3, 1).unwrap());
    }

    #[test]
    fn excel_1900_modern_date_regression() {
        // Regression: Excel serial for 2023-03-01 should decode to 2023-03-01.
        let d = serial_to_datetime(44986.0).date();
        assert_eq!(d, NaiveDate::from_ymd_opt(2023, 3, 1).unwrap());
    }
}