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}