Skip to main content

xl3_core/
value.rs

1//! Cell value representation used by the planner, source reader, and
2//! evaluator. Deliberately simple — strings, numbers, booleans, plus an
3//! explicit Empty so we can distinguish "blank cell" from "empty string".
4//!
5//! Errors propagate up the eval stack as `Err`, not as a `Value::Error`
6//! variant — that matches what the XTL spec calls "expression error" and
7//! avoids the JS-side `__xl3_error__` marker object until later milestones.
8
9use std::collections::HashMap;
10use std::sync::Arc;
11
12use crate::calamine::Data as CalamineData;
13
14/// A bag of source rows reachable from the evaluator. Lives in
15/// `EvalContext` under the reserved `__rows__` key so row-aggregate
16/// builtins (`SUM`, `AVERAGE`, `MIN`, `MAX`, `COUNT`) can walk every
17/// row in the active block without each row needing to know about it.
18pub type RowsHandle = Arc<Vec<HashMap<String, Value>>>;
19pub type MapHandle = Arc<HashMap<String, Value>>;
20pub type ListHandle = Arc<Vec<Value>>;
21
22#[derive(Debug, Clone, PartialEq)]
23pub enum Value {
24    Empty,
25    String(String),
26    Number(f64),
27    Bool(bool),
28    /// An Excel serial number whose source cell carried a date /
29    /// datetime numFmt. Behaves like `Number` for arithmetic and
30    /// numeric comparison; its canonical string form is the ISO
31    /// `YYYY-MM-DD` / `YYYY-MM-DDTHH:MM:SS` representation per
32    /// ADR-0017. Output cells write the raw serial — the format is
33    /// reapplied by the consuming spreadsheet.
34    DateNumber(f64),
35    /// All source rows visible to the active expansion block. Used for
36    /// row aggregates. Not emitted as a cell value.
37    Rows(RowsHandle),
38    /// Reserved-sheet dictionary value (`__config__`, `__inputs__`,
39    /// `__lists__`). Looked up via `<ns>[key]` in expressions.
40    Map(MapHandle),
41    /// A list — typically a column of `__lists__`. Used by the `in`
42    /// and `!in` operators inside `@filter`.
43    List(ListHandle),
44}
45
46/// xl3 ADR-0009 / ECMA-262 §6.1.6.1.13: a number's canonical string
47/// form is decimal when its magnitude is in [1e-6, 1e21), and
48/// exponential ("xeY") below 1e-6. Integers within ±1e16 round-trip
49/// without a decimal point.
50pub fn canonical_number(n: f64) -> String {
51    if !n.is_finite() {
52        return format!("{n}");
53    }
54    if n.fract() == 0.0 && n.abs() < 1e16 {
55        return format!("{}", n as i64);
56    }
57    let abs = n.abs();
58    if abs > 0.0 && abs < 1e-6 {
59        format!("{n:e}")
60    } else {
61        format!("{n}")
62    }
63}
64
65impl Value {
66    /// Canonical string form per ADR-0009 (xl3 TS `canonicalString` mirror).
67    /// Used when a value is substituted into a mixed text cell — e.g.
68    /// `"Hello {{ [Name] }}"` — so cross-impl rendering of booleans /
69    /// numbers / empty values is stable.
70    pub fn canonical(&self) -> String {
71        match self {
72            Value::Empty => String::new(),
73            Value::String(s) => s.clone(),
74            Value::Number(n) => canonical_number(*n),
75            Value::DateNumber(n) => crate::functions::serial_to_iso_canonical(*n)
76                .unwrap_or_else(|| canonical_number(*n)),
77            Value::Bool(b) => if *b { "TRUE" } else { "FALSE" }.to_string(),
78            // Internal scaffolding values — defensive empty render.
79            Value::Rows(_) | Value::Map(_) | Value::List(_) => String::new(),
80        }
81    }
82
83    pub fn from_calamine(d: &CalamineData) -> Value {
84        match d {
85            CalamineData::Empty => Value::Empty,
86            CalamineData::String(s) => Value::String(s.clone()),
87            CalamineData::Float(f) => Value::Number(*f),
88            CalamineData::Int(i) => Value::Number(*i as f64),
89            CalamineData::Bool(b) => Value::Bool(*b),
90            CalamineData::DateTime(dt) => Value::Number(dt.as_f64()),
91            CalamineData::DateTimeIso(s) | CalamineData::DurationIso(s) => {
92                Value::String(s.clone())
93            }
94            CalamineData::Error(_) => Value::Empty,
95        }
96    }
97}