Skip to main content

vla_lang/calc/
value.rs

1//! Values and their coercions (KERNEL.7, 2026-10-08): what a formula
2//! computes to, and how one kind becomes another when an operator or a
3//! function wants it.
4//!
5//! The kinds are a cell's: a number (an IEEE 754 double, Excel's own and
6//! VBA's `Double`; `Alonzo/SPEC.md` section 6, rule 5), a text, a truth
7//! value, one of the seven error values a cell can hold, and the empty
8//! cell, which a reference to nothing yields and which a formula never
9//! returns: a formula's empty result settles to 0, as Excel's does.
10//! Coercion is where a spreadsheet's semantics hide (the roadmap's catch
11//! for `KERNEL.7`), so each rule here is one method with Excel's rule in
12//! its comment, and each enters the oracle with a fixture that shows it
13//! (`scripts/recalc/`). The language's own functions carry no tolerance;
14//! the agreement of a computed value with a host's cached one carries the
15//! one stated tolerance, [`agrees`], fifteen significant digits, which is
16//! the precision Excel documents for its arithmetic (`Alonzo/docs/ALGEBRA.md`
17//! section 7, crack 1: written here before the oracle was).
18
19use crate::intrinsics::fold;
20use crate::rows;
21use crate::sheet::number_text;
22
23/// The seven error values a cell can hold, Excel's spelling.
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
25pub enum ErrorKind {
26    /// `#NULL!`: an intersection of two ranges that do not meet.
27    Null,
28    /// `#DIV/0!`
29    Div0,
30    /// `#VALUE!`: an operand of the wrong kind.
31    Value,
32    /// `#REF!`: a reference to a cell or a sheet that is not there.
33    Ref,
34    /// `#NAME?`: a name nothing defines.
35    Name,
36    /// `#NUM!`: a number no cell can hold.
37    Num,
38    /// `#N/A`
39    NA,
40}
41
42impl ErrorKind {
43    /// The text a cell shows, which is the text the file stores.
44    pub fn text(self) -> &'static str {
45        match self {
46            ErrorKind::Null => "#NULL!",
47            ErrorKind::Div0 => "#DIV/0!",
48            ErrorKind::Value => "#VALUE!",
49            ErrorKind::Ref => "#REF!",
50            ErrorKind::Name => "#NAME?",
51            ErrorKind::Num => "#NUM!",
52            ErrorKind::NA => "#N/A",
53        }
54    }
55
56    /// The kind an error text names, without case; `None` for an error this
57    /// version does not hold (`#SPILL!`, `#CALC!`, `#GETTING_DATA`).
58    pub fn parse(text: &str) -> Option<ErrorKind> {
59        match fold(text.trim()).as_str() {
60            "#null!" => Some(ErrorKind::Null),
61            "#div/0!" => Some(ErrorKind::Div0),
62            "#value!" => Some(ErrorKind::Value),
63            "#ref!" => Some(ErrorKind::Ref),
64            "#name?" => Some(ErrorKind::Name),
65            "#num!" => Some(ErrorKind::Num),
66            "#n/a" => Some(ErrorKind::NA),
67            _ => None,
68        }
69    }
70}
71
72/// What a cell holds or a formula computes.
73#[derive(Clone, Debug, PartialEq)]
74pub enum Value {
75    Number(f64),
76    Text(String),
77    Bool(bool),
78    Error(ErrorKind),
79    /// A cell with nothing in it. An operand that is empty reads as 0, as
80    /// `""` or as FALSE, whichever the operator wants; a formula whose
81    /// result is empty settles to 0 ([`Value::settled`]).
82    Empty,
83}
84
85impl Value {
86    /// The number an arithmetic operator reads: a number itself; a truth
87    /// value 1 or 0; an empty cell 0; a text its number when it spells one
88    /// (`"5"`, `" 1e3 "`, `"50%"`) and `#VALUE!` otherwise; an error the
89    /// error. Excel's rule for `+`, `-`, `*`, `/`, `^` and a function's
90    /// numeric argument given directly (a value read out of a range follows
91    /// the range rule instead, which each function states).
92    pub fn number(&self) -> Result<f64, ErrorKind> {
93        match self {
94            Value::Number(v) => Ok(*v),
95            Value::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
96            Value::Empty => Ok(0.0),
97            Value::Text(t) => text_to_number(t).ok_or(ErrorKind::Value),
98            Value::Error(k) => Err(*k),
99        }
100    }
101
102    /// The text `&` reads: a text itself; a number in Excel's General
103    /// spelling ([`number_to_text`]); a truth value `TRUE` or `FALSE`; an
104    /// empty cell `""`; an error the error.
105    pub fn text(&self) -> Result<String, ErrorKind> {
106        match self {
107            Value::Text(t) => Ok(t.clone()),
108            Value::Number(v) => Ok(number_to_text(*v)),
109            Value::Bool(true) => Ok("TRUE".to_string()),
110            Value::Bool(false) => Ok("FALSE".to_string()),
111            Value::Empty => Ok(String::new()),
112            Value::Error(k) => Err(*k),
113        }
114    }
115
116    /// The truth value `IF`, `AND`, `OR` and `NOT` read: a truth value
117    /// itself; a number true unless 0; an empty cell false; a text only
118    /// when it spells `TRUE` or `FALSE`, `#VALUE!` otherwise; an error the
119    /// error.
120    pub fn truth(&self) -> Result<bool, ErrorKind> {
121        match self {
122            Value::Bool(b) => Ok(*b),
123            Value::Number(v) => Ok(*v != 0.0),
124            Value::Empty => Ok(false),
125            Value::Text(t) => match fold(t).as_str() {
126                "true" => Ok(true),
127                "false" => Ok(false),
128                _ => Err(ErrorKind::Value),
129            },
130            Value::Error(k) => Err(*k),
131        }
132    }
133
134    /// A formula's result as its cell holds it: an empty result is 0, as
135    /// `=A1` over an empty A1 shows 0 and is a number to `ISNUMBER`.
136    pub fn settled(self) -> Value {
137        match self {
138            Value::Empty => Value::Number(0.0),
139            other => other,
140        }
141    }
142
143    /// The value as a datum, spelled as a `cell` row spells one
144    /// (`crate::rows::datum`): a number in its shortest spelling, a text
145    /// quoted, `true` or `false`, `(error "#DIV/0!")`; a number no cell can
146    /// hold is `#NUM!`, as the writer spells it; an empty value is 0.
147    pub fn spell(&self) -> String {
148        match self {
149            Value::Number(v) if *v == 0.0 => "0".to_string(),
150            Value::Number(v) => match number_text(*v) {
151                Some(text) => text,
152                None => rows::datum(&rows::Value::Error(ErrorKind::Num.text().to_string())),
153            },
154            Value::Text(t) => rows::quoted(t),
155            Value::Bool(true) => "true".to_string(),
156            Value::Bool(false) => "false".to_string(),
157            Value::Error(k) => rows::datum(&rows::Value::Error(k.text().to_string())),
158            Value::Empty => "0".to_string(),
159        }
160    }
161
162    /// A cached value as the file holds it, read as a computed kind: the
163    /// file's number text parsed, a text, a truth value, one of the seven
164    /// errors. `None` for a value this version does not compare: a date
165    /// cell, a number text that is not a number, an error outside the
166    /// seven.
167    pub fn from_file(v: &rows::Value) -> Option<Value> {
168        match v {
169            rows::Value::Number(text) => {
170                let n: f64 = text.trim().parse().ok()?;
171                n.is_finite().then_some(Value::Number(n))
172            }
173            rows::Value::Text(t) => Some(Value::Text(t.clone())),
174            rows::Value::Bool(b) => Some(Value::Bool(*b)),
175            rows::Value::Error(text) => ErrorKind::parse(text).map(Value::Error),
176            rows::Value::Date(_) => None,
177        }
178    }
179}
180
181/// A number rounded to fifteen significant digits, the precision Excel
182/// documents for its arithmetic; 0 and a value that is not finite stay as
183/// they are.
184pub fn round15(v: f64) -> f64 {
185    if v == 0.0 || !v.is_finite() {
186        return v;
187    }
188    format!("{v:.14e}").parse().unwrap_or(v)
189}
190
191/// A number's text as Excel's General format spells it for `&` and for a
192/// text argument: fifteen significant digits at most, fixed while the
193/// decimal exponent is between -14 and 14, and Excel's scientific spelling
194/// outside that, `1E+20`, `1.5E-15`, the exponent signed and at least two
195/// digits. Measured: Excel's save of the subset fixture spells 1E-7 fixed,
196/// `0.0000001`, and 1E+20 and 1E+21 scientific
197/// (`scripts/recalc/subset_saved.xlsx`, `Output!C21` to `C23`). The two
198/// boundaries are LibreOffice's automatic number format's (`nExp <= -15 ||
199/// nExp >= 15` in `sal/rtl/strtmpl.hxx`, `doubleToString`), which both
200/// measured points agree with; `scripts/recalc/edges.txt` holds them for an
201/// Excel save to witness.
202pub fn number_to_text(v: f64) -> String {
203    if v == 0.0 {
204        return "0".to_string();
205    }
206    let r = round15(v);
207    let sci = format!("{r:.14e}");
208    let (mantissa, exponent) = sci.split_once('e').unwrap_or((sci.as_str(), "0"));
209    let exponent: i32 = exponent.parse().unwrap_or(0);
210    if (-14..=14).contains(&exponent) {
211        // Fixed: the shortest spelling of a value already rounded to fifteen
212        // significant digits is those digits, and Rust never writes an
213        // exponent in this form.
214        return format!("{r}");
215    }
216    let mantissa = mantissa.trim_end_matches('0').trim_end_matches('.');
217    format!(
218        "{mantissa}E{}{:02}",
219        if exponent < 0 { '-' } else { '+' },
220        exponent.abs()
221    )
222}
223
224/// The number a text spells, as an arithmetic operator reads it: blanks
225/// around it ignored, an optional sign, digits with at most one `.`, an
226/// optional exponent, an optional `%` at the end dividing by 100. Nothing
227/// else is a number here: a thousands separator, a currency sign and a
228/// date are a locale's, and the locale is the host's.
229pub fn text_to_number(text: &str) -> Option<f64> {
230    let t = text.trim();
231    let (t, percent) = match t.strip_suffix('%') {
232        Some(rest) => (rest.trim_end(), true),
233        None => (t, false),
234    };
235    let bytes = t.as_bytes();
236    if bytes.is_empty() {
237        return None;
238    }
239    let mut i = 0;
240    if bytes[i] == b'+' || bytes[i] == b'-' {
241        i += 1;
242    }
243    let mut digits = 0;
244    let mut dot = false;
245    while i < bytes.len() {
246        match bytes[i] {
247            b'0'..=b'9' => digits += 1,
248            b'.' if !dot => dot = true,
249            _ => break,
250        }
251        i += 1;
252    }
253    if digits == 0 {
254        return None;
255    }
256    if i < bytes.len() && (bytes[i] == b'e' || bytes[i] == b'E') {
257        let mut j = i + 1;
258        if j < bytes.len() && (bytes[j] == b'+' || bytes[j] == b'-') {
259            j += 1;
260        }
261        let first = j;
262        while j < bytes.len() && bytes[j].is_ascii_digit() {
263            j += 1;
264        }
265        if j == first {
266            return None;
267        }
268        i = j;
269    }
270    if i != bytes.len() {
271        return None;
272    }
273    let v: f64 = t.parse().ok()?;
274    let v = if percent { v / 100.0 } else { v };
275    v.is_finite().then_some(v)
276}
277
278/// Whether a computed value agrees with the value a host saved in the
279/// file: two numbers when they round to the same fifteen significant
280/// digits (the stated tolerance), two texts when equal, two truth values
281/// or two errors when the same; `None` when the file's value is one this
282/// version does not compare (a date; see [`Value::from_file`]).
283pub fn agrees(computed: &Value, cached: &rows::Value) -> Option<bool> {
284    let cached = Value::from_file(cached)?;
285    let computed = computed.clone().settled();
286    Some(match (&computed, &cached) {
287        (Value::Number(a), Value::Number(b)) => {
288            let key = |x: f64| format!("{:.14e}", if x == 0.0 { 0.0 } else { round15(x) });
289            key(*a) == key(*b)
290        }
291        (Value::Text(a), Value::Text(b)) => a == b,
292        (Value::Bool(a), Value::Bool(b)) => a == b,
293        (Value::Error(a), Value::Error(b)) => a == b,
294        _ => false,
295    })
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301
302    #[test]
303    fn the_seven_errors_spell_and_parse() {
304        for k in [
305            ErrorKind::Null,
306            ErrorKind::Div0,
307            ErrorKind::Value,
308            ErrorKind::Ref,
309            ErrorKind::Name,
310            ErrorKind::Num,
311            ErrorKind::NA,
312        ] {
313            assert_eq!(ErrorKind::parse(k.text()), Some(k));
314            assert_eq!(ErrorKind::parse(&k.text().to_lowercase()), Some(k));
315        }
316        assert_eq!(ErrorKind::parse("#SPILL!"), None);
317    }
318
319    #[test]
320    fn numbers_are_read_as_excel_reads_an_operand() {
321        assert_eq!(Value::Text("5".into()).number(), Ok(5.0));
322        assert_eq!(Value::Text(" 1e3 ".into()).number(), Ok(1000.0));
323        assert_eq!(Value::Text("50%".into()).number(), Ok(0.5));
324        assert_eq!(Value::Text("-.5".into()).number(), Ok(-0.5));
325        assert_eq!(Value::Text("abc".into()).number(), Err(ErrorKind::Value));
326        assert_eq!(Value::Text("".into()).number(), Err(ErrorKind::Value));
327        assert_eq!(Value::Text("1,000".into()).number(), Err(ErrorKind::Value));
328        assert_eq!(Value::Bool(true).number(), Ok(1.0));
329        assert_eq!(Value::Empty.number(), Ok(0.0));
330        assert_eq!(Value::Error(ErrorKind::NA).number(), Err(ErrorKind::NA));
331    }
332
333    #[test]
334    fn texts_are_excel_s_general_spelling() {
335        assert_eq!(number_to_text(15.0), "15");
336        assert_eq!(number_to_text(0.1 + 0.2), "0.3");
337        assert_eq!(number_to_text(1.0 / 3.0), "0.333333333333333");
338        assert_eq!(number_to_text(-2.5), "-2.5");
339        assert_eq!(number_to_text(0.000001), "0.000001");
340        // Measured: Excel's save of the subset fixture (subset_saved.xlsx,
341        // Output!C21 to C23).
342        assert_eq!(number_to_text(0.0000001), "0.0000001");
343        assert_eq!(number_to_text(1e20), "1E+20");
344        assert_eq!(number_to_text(1e21), "1E+21");
345        // The boundaries the rule takes from LibreOffice, held for Excel by
346        // scripts/recalc/edges.txt.
347        assert_eq!(number_to_text(1e14), "100000000000000");
348        assert_eq!(number_to_text(1e15), "1E+15");
349        assert_eq!(number_to_text(999_999_999_999_999.0), "999999999999999");
350        assert_eq!(number_to_text(999_999_999_999_999.9), "1E+15");
351        assert_eq!(number_to_text(2f64.powi(57)), "1.44115188075856E+17");
352        assert_eq!(number_to_text(1e-14), "0.00000000000001");
353        assert_eq!(number_to_text(1e-15), "1E-15");
354        assert_eq!(number_to_text(1.5e-7), "0.00000015");
355        assert_eq!(number_to_text(-0.0000001), "-0.0000001");
356        assert_eq!(
357            number_to_text(1.0 / 3.0 * 1e-10),
358            "0.0000000000333333333333333"
359        );
360        assert_eq!(number_to_text(2f64.sqrt()), "1.4142135623731");
361        assert_eq!(number_to_text(0.0), "0");
362        assert_eq!(Value::Bool(true).text(), Ok("TRUE".to_string()));
363        assert_eq!(Value::Empty.text(), Ok(String::new()));
364    }
365
366    #[test]
367    fn truth_is_read_from_numbers_and_the_two_words() {
368        assert_eq!(Value::Number(2.0).truth(), Ok(true));
369        assert_eq!(Value::Number(0.0).truth(), Ok(false));
370        assert_eq!(Value::Text("true".into()).truth(), Ok(true));
371        assert_eq!(Value::Text("yes".into()).truth(), Err(ErrorKind::Value));
372        assert_eq!(Value::Empty.truth(), Ok(false));
373    }
374
375    #[test]
376    fn a_value_spells_as_a_cell_row_does() {
377        assert_eq!(Value::Number(15.0).spell(), "15");
378        assert_eq!(Value::Number(0.6).spell(), "0.6");
379        assert_eq!(Value::Text("big".into()).spell(), "\"big\"");
380        assert_eq!(Value::Bool(false).spell(), "false");
381        assert_eq!(Value::Error(ErrorKind::Div0).spell(), "(error \"#DIV/0!\")");
382        assert_eq!(Value::Number(f64::INFINITY).spell(), "(error \"#NUM!\")");
383        assert_eq!(Value::Empty.spell(), "0");
384        assert_eq!(Value::Empty.settled(), Value::Number(0.0));
385    }
386
387    #[test]
388    fn agreement_carries_the_stated_tolerance_and_nothing_more() {
389        let cached = |s: &str| rows::Value::Number(s.to_string());
390        assert_eq!(
391            agrees(&Value::Number(0.1 + 0.2), &cached("0.3")),
392            Some(true)
393        );
394        assert_eq!(
395            agrees(&Value::Number(0.1 + 0.2), &cached("0.30000000000000004")),
396            Some(true)
397        );
398        assert_eq!(agrees(&Value::Number(15.0), &cached("15")), Some(true));
399        assert_eq!(
400            agrees(&Value::Number(15.0), &cached("15.000000000001")),
401            Some(false)
402        );
403        assert_eq!(agrees(&Value::Empty, &cached("0")), Some(true));
404        assert_eq!(
405            agrees(&Value::Text("big".into()), &rows::Value::Text("big".into())),
406            Some(true)
407        );
408        assert_eq!(
409            agrees(&Value::Text("big".into()), &rows::Value::Text("Big".into())),
410            Some(false)
411        );
412        assert_eq!(
413            agrees(
414                &Value::Error(ErrorKind::Div0),
415                &rows::Value::Error("#DIV/0!".into())
416            ),
417            Some(true)
418        );
419        assert_eq!(
420            agrees(&Value::Number(1.0), &rows::Value::Date("2026-10-08".into())),
421            None
422        );
423        assert_eq!(
424            agrees(&Value::Number(1.0), &rows::Value::Bool(true)),
425            Some(false)
426        );
427    }
428}