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::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#[derive(Debug, Clone)]
25pub enum Value {
26    /// Finite IEEE-754 f64.
27    Number(f64),
28    /// Any Unicode string.
29    Text(String),
30    /// A boolean.
31    Boolean(bool),
32    /// A spreadsheet error code, e.g. `#REF!`. Allowed codes are the
33    /// engine's error set for the workbook's flavor (registry-driven).
34    Error(String),
35    /// An error code carrying an *additive* diagnostic message (Google Sheets
36    /// parity, e.g. the arity message for `DATE()`). The message is in-memory
37    /// metadata only: it is **not** part of value identity (equality and
38    /// hashing compare by code — see the hand-written `PartialEq`/`Hash`) and
39    /// is **dropped** by canonical serialization, so the persisted JSON, the
40    /// `to_json ∘ from_json = id` guarantee, and hash/equality are all
41    /// byte-for-byte unchanged from a bare `Error(code)`. Consumers read it via
42    /// [`Value::error_message`].
43    ErrorMsg(String, String),
44    /// An evaluated-empty result (a formula cell before first recalc, or a
45    /// formula referencing an unauthored cell). Never used to pad the
46    /// sparse grid.
47    Empty,
48    /// Row-major 2-D array of scalar values; a spill anchor's full
49    /// evaluated array.
50    Array(Vec<Vec<Value>>),
51    /// A date as a serial number (fractional part = time of day). The epoch
52    /// is implied by the workbook's engine flavor, never stored per-value.
53    Date(f64),
54    /// A zone-aware instant (Model B). Serialized as its canonical, self-
55    /// describing RFC-9557 string, e.g. `2026-07-14T11:00:00+02:00[Europe/Berlin]`.
56    Zoned(Box<ZonedInstant>),
57}
58
59/// Bit pattern of a finite f64 with `-0.0` normalized to `0.0`, so that
60/// hashing agrees with `==` (schema spec §8: hash and float equality operate
61/// on post-normalization bit patterns; sound because the serializer rejects NaN).
62fn normalized_bits(x: f64) -> u64 {
63    if x == 0.0 {
64        0.0_f64.to_bits()
65    } else {
66        x.to_bits()
67    }
68}
69
70impl Value {
71    /// The error code if this value is any error variant (bare `Error` or
72    /// message-carrying `ErrorMsg`), else `None`.
73    fn error_code(&self) -> Option<&str> {
74        match self {
75            Value::Error(code) | Value::ErrorMsg(code, _) => Some(code.as_str()),
76            _ => None,
77        }
78    }
79
80    /// The additive diagnostic message attached to an error value, if any.
81    /// Bare errors (and non-errors) return `None`.
82    pub fn error_message(&self) -> Option<&str> {
83        match self {
84            Value::ErrorMsg(_, msg) => Some(msg.as_str()),
85            _ => None,
86        }
87    }
88}
89
90/// Errors compare **by code only** — the diagnostic message is additive
91/// metadata and must never affect identity (a message-carrying error stays
92/// equal to the same bare error code, so canonical round-tripping and grid
93/// deduplication are unchanged). Every non-error arm matches the previous
94/// `#[derive(PartialEq)]` behaviour exactly.
95impl PartialEq for Value {
96    fn eq(&self, other: &Self) -> bool {
97        match (self, other) {
98            (Value::Number(a), Value::Number(b)) => a == b,
99            (Value::Text(a), Value::Text(b)) => a == b,
100            (Value::Boolean(a), Value::Boolean(b)) => a == b,
101            (Value::Empty, Value::Empty) => true,
102            (Value::Array(a), Value::Array(b)) => a == b,
103            (Value::Date(a), Value::Date(b)) => a == b,
104            (Value::Zoned(a), Value::Zoned(b)) => a == b,
105            _ => match (self.error_code(), other.error_code()) {
106                (Some(a), Some(b)) => a == b,
107                _ => false,
108            },
109        }
110    }
111}
112
113impl Hash for Value {
114    fn hash<H: Hasher>(&self, state: &mut H) {
115        // Both error variants hash identically (message ignored, fixed tag) so
116        // that `Error(code)` and `ErrorMsg(code, _)` — which compare equal —
117        // also hash equal, keeping the Hash/Eq contract intact.
118        if let Some(code) = self.error_code() {
119            "error".hash(state);
120            code.hash(state);
121            return;
122        }
123        std::mem::discriminant(self).hash(state);
124        match self {
125            Value::Number(n) | Value::Date(n) => normalized_bits(*n).hash(state),
126            Value::Text(s) => s.hash(state),
127            Value::Boolean(b) => b.hash(state),
128            // Hash the canonical RFC-9557 form: structurally equal instants
129            // (same utc_nanos + zone) produce the same string, agreeing with `==`.
130            Value::Zoned(z) => z.to_rfc9557().hash(state),
131            Value::Empty => {}
132            Value::Array(rows) => {
133                rows.len().hash(state);
134                for row in rows {
135                    row.len().hash(state);
136                    for v in row {
137                        v.hash(state);
138                    }
139                }
140            }
141            // Handled above via `error_code()`.
142            Value::Error(_) | Value::ErrorMsg(_, _) => unreachable!(),
143        }
144    }
145}
146
147fn serialize_tagged_number<S: Serializer>(
148    kind: &'static str,
149    n: f64,
150    serializer: S,
151) -> Result<S::Ok, S::Error> {
152    if !n.is_finite() {
153        return Err(S::Error::custom(format!(
154            "non-finite {kind} value cannot be serialized (schema spec §8)"
155        )));
156    }
157    let n = if n == 0.0 { 0.0 } else { n };
158    let mut map = serializer.serialize_map(Some(2))?;
159    map.serialize_entry("type", kind)?;
160    map.serialize_entry("value", &n)?;
161    map.end()
162}
163
164impl Serialize for Value {
165    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
166        match self {
167            Value::Number(n) => serialize_tagged_number("number", *n, serializer),
168            Value::Date(n) => serialize_tagged_number("date", *n, serializer),
169            Value::Zoned(z) => {
170                let mut map = serializer.serialize_map(Some(2))?;
171                map.serialize_entry("type", "zoned")?;
172                map.serialize_entry("value", &z.to_rfc9557())?;
173                map.end()
174            }
175            Value::Text(s) => {
176                let mut map = serializer.serialize_map(Some(2))?;
177                map.serialize_entry("type", "text")?;
178                map.serialize_entry("value", s)?;
179                map.end()
180            }
181            Value::Boolean(b) => {
182                let mut map = serializer.serialize_map(Some(2))?;
183                map.serialize_entry("type", "boolean")?;
184                map.serialize_entry("value", b)?;
185                map.end()
186            }
187            // Both error variants serialize identically: the diagnostic message
188            // is in-memory-only metadata and is dropped here, so canonical JSON
189            // (and the round-trip identity guarantee) is byte-for-byte unchanged.
190            Value::Error(code) | Value::ErrorMsg(code, _) => {
191                // Key is `error`, not `value`; emitted before `type` to match
192                // canonical (JCS) key order.
193                let mut map = serializer.serialize_map(Some(2))?;
194                map.serialize_entry("error", code)?;
195                map.serialize_entry("type", "error")?;
196                map.end()
197            }
198            Value::Empty => {
199                let mut map = serializer.serialize_map(Some(2))?;
200                map.serialize_entry("type", "empty")?;
201                map.serialize_entry("value", &())?;
202                map.end()
203            }
204            Value::Array(rows) => {
205                if rows.is_empty() || rows[0].is_empty() {
206                    return Err(S::Error::custom("array value must be non-empty"));
207                }
208                if rows.len() == 1 && rows[0].len() == 1 {
209                    return Err(S::Error::custom(
210                        "a 1x1 array does not exist in serialized form; collapse it \
211                         to its scalar element (schema spec §6)",
212                    ));
213                }
214                let width = rows[0].len();
215                for row in rows {
216                    if row.len() != width {
217                        return Err(S::Error::custom("array value must be rectangular"));
218                    }
219                    for v in row {
220                        if matches!(v, Value::Array(_)) {
221                            return Err(S::Error::custom(
222                                "array elements must be scalar values (no nested arrays)",
223                            ));
224                        }
225                    }
226                }
227                let mut map = serializer.serialize_map(Some(2))?;
228                map.serialize_entry("type", "array")?;
229                map.serialize_entry("value", rows)?;
230                map.end()
231            }
232        }
233    }
234}
235
236impl<'de> Deserialize<'de> for Value {
237    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
238        // Buffer into a JSON tree first: the payload's parse depends on the
239        // `type` tag, and non-canonical input may order keys arbitrarily.
240        let raw = serde_json::Value::deserialize(deserializer)?;
241        parse_value(&raw).map_err(D::Error::custom)
242    }
243}
244
245fn parse_value(raw: &serde_json::Value) -> Result<Value, String> {
246    let obj = match raw.as_object() {
247        Some(obj) => obj,
248        None => return Err("a cell value must be a JSON object".to_string()),
249    };
250    let kind = match obj.get("type").and_then(serde_json::Value::as_str) {
251        Some(kind) => kind,
252        None => return Err("a cell value requires a string \"type\" field".to_string()),
253    };
254    let payload_key = if kind == "error" { "error" } else { "value" };
255    let payload = match obj.get(payload_key) {
256        Some(payload) if obj.len() == 2 => payload,
257        _ => {
258            return Err(format!(
259                "a {kind} value must have exactly the fields \"type\" and \"{payload_key}\""
260            ));
261        }
262    };
263    match kind {
264        "number" => Ok(Value::Number(parse_finite_f64(payload, kind)?)),
265        "date" => Ok(Value::Date(parse_finite_f64(payload, kind)?)),
266        "zoned" => match payload.as_str() {
267            Some(s) => parse_rfc9557(s)
268                .map(|zi| Value::Zoned(Box::new(zi)))
269                .ok_or_else(|| format!("a zoned value must be a valid RFC-9557 string, got {s:?}")),
270            None => Err("a zoned value must be a JSON string".to_string()),
271        },
272        "text" => match payload.as_str() {
273            Some(s) => Ok(Value::Text(s.to_owned())),
274            None => Err("a text value must be a JSON string".to_string()),
275        },
276        "boolean" => match payload.as_bool() {
277            Some(b) => Ok(Value::Boolean(b)),
278            None => Err("a boolean value must be a JSON boolean".to_string()),
279        },
280        "error" => match payload.as_str() {
281            Some(code) => Ok(Value::Error(code.to_owned())),
282            None => Err("an error value must carry a string error code".to_string()),
283        },
284        "empty" => {
285            if payload.is_null() {
286                Ok(Value::Empty)
287            } else {
288                Err("an empty value must be JSON null".to_string())
289            }
290        }
291        "array" => parse_array(payload),
292        other => Err(format!("unknown value type {other:?}")),
293    }
294}
295
296fn parse_finite_f64(payload: &serde_json::Value, kind: &str) -> Result<f64, String> {
297    // `as_f64` is correctly rounded only because the crate enables serde_json's
298    // `float_roundtrip` feature; without it, serde_json's default parser is off
299    // by up to one ULP for some extreme exponents, which would break the
300    // `to_json ∘ from_json = id` byte guarantee (the canonical bytes of the
301    // reparsed value would differ in the last shortest-round-trip digit).
302    let n = match payload.as_f64() {
303        Some(n) => n,
304        None => return Err(format!("a {kind} value must be a JSON number")),
305    };
306    if !n.is_finite() {
307        return Err(format!(
308            "non-finite {kind} value is forbidden (schema spec §8)"
309        ));
310    }
311    // Normalize -0.0 to 0.0 at the value level (schema spec §8).
312    Ok(if n == 0.0 { 0.0 } else { n })
313}
314
315fn parse_array(payload: &serde_json::Value) -> Result<Value, String> {
316    let raw_rows = match payload.as_array() {
317        Some(rows) => rows,
318        None => return Err("an array value must be a 2-D JSON array".to_string()),
319    };
320    if raw_rows.is_empty() {
321        return Err("array value must be non-empty".to_string());
322    }
323    let mut rows = Vec::with_capacity(raw_rows.len());
324    let mut width = None;
325    for raw_row in raw_rows {
326        let raw_row = match raw_row.as_array() {
327            Some(row) => row,
328            None => return Err("array rows must be JSON arrays".to_string()),
329        };
330        if raw_row.is_empty() {
331            return Err("array value must be non-empty".to_string());
332        }
333        match width {
334            None => width = Some(raw_row.len()),
335            Some(w) if w != raw_row.len() => {
336                return Err("array value must be rectangular".to_string());
337            }
338            Some(_) => {}
339        }
340        let mut row = Vec::with_capacity(raw_row.len());
341        for raw_elem in raw_row {
342            let elem = parse_value(raw_elem)?;
343            if matches!(elem, Value::Array(_)) {
344                return Err("array elements must be scalar values (no nested arrays)".to_string());
345            }
346            row.push(elem);
347        }
348        rows.push(row);
349    }
350    if rows.len() == 1 && rows[0].len() == 1 {
351        return Err(
352            "a 1x1 array does not exist in serialized form; it must be collapsed \
353             to its scalar element (schema spec §6)"
354                .to_string(),
355        );
356    }
357    Ok(Value::Array(rows))
358}