Skip to main content

truecalc_workbook/
value.rs

1use std::hash::{Hash, Hasher};
2
3use serde::de::Error as _;
4use serde::ser::{Error as _, SerializeMap};
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6use truecalc_core::types::zoned::parse_rfc9557;
7use truecalc_core::types::{SparklineChartType, SparklineSpec, SparklineValue, ZonedInstant};
8
9/// An evaluated cell value — one of the seven types of schema spec §6.
10///
11/// Wire encodings extend the published `@truecalc/core` npm shapes:
12/// `{ "type": "number", "value": 1.5 }`, with `error` using an `error` key
13/// (`{ "error": "#REF!", "type": "error" }`).
14///
15/// Invariants (schema spec §6 and §8):
16/// - `Number` and `Date` are always finite — NaN and infinity are
17///   unrepresentable; the serializer rejects them and the deserializer
18///   refuses them. `-0.0` is normalized to `0.0` on deserialization, and
19///   equality/hashing treat them as the same value.
20/// - `Array` is row-major, rectangular, non-empty, larger than 1×1 (a 1×1
21///   array is collapsed to its scalar element before storage, schema spec
22///   §6), and holds only scalar values (never a nested `Array`). It appears
23///   only as a spill anchor's value (schema spec §5).
24/// - `Sparkline` plots at least two points, and its option keys are lower-case
25///   and never `charttype`. The serializer rejects a spec that breaks this and
26///   the deserializer refuses one, so a spec that can be written can be read.
27/// - `Zoned` is written, and must be read, as an unpadded RFC-9557 string: the
28///   wire form is canonical, even where the formula-level parsers it delegates
29///   to are lenient about casing and surrounding whitespace.
30#[derive(Debug, Clone)]
31pub enum Value {
32    /// Finite IEEE-754 f64.
33    Number(f64),
34    /// Any Unicode string.
35    Text(String),
36    /// A boolean.
37    Boolean(bool),
38    /// A spreadsheet error code, e.g. `#REF!`. Allowed codes are the
39    /// engine's error set for the workbook's flavor (registry-driven).
40    Error(String),
41    /// An error code carrying an *additive* diagnostic message (Google Sheets
42    /// parity, e.g. the arity message for `DATE()`). The message is in-memory
43    /// metadata only: it is **not** part of value identity (equality and
44    /// hashing compare by code — see the hand-written `PartialEq`/`Hash`) and
45    /// is **dropped** by canonical serialization, so the persisted JSON, the
46    /// `to_json ∘ from_json = id` guarantee, and hash/equality are all
47    /// byte-for-byte unchanged from a bare `Error(code)`. Consumers read it via
48    /// [`Value::error_message`].
49    ErrorMsg(String, String),
50    /// An evaluated-empty result (a formula cell before first recalc, or a
51    /// formula referencing an unauthored cell). Never used to pad the
52    /// sparse grid.
53    Empty,
54    /// Row-major 2-D array of scalar values; a spill anchor's full
55    /// evaluated array.
56    Array(Vec<Vec<Value>>),
57    /// A date as a serial number (fractional part = time of day). The epoch
58    /// is implied by the workbook's engine flavor, never stored per-value.
59    Date(f64),
60    /// A zone-aware instant (Model B). Serialized as its canonical, self-
61    /// describing RFC-9557 string, e.g. `2026-07-14T11:00:00+02:00[Europe/Berlin]`.
62    Zoned(Box<ZonedInstant>),
63    /// A sparkline: the parsed, validated render spec produced by `SPARKLINE`
64    /// (Google Sheets models it as a value kind of its own — `TYPE()` reports
65    /// the undocumented code `128`).
66    ///
67    /// Sheets keeps *two* notions of sameness for a sparkline, and this type
68    /// carries the deeper one. The `=` operator reports any two sparklines
69    /// equal, whatever they plot (that is the engine's
70    /// [`truecalc_core::Value`] equality); `COUNTUNIQUE` nonetheless counts two
71    /// different sparklines as 2 and two identical ones as 1. Storage needs the
72    /// deeper notion: recalc writes a recomputed cell back only when the new
73    /// value differs from the old, so if every sparkline compared equal here a
74    /// changed chart would silently keep its stale spec. Equality and hashing
75    /// therefore compare the whole spec, and canonical JSON carries it in
76    /// full — serializing it lossily (as `""`, or by dropping it and
77    /// recomputing from the formula) would collapse two genuinely different
78    /// sparklines into one canonical form.
79    Sparkline(Box<SparklineSpec>),
80}
81
82/// Bit pattern of a finite f64 with `-0.0` normalized to `0.0`, so that
83/// hashing agrees with `==` (schema spec §8: hash and float equality operate
84/// on post-normalization bit patterns; sound because the serializer rejects NaN).
85fn normalized_bits(x: f64) -> u64 {
86    if x == 0.0 {
87        0.0_f64.to_bits()
88    } else {
89        x.to_bits()
90    }
91}
92
93impl Value {
94    /// The error code if this value is any error variant (bare `Error` or
95    /// message-carrying `ErrorMsg`), else `None`.
96    fn error_code(&self) -> Option<&str> {
97        match self {
98            Value::Error(code) | Value::ErrorMsg(code, _) => Some(code.as_str()),
99            _ => None,
100        }
101    }
102
103    /// The additive diagnostic message attached to an error value, if any.
104    /// Bare errors (and non-errors) return `None`.
105    pub fn error_message(&self) -> Option<&str> {
106        match self {
107            Value::ErrorMsg(_, msg) => Some(msg.as_str()),
108            _ => None,
109        }
110    }
111}
112
113/// Errors compare **by code only** — the diagnostic message is additive
114/// metadata and must never affect identity (a message-carrying error stays
115/// equal to the same bare error code, so canonical round-tripping and grid
116/// deduplication are unchanged). Every non-error arm matches the previous
117/// `#[derive(PartialEq)]` behaviour exactly.
118impl PartialEq for Value {
119    fn eq(&self, other: &Self) -> bool {
120        match (self, other) {
121            (Value::Number(a), Value::Number(b)) => a == b,
122            (Value::Text(a), Value::Text(b)) => a == b,
123            (Value::Boolean(a), Value::Boolean(b)) => a == b,
124            (Value::Empty, Value::Empty) => true,
125            (Value::Array(a), Value::Array(b)) => a == b,
126            (Value::Date(a), Value::Date(b)) => a == b,
127            (Value::Zoned(a), Value::Zoned(b)) => a == b,
128            // Storage identity is the deep (COUNTUNIQUE-grade) one, not the
129            // `=` operator's — see the variant's doc comment.
130            (Value::Sparkline(a), Value::Sparkline(b)) => a == b,
131            _ => match (self.error_code(), other.error_code()) {
132                (Some(a), Some(b)) => a == b,
133                _ => false,
134            },
135        }
136    }
137}
138
139impl Hash for Value {
140    fn hash<H: Hasher>(&self, state: &mut H) {
141        // Both error variants hash identically (message ignored, fixed tag) so
142        // that `Error(code)` and `ErrorMsg(code, _)` — which compare equal —
143        // also hash equal, keeping the Hash/Eq contract intact.
144        if let Some(code) = self.error_code() {
145            "error".hash(state);
146            code.hash(state);
147            return;
148        }
149        std::mem::discriminant(self).hash(state);
150        match self {
151            Value::Number(n) | Value::Date(n) => normalized_bits(*n).hash(state),
152            Value::Text(s) => s.hash(state),
153            Value::Boolean(b) => b.hash(state),
154            // Hash the canonical RFC-9557 form: structurally equal instants
155            // (same utc_nanos + zone) produce the same string, agreeing with `==`.
156            Value::Zoned(z) => z.to_rfc9557().hash(state),
157            Value::Empty => {}
158            Value::Array(rows) => {
159                rows.len().hash(state);
160                for row in rows {
161                    row.len().hash(state);
162                    for v in row {
163                        v.hash(state);
164                    }
165                }
166            }
167            // Hash the whole spec: it is this value's identity, so two
168            // sparklines that compare equal must hash equal.
169            Value::Sparkline(spec) => {
170                spec.chart_type.as_str().hash(state);
171                spec.data.len().hash(state);
172                for point in &spec.data {
173                    hash_sparkline_value(point, state);
174                }
175                spec.options.len().hash(state);
176                for (key, value) in &spec.options {
177                    key.hash(state);
178                    hash_sparkline_value(value, state);
179                }
180            }
181            // Handled above via `error_code()`.
182            Value::Error(_) | Value::ErrorMsg(_, _) => unreachable!(),
183        }
184    }
185}
186
187/// A sparkline data point / option value as an ordinary scalar cell value, so
188/// a spec serializes in the same vocabulary as every other value on the wire.
189fn sparkline_value_to_value(v: &SparklineValue) -> Value {
190    match v {
191        SparklineValue::Number(n) => Value::Number(if *n == 0.0 { 0.0 } else { *n }),
192        SparklineValue::Text(s) => Value::Text(s.clone()),
193        SparklineValue::Bool(b) => Value::Boolean(*b),
194        SparklineValue::Blank => Value::Empty,
195    }
196}
197
198/// The inverse of [`sparkline_value_to_value`]; only scalar cell values can be
199/// a data point or an option value.
200fn value_to_sparkline_value(v: &Value) -> Result<SparklineValue, String> {
201    match v {
202        Value::Number(n) => Ok(SparklineValue::number(*n)),
203        Value::Text(s) => Ok(SparklineValue::Text(s.clone())),
204        Value::Boolean(b) => Ok(SparklineValue::Bool(*b)),
205        Value::Empty => Ok(SparklineValue::Blank),
206        _ => Err(
207            "a sparkline data point or option value must be a number, text, boolean or empty"
208                .to_string(),
209        ),
210    }
211}
212
213fn hash_sparkline_value<H: Hasher>(v: &SparklineValue, state: &mut H) {
214    std::mem::discriminant(v).hash(state);
215    match v {
216        SparklineValue::Number(n) => normalized_bits(*n).hash(state),
217        SparklineValue::Text(s) => s.hash(state),
218        SparklineValue::Bool(b) => b.hash(state),
219        SparklineValue::Blank => {}
220    }
221}
222
223/// Canonical wire form of a parsed sparkline spec. Keys are emitted in
224/// lexicographic order (`charttype` < `data` < `options`) so the encoding is
225/// canonical (JCS) like every other value in this module.
226struct SparklineSpecWire<'a>(&'a SparklineSpec);
227
228impl Serialize for SparklineSpecWire<'_> {
229    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
230        // Enforce the reader's invariants at the writing end too, exactly as
231        // the `Array` arm of `Value`'s serializer does for its own shape rules:
232        // a `SparklineSpec` is a public struct, so it can be hand-built in a
233        // state the evaluator never produces, and emitting it would yield bytes
234        // that neither `parse_sparkline` nor the published schema accepts —
235        // breaking this crate's round-trip guarantee.
236        if self.0.data.len() < 2 {
237            return Err(S::Error::custom(
238                "a sparkline plots at least two points; a shorter spec does not exist \
239                 in serialized form (the evaluator answers #N/A for one point)",
240            ));
241        }
242        for (key, _) in &self.0.options {
243            if *key != key.to_ascii_lowercase() {
244                return Err(S::Error::custom(format!(
245                    "a sparkline option key must be lower-case, got {key:?}"
246                )));
247            }
248            if key == "charttype" {
249                return Err(S::Error::custom(
250                    "charttype is carried by the sparkline's own field, not in options",
251                ));
252            }
253        }
254        let data: Vec<Value> = self.0.data.iter().map(sparkline_value_to_value).collect();
255        let options: Vec<(&str, Value)> = self
256            .0
257            .options
258            .iter()
259            .map(|(k, v)| (k.as_str(), sparkline_value_to_value(v)))
260            .collect();
261        let mut map = serializer.serialize_map(Some(3))?;
262        map.serialize_entry("charttype", self.0.chart_type.as_str())?;
263        map.serialize_entry("data", &data)?;
264        map.serialize_entry("options", &options)?;
265        map.end()
266    }
267}
268
269fn serialize_tagged_number<S: Serializer>(
270    kind: &'static str,
271    n: f64,
272    serializer: S,
273) -> Result<S::Ok, S::Error> {
274    if !n.is_finite() {
275        return Err(S::Error::custom(format!(
276            "non-finite {kind} value cannot be serialized (schema spec §8)"
277        )));
278    }
279    let n = if n == 0.0 { 0.0 } else { n };
280    let mut map = serializer.serialize_map(Some(2))?;
281    map.serialize_entry("type", kind)?;
282    map.serialize_entry("value", &n)?;
283    map.end()
284}
285
286impl Serialize for Value {
287    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
288        match self {
289            Value::Number(n) => serialize_tagged_number("number", *n, serializer),
290            Value::Date(n) => serialize_tagged_number("date", *n, serializer),
291            Value::Zoned(z) => {
292                let mut map = serializer.serialize_map(Some(2))?;
293                map.serialize_entry("type", "zoned")?;
294                map.serialize_entry("value", &z.to_rfc9557())?;
295                map.end()
296            }
297            Value::Text(s) => {
298                let mut map = serializer.serialize_map(Some(2))?;
299                map.serialize_entry("type", "text")?;
300                map.serialize_entry("value", s)?;
301                map.end()
302            }
303            // The full parsed spec, never a lossy projection: it is the value's
304            // identity, so a canonical form that dropped it would make two
305            // different sparklines indistinguishable.
306            Value::Sparkline(spec) => {
307                let mut map = serializer.serialize_map(Some(2))?;
308                map.serialize_entry("type", "sparkline")?;
309                map.serialize_entry("value", &SparklineSpecWire(spec))?;
310                map.end()
311            }
312            Value::Boolean(b) => {
313                let mut map = serializer.serialize_map(Some(2))?;
314                map.serialize_entry("type", "boolean")?;
315                map.serialize_entry("value", b)?;
316                map.end()
317            }
318            // Both error variants serialize identically: the diagnostic message
319            // is in-memory-only metadata and is dropped here, so canonical JSON
320            // (and the round-trip identity guarantee) is byte-for-byte unchanged.
321            Value::Error(code) | Value::ErrorMsg(code, _) => {
322                // Key is `error`, not `value`; emitted before `type` to match
323                // canonical (JCS) key order.
324                let mut map = serializer.serialize_map(Some(2))?;
325                map.serialize_entry("error", code)?;
326                map.serialize_entry("type", "error")?;
327                map.end()
328            }
329            Value::Empty => {
330                let mut map = serializer.serialize_map(Some(2))?;
331                map.serialize_entry("type", "empty")?;
332                map.serialize_entry("value", &())?;
333                map.end()
334            }
335            Value::Array(rows) => {
336                if rows.is_empty() || rows[0].is_empty() {
337                    return Err(S::Error::custom("array value must be non-empty"));
338                }
339                if rows.len() == 1 && rows[0].len() == 1 {
340                    return Err(S::Error::custom(
341                        "a 1x1 array does not exist in serialized form; collapse it \
342                         to its scalar element (schema spec §6)",
343                    ));
344                }
345                let width = rows[0].len();
346                for row in rows {
347                    if row.len() != width {
348                        return Err(S::Error::custom("array value must be rectangular"));
349                    }
350                    for v in row {
351                        if matches!(v, Value::Array(_)) {
352                            return Err(S::Error::custom(
353                                "array elements must be scalar values (no nested arrays)",
354                            ));
355                        }
356                    }
357                }
358                let mut map = serializer.serialize_map(Some(2))?;
359                map.serialize_entry("type", "array")?;
360                map.serialize_entry("value", rows)?;
361                map.end()
362            }
363        }
364    }
365}
366
367impl<'de> Deserialize<'de> for Value {
368    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
369        // Buffer into a JSON tree first: the payload's parse depends on the
370        // `type` tag, and non-canonical input may order keys arbitrarily.
371        let raw = serde_json::Value::deserialize(deserializer)?;
372        parse_value(&raw).map_err(D::Error::custom)
373    }
374}
375
376fn parse_value(raw: &serde_json::Value) -> Result<Value, String> {
377    let obj = match raw.as_object() {
378        Some(obj) => obj,
379        None => return Err("a cell value must be a JSON object".to_string()),
380    };
381    let kind = match obj.get("type").and_then(serde_json::Value::as_str) {
382        Some(kind) => kind,
383        None => return Err("a cell value requires a string \"type\" field".to_string()),
384    };
385    let payload_key = if kind == "error" { "error" } else { "value" };
386    let payload = match obj.get(payload_key) {
387        Some(payload) if obj.len() == 2 => payload,
388        _ => {
389            return Err(format!(
390                "a {kind} value must have exactly the fields \"type\" and \"{payload_key}\""
391            ));
392        }
393    };
394    match kind {
395        "number" => Ok(Value::Number(parse_finite_f64(payload, kind)?)),
396        "date" => Ok(Value::Date(parse_finite_f64(payload, kind)?)),
397        "zoned" => match payload.as_str() {
398            Some(s) => parse_zoned(s),
399            None => Err("a zoned value must be a JSON string".to_string()),
400        },
401        "text" => match payload.as_str() {
402            Some(s) => Ok(Value::Text(s.to_owned())),
403            None => Err("a text value must be a JSON string".to_string()),
404        },
405        "boolean" => match payload.as_bool() {
406            Some(b) => Ok(Value::Boolean(b)),
407            None => Err("a boolean value must be a JSON boolean".to_string()),
408        },
409        "error" => match payload.as_str() {
410            Some(code) => Ok(Value::Error(code.to_owned())),
411            None => Err("an error value must carry a string error code".to_string()),
412        },
413        "empty" => {
414            if payload.is_null() {
415                Ok(Value::Empty)
416            } else {
417                Err("an empty value must be JSON null".to_string())
418            }
419        }
420        "array" => parse_array(payload),
421        "sparkline" => parse_sparkline(payload),
422        other => Err(format!("unknown value type {other:?}")),
423    }
424}
425
426fn parse_finite_f64(payload: &serde_json::Value, kind: &str) -> Result<f64, String> {
427    // `as_f64` is correctly rounded only because the crate enables serde_json's
428    // `float_roundtrip` feature; without it, serde_json's default parser is off
429    // by up to one ULP for some extreme exponents, which would break the
430    // `to_json ∘ from_json = id` byte guarantee (the canonical bytes of the
431    // reparsed value would differ in the last shortest-round-trip digit).
432    let n = match payload.as_f64() {
433        Some(n) => n,
434        None => return Err(format!("a {kind} value must be a JSON number")),
435    };
436    if !n.is_finite() {
437        return Err(format!(
438            "non-finite {kind} value is forbidden (schema spec §8)"
439        ));
440    }
441    // Normalize -0.0 to 0.0 at the value level (schema spec §8).
442    Ok(if n == 0.0 { 0.0 } else { n })
443}
444
445/// Parse the payload of a `zoned` value: the canonical RFC-9557 string, in the
446/// same shape the serializer emits.
447///
448/// `parse_rfc9557` trims the string, and trims the bracketed zone inside it,
449/// because the *formula* level has to accept a padded argument. The wire is
450/// canonical-only, so padding is rejected here rather than silently absorbed —
451/// the serializer never emits it, and accepting it would put a document on disk
452/// that the published schema calls malformed.
453fn parse_zoned(s: &str) -> Result<Value, String> {
454    let zone = s
455        .split_once('[')
456        .and_then(|(_, rest)| rest.strip_suffix(']'));
457    if s.trim() != s || zone.is_some_and(|z| z.trim() != z) {
458        return Err(format!(
459            "a zoned value must not be padded with whitespace, got {s:?}"
460        ));
461    }
462    parse_rfc9557(s)
463        .map(|zi| Value::Zoned(Box::new(zi)))
464        .ok_or_else(|| format!("a zoned value must be a valid RFC-9557 string, got {s:?}"))
465}
466
467/// Parse the payload of a `sparkline` value: the full parsed spec, in the same
468/// shape [`SparklineSpecWire`] emits.
469fn parse_sparkline(payload: &serde_json::Value) -> Result<Value, String> {
470    let obj = payload
471        .as_object()
472        .ok_or_else(|| "a sparkline value must be a JSON object".to_string())?;
473    if obj.len() != 3
474        || !obj.contains_key("charttype")
475        || !obj.contains_key("data")
476        || !obj.contains_key("options")
477    {
478        return Err(
479            "a sparkline value must have exactly the fields \"charttype\", \"data\" and \"options\""
480                .to_string(),
481        );
482    }
483
484    let raw_chart_type = obj["charttype"]
485        .as_str()
486        .ok_or_else(|| "a sparkline charttype must be a JSON string".to_string())?;
487    // `SparklineChartType::parse` is ASCII case-insensitive because the
488    // *formula* level has to accept `=SPARKLINE({1,2},{"charttype","LINE"})`.
489    // The wire is canonical-only — as it already is for option keys below — so
490    // a non-canonical spelling is rejected here rather than silently
491    // normalized: the serializer never emits one, and accepting one would put
492    // a document on disk that the published schema calls malformed.
493    let chart_type = SparklineChartType::parse(raw_chart_type)
494        .ok_or_else(|| format!("unknown sparkline charttype {raw_chart_type:?}"))?;
495    if chart_type.as_str() != raw_chart_type {
496        return Err(format!(
497            "a sparkline charttype must be spelled in its canonical lower-case \
498             form, got {raw_chart_type:?}"
499        ));
500    }
501
502    let raw_data = obj["data"]
503        .as_array()
504        .ok_or_else(|| "sparkline data must be a JSON array".to_string())?;
505    // The evaluator rejects a single-point `data` argument with `#N/A`, so a
506    // shorter spec is unrepresentable and must not round-trip in.
507    if raw_data.len() < 2 {
508        return Err("sparkline data must hold at least two points".to_string());
509    }
510    let mut data = Vec::with_capacity(raw_data.len());
511    for raw in raw_data {
512        data.push(value_to_sparkline_value(&parse_value(raw)?)?);
513    }
514
515    let raw_options = obj["options"]
516        .as_array()
517        .ok_or_else(|| "sparkline options must be a JSON array".to_string())?;
518    let mut options = Vec::with_capacity(raw_options.len());
519    for raw in raw_options {
520        let pair = raw
521            .as_array()
522            .filter(|p| p.len() == 2)
523            .ok_or_else(|| "a sparkline option must be a [key, value] pair".to_string())?;
524        let key = pair[0]
525            .as_str()
526            .ok_or_else(|| "a sparkline option key must be a JSON string".to_string())?;
527        if key != key.to_ascii_lowercase() {
528            return Err(format!("a sparkline option key must be lower-case, got {key:?}"));
529        }
530        if key == "charttype" {
531            return Err(
532                "charttype is carried by the sparkline's own field, not in options".to_string(),
533            );
534        }
535        options.push((key.to_owned(), value_to_sparkline_value(&parse_value(&pair[1])?)?));
536    }
537
538    Ok(Value::Sparkline(Box::new(SparklineSpec {
539        chart_type,
540        data,
541        options,
542    })))
543}
544
545fn parse_array(payload: &serde_json::Value) -> Result<Value, String> {
546    let raw_rows = match payload.as_array() {
547        Some(rows) => rows,
548        None => return Err("an array value must be a 2-D JSON array".to_string()),
549    };
550    if raw_rows.is_empty() {
551        return Err("array value must be non-empty".to_string());
552    }
553    let mut rows = Vec::with_capacity(raw_rows.len());
554    let mut width = None;
555    for raw_row in raw_rows {
556        let raw_row = match raw_row.as_array() {
557            Some(row) => row,
558            None => return Err("array rows must be JSON arrays".to_string()),
559        };
560        if raw_row.is_empty() {
561            return Err("array value must be non-empty".to_string());
562        }
563        match width {
564            None => width = Some(raw_row.len()),
565            Some(w) if w != raw_row.len() => {
566                return Err("array value must be rectangular".to_string());
567            }
568            Some(_) => {}
569        }
570        let mut row = Vec::with_capacity(raw_row.len());
571        for raw_elem in raw_row {
572            let elem = parse_value(raw_elem)?;
573            if matches!(elem, Value::Array(_)) {
574                return Err("array elements must be scalar values (no nested arrays)".to_string());
575            }
576            row.push(elem);
577        }
578        rows.push(row);
579    }
580    if rows.len() == 1 && rows[0].len() == 1 {
581        return Err(
582            "a 1x1 array does not exist in serialized form; it must be collapsed \
583             to its scalar element (schema spec §6)"
584                .to_string(),
585        );
586    }
587    Ok(Value::Array(rows))
588}