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};
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();
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;
let base = NaiveDate::from_ymd_opt(1899, 12, 31).unwrap();
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)
}
#[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"),
}
}
}
#[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>>), Date(chrono::NaiveDate), DateTime(chrono::NaiveDateTime), Time(chrono::NaiveTime), Duration(chrono::Duration), Empty, Pending,
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 {
pub fn coerce_to_single_value(&self) -> Result<LiteralValue, ValueError> {
match self {
LiteralValue::Array(arr) => {
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) } 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,
}
}
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() {
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() {
let d = serial_to_datetime(44986.0).date();
assert_eq!(d, NaiveDate::from_ymd_opt(2023, 3, 1).unwrap());
}
}