visi_core/core/engine/result_data.rs
1use serde::{Deserialize, Serialize};
2
3/// What a cell holds once it has been evaluated.
4///
5/// There is deliberately **no date variant**. As in Excel, a date is a plain
6/// numeric serial and the notation it was typed in lives on the cell, as
7/// `CellStyle::num_format` -- so `ISNUMBER` is true for a date, `SUM` counts
8/// it, and every numeric path works on it untouched. Only rendering consults
9/// the format, through `Sheet::get_display_string`.
10///
11/// An Excel error is a *value*, not a Rust error: `=1/0` evaluates
12/// successfully to `Error("#DIV/0!")`. See [`EngineError`] for the failures
13/// that are not values.
14///
15/// [`EngineError`]: crate::core::EngineError
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub enum ResultData {
18 /// A blank cell. Coerces to 0 or `""` depending on what reads it.
19 None,
20 /// `TRUE` or `FALSE`.
21 Boolean(bool),
22 /// A whole number.
23 Integer(i64),
24 /// A number that is not a whole number, or one too large for an `i64`.
25 /// A date is a `Float` holding its Excel serial.
26 Float(f64),
27 /// Text.
28 String(String),
29 /// An ordered sequence, for the engine-specific functions that return one.
30 /// Not an Excel array.
31 List(Vec<ResultData>),
32 /// Key/value pairs, for the engine-specific functions that return them.
33 Dict(Vec<(ResultData, ResultData)>),
34 /// An Excel error value, held as its code: `#DIV/0!`, `#VALUE!`, `#N/A`.
35 Error(String),
36}
37
38impl std::fmt::Display for ResultData {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 match self {
41 ResultData::None => write!(f, ""),
42 ResultData::Boolean(b) => write!(f, "{}", if *b { "TRUE" } else { "FALSE" }),
43 ResultData::Integer(i) => write!(f, "{}", i),
44 ResultData::Float(fl) => write!(f, "{}", format_excel_number(*fl)),
45 ResultData::String(s) => write!(f, "{}", s),
46 ResultData::List(l) => {
47 let items: Vec<String> = l.iter().map(|i| i.to_string()).collect();
48 write!(f, "[{}]", items.join(", "))
49 }
50 ResultData::Dict(d) => {
51 let items: Vec<String> = d.iter().map(|(k, v)| format!("{}: {}", k, v)).collect();
52 write!(f, "{{ {} }}", items.join(", "))
53 }
54 ResultData::Error(e) => write!(f, "Error: {}", e),
55 }
56 }
57}
58
59/// Rounds a significant-digit string to `keep` digits, half away from zero,
60/// trimming the trailing zeros Excel does not display. Returns the digits
61/// and the (possibly incremented) exponent -- rounding 999 up to 100 shifts
62/// the decimal point.
63fn round_digits_half_up(digits: &str, exp: i32, keep: usize) -> (String, i32) {
64 if digits.len() <= keep {
65 return (digits.to_string(), exp);
66 }
67 let mut kept: Vec<u8> = digits.as_bytes()[..keep].to_vec();
68 let round_up = digits.as_bytes()[keep] >= b'5';
69 let mut exp = exp;
70 if round_up {
71 let mut i = keep;
72 loop {
73 if i == 0 {
74 // Every digit carried: 999... becomes 1000..., one decimal
75 // place further left.
76 kept.insert(0, b'1');
77 kept.pop();
78 exp += 1;
79 break;
80 }
81 i -= 1;
82 if kept[i] == b'9' {
83 kept[i] = b'0';
84 } else {
85 kept[i] += 1;
86 break;
87 }
88 }
89 }
90 let mut out = String::from_utf8(kept).expect("ascii digits");
91 while out.len() > 1 && out.ends_with('0') {
92 out.pop();
93 }
94 (out, exp)
95}
96
97/// The Excel error values, spelled exactly as a cell shows them.
98///
99/// A closed set: these are the only strings a cell can hold that are an error
100/// rather than text, which is what makes recognising one on entry safe.
101pub(crate) const EXCEL_ERROR_CODES: &[&str] = &[
102 "#NULL!", "#DIV/0!", "#VALUE!", "#REF!", "#NAME?", "#NUM!", "#N/A", "#CALC!", "#SPILL!",
103];
104
105/// Whether a literal cell entry is one of Excel's error values.
106///
107/// Typing `#NUM!` into Excel produces the error, not the text -- measured,
108/// along with the same thing happening when VBA assigns the string through
109/// `Range.Value`. So `Sheet::commit` recognises one, and `xlsx::text_cell_src`
110/// quotes it on import for the same reason it quotes `TRUE` and `6/22/26`:
111/// a cell Excel told us is *text* has to survive the round trip as text.
112pub(crate) fn is_excel_error_code(src: &str) -> bool {
113 EXCEL_ERROR_CODES
114 .iter()
115 .any(|e| src.eq_ignore_ascii_case(e))
116}
117
118pub(crate) fn format_excel_number(f: f64) -> String {
119 if f == 0.0 {
120 return "0".to_string();
121 }
122 if f.is_nan() || f.is_infinite() {
123 return "#NUM!".to_string();
124 }
125
126 // Excel displays 15 significant digits and no more. Everything below
127 // works from the *rounded* scientific form rather than from the f64
128 // directly, so digits past that precision are dropped instead of
129 // leaking out: (-43)^11 is 21611482313284248 in f64, but Excel writes
130 // 21611482313284200.
131 let sci = format!("{:.14e}", f);
132 let (mantissa, exp_str) = sci.split_once('e').expect("{:e} always emits an exponent");
133 let exp: i32 = exp_str.parse().expect("{:e} emits an integer exponent");
134 let sign = if f < 0.0 { "-" } else { "" };
135 let digits: String = mantissa.chars().filter(|c| c.is_ascii_digit()).collect();
136 let digits = digits.trim_end_matches('0');
137 let digits = if digits.is_empty() { "0" } else { digits };
138
139 // Excel keeps plain decimal notation for as long as the decimal
140 // rendering stays within 20 characters, and only then falls back to
141 // scientific. The minus sign is *not* charged against that budget --
142 // real Excel writes -2.05237592634038E-10, which is 21 characters.
143 // Verified against real Excel: 1e18 and 1e19 render in full (19 and 20
144 // characters) while 1e20 (21) goes scientific, and 0.000001207666770903
145 // renders in full (20) while 0.00000120766677090395 (22) goes
146 // scientific.
147 let decimal_len = if exp >= 0 {
148 let int_digits = (exp + 1) as usize;
149 let frac_digits = digits.len().saturating_sub(int_digits);
150 int_digits + usize::from(frac_digits > 0) + frac_digits
151 } else {
152 // "0." + leading zeros + significant digits
153 2 + (-exp - 1) as usize + digits.len()
154 };
155
156 if decimal_len <= 20 {
157 if exp >= 0 {
158 let int_digits = (exp + 1) as usize;
159 let mut out = String::from(sign);
160 if digits.len() <= int_digits {
161 out.push_str(digits);
162 out.push_str(&"0".repeat(int_digits - digits.len()));
163 } else {
164 out.push_str(&digits[..int_digits]);
165 out.push('.');
166 out.push_str(&digits[int_digits..]);
167 }
168 out
169 } else {
170 format!("{}0.{}{}", sign, "0".repeat((-exp - 1) as usize), digits)
171 }
172 } else {
173 // The 20-character budget applies to the scientific rendering too,
174 // and the exponent is charged against it: a three-digit exponent
175 // leaves one fewer mantissa digit than a two-digit one. Real Excel
176 // writes PHI(28) as "2.2775774787367E-171" (13 fractional digits)
177 // and CSCH(-23) as "-2.05237592634038E-10" (14), both 20 characters
178 // once the sign is set aside.
179 // Excel starts reserving the three-exponent-digit budget at |99|,
180 // even though the textual exponent still has only two digits there:
181 // 6.37409849780041E-98 keeps 14 fractional digits, while
182 // 6.3740984978004E-99 keeps 13. The actual `E+99`/`E-99` suffix is
183 // still rendered with two digits; only the mantissa budget shrinks.
184 let suffix_budget_len = if exp.abs() >= 99 {
185 5
186 } else {
187 format!("E{:+03}", exp).len()
188 };
189 let frac_digits = 18usize.saturating_sub(suffix_budget_len).min(14);
190
191 // Rounded from the *15-significant-digit* value, not from the raw
192 // f64. Excel snaps a result to 15 significant digits and only then
193 // formats it, so when a three-digit exponent leaves room for just
194 // 14 the two roundings compose. 28^-92 is
195 // 7.26877317134744769...e-134: rounding that straight to 14 digits
196 // gives ...7474, but snapping to 15 first gives 7.26877317134745
197 // and then 14 gives ...7475, which is what Excel prints.
198 //
199 // Working from the digit string rather than re-rounding the f64
200 // keeps the two steps exact, and rounds half away from zero, as
201 // Excel does elsewhere (DOLLAR/FIXED/TEXT).
202 let (rounded_digits, exp) = round_digits_half_up(digits, exp, frac_digits + 1);
203 let mut mantissa = String::from(sign);
204 mantissa.push_str(&rounded_digits[..1]);
205 if rounded_digits.len() > 1 {
206 mantissa.push('.');
207 mantissa.push_str(&rounded_digits[1..]);
208 }
209 format!("{}E{:+03}", mantissa, exp)
210 }
211}