Skip to main content

ravenrs_sheet/formula/
printer.rs

1//! [`Expr`] → formula text (without `=`), A1 or R1C1.
2
3use std::fmt::Write;
4
5use crate::base::{CellRef, number_to_text};
6
7use crate::formula::ast::{Anchor, BinOp, Expr, RefKind, Reference, SheetSel, StructItem, StructRef, UnOp};
8
9pub use crate::base::{quote_sheet, sheet_needs_quotes as needs_quotes};
10
11fn sheet_prefix(s: &SheetSel) -> String {
12    match s {
13        SheetSel::Current => String::new(),
14        SheetSel::Named(n) => format!("{}!", quote_sheet(n)),
15        SheetSel::Span(a, b) => {
16            let joined = format!("{a}:{b}");
17            if needs_quotes(a) || needs_quotes(b) { format!("'{}'!", joined.replace('\'', "''")) } else { format!("{joined}!") }
18        }
19    }
20}
21
22fn push_anchor_a1(s: &mut String, a: &Anchor) {
23    if a.col_abs {
24        s.push('$');
25    }
26    push_col(s, a.col);
27    if a.row_abs {
28        s.push('$');
29    }
30    let _ = write!(s, "{}", a.row as u64 + 1);
31}
32
33/// Appends [`crate::base::col_to_letters`]`(col)` without allocating.
34fn push_col(s: &mut String, col: u32) {
35    let mut letters = [0u8; 8];
36    let mut i = letters.len();
37    let mut n = col as u64 + 1;
38    while n > 0 && i > 0 {
39        i -= 1;
40        if let Some(l) = letters.get_mut(i) {
41            *l = b'A' + ((n - 1) % 26) as u8;
42        }
43        n = (n - 1) / 26;
44    }
45    for &l in letters.get(i..).unwrap_or_default() {
46        s.push(char::from(l));
47    }
48}
49
50pub fn reference_a1(r: &Reference) -> String {
51    range_a1(r, ":")
52}
53
54/// A1 text with `sep` between the two ends of a range (`:` or a trim operator).
55fn range_a1(r: &Reference, sep: &str) -> String {
56    let mut s = String::new();
57    push_range_a1(&mut s, r, sep);
58    s
59}
60
61/// Appends [`range_a1`]`(r, sep)` to `s`.
62fn push_range_a1(s: &mut String, r: &Reference, sep: &str) {
63    match &r.sheet {
64        SheetSel::Current => {}
65        SheetSel::Named(n) => {
66            s.push_str(&quote_sheet(n));
67            s.push('!');
68        }
69        sheet @ SheetSel::Span(_, _) => s.push_str(&sheet_prefix(sheet)),
70    }
71    let dollar = |abs: bool| if abs { "$" } else { "" };
72    match &r.kind {
73        RefKind::Cell(a) => push_anchor_a1(s, a),
74        RefKind::Range(a, b) => {
75            push_anchor_a1(s, a);
76            s.push_str(sep);
77            push_anchor_a1(s, b);
78        }
79        RefKind::Rows(r0, a0, r1, a1) => {
80            let _ = write!(s, "{}{}{sep}{}{}", dollar(*a0), r0 + 1, dollar(*a1), r1 + 1);
81        }
82        RefKind::Cols(c0, a0, c1, a1) => {
83            s.push_str(dollar(*a0));
84            push_col(s, *c0);
85            s.push_str(sep);
86            s.push_str(dollar(*a1));
87            push_col(s, *c1);
88        }
89    }
90}
91
92fn r1c1_part(letter: char, v: u32, abs: bool, base: u32) -> String {
93    if abs {
94        format!("{letter}{}", v + 1)
95    } else {
96        let d = v as i64 - base as i64;
97        if d == 0 { letter.to_string() } else { format!("{letter}[{d}]") }
98    }
99}
100
101fn anchor_r1c1(a: &Anchor, at: CellRef) -> String {
102    format!("{}{}", r1c1_part('R', a.row, a.row_abs, at.row), r1c1_part('C', a.col, a.col_abs, at.col))
103}
104
105pub fn reference_r1c1(r: &Reference, at: CellRef) -> String {
106    range_r1c1(r, at, ":")
107}
108
109/// R1C1 text with `sep` between the two ends of a range; a trim operator never collapses
110/// `R1:R1` to `R1`.
111fn range_r1c1(r: &Reference, at: CellRef, sep: &str) -> String {
112    let body = match &r.kind {
113        RefKind::Cell(a) => anchor_r1c1(a, at),
114        RefKind::Range(a, b) => format!("{}{sep}{}", anchor_r1c1(a, at), anchor_r1c1(b, at)),
115        RefKind::Rows(r0, a0, r1, a1) => {
116            let (x, y) = (r1c1_part('R', *r0, *a0, at.row), r1c1_part('R', *r1, *a1, at.row));
117            if x == y && sep == ":" { x } else { format!("{x}{sep}{y}") }
118        }
119        RefKind::Cols(c0, a0, c1, a1) => {
120            let (x, y) = (r1c1_part('C', *c0, *a0, at.col), r1c1_part('C', *c1, *a1, at.col));
121            if x == y && sep == ":" { x } else { format!("{x}{sep}{y}") }
122        }
123    };
124    format!("{}{}", sheet_prefix(&r.sheet), body)
125}
126
127pub fn struct_ref(s: &StructRef) -> String {
128    write_struct_ref(s, true)
129}
130
131/// Structured reference as files spell it: `[@Col]` is only Excel's display shorthand, files
132/// hold `[[#This Row],[Col]]` (and `[#This Row]` for `[@]`).
133pub fn struct_ref_file(s: &StructRef) -> String {
134    write_struct_ref(s, false)
135}
136
137/// Splits a sheet-qualified name at its last `!` outside brackets. A `!` inside brackets
138/// belongs to a structured reference's column (`Table1[Done!]`, `Table1[#REF!]`).
139fn sheet_split(n: &str) -> Option<(&str, &str)> {
140    let mut depth = 0usize;
141    let mut escaped = false;
142    let mut at = None;
143    for (i, c) in n.char_indices() {
144        if escaped {
145            escaped = false;
146            continue;
147        }
148        match c {
149            '\'' if depth > 0 => escaped = true,
150            '[' => depth = depth.saturating_add(1),
151            ']' => depth = depth.saturating_sub(1),
152            '!' if depth == 0 => at = Some(i),
153            _ => {}
154        }
155    }
156    at.and_then(|i| Some((n.get(..i)?, n.get(i + 1..)?)))
157}
158
159fn write_struct_ref(s: &StructRef, at_shorthand: bool) -> String {
160    let esc = |c: &str| c.replace('\'', "''").replace('[', "'[").replace(']', "']").replace('#', "'#");
161    let item = |i: &StructItem| match i {
162        StructItem::All => "#All",
163        StructItem::Data => "#Data",
164        StructItem::Headers => "#Headers",
165        StructItem::Totals => "#Totals",
166        StructItem::ThisRow => "#This Row",
167    };
168    let this_row = at_shorthand && s.specifiers == [StructItem::ThisRow];
169    let mut out = s.table.clone();
170    let simple_col = |c: &str| !c.chars().any(|ch| matches!(ch, ' ' | '[' | ']' | '#' | '\'' | ',' | ':'));
171    match (&s.col_start, &s.col_end) {
172        (None, None) if s.specifiers.is_empty() => out.push_str("[]"),
173        (None, None) if this_row => out.push_str("[@]"),
174        (None, None) if s.specifiers.len() == 1 => {
175            let _ = write!(out, "[{}]", s.specifiers.first().map(item).unwrap_or(""));
176        }
177        (Some(c), None) if s.specifiers.is_empty() => {
178            // Names with spaces at their edges, or starting with `@` (this-row shorthand), use the double-bracket form.
179            if c.trim() == c && !c.starts_with('@') {
180                let _ = write!(out, "[{}]", esc(c));
181            } else {
182                let _ = write!(out, "[[{}]]", esc(c));
183            }
184        }
185        (Some(c), None) if this_row => {
186            if simple_col(c) {
187                let _ = write!(out, "[@{}]", esc(c));
188            } else {
189                let _ = write!(out, "[@[{}]]", esc(c));
190            }
191        }
192        _ => {
193            let mut parts: Vec<String> = s.specifiers.iter().map(|i| format!("[{}]", item(i))).collect();
194            match (&s.col_start, &s.col_end) {
195                (Some(a), Some(b)) => parts.push(format!("[{}]:[{}]", esc(a), esc(b))),
196                (Some(a), None) => parts.push(format!("[{}]", esc(a))),
197                _ => {}
198            }
199            let _ = write!(out, "[{}]", parts.join(","));
200        }
201    }
202    out
203}
204
205fn number(n: f64) -> String {
206    let text = number_to_text(n);
207    // 15 significant digits can round past f64::MAX, which no longer parses.
208    if text.parse::<f64>().is_ok_and(f64::is_finite) {
209        return text;
210    }
211    let exact = format!("{:E}", n);
212    match exact.split_once('E') {
213        Some((m, e)) if !e.starts_with('-') => format!("{m}E+{e}"),
214        _ => exact,
215    }
216}
217
218/// Prints an expression in A1 notation.
219pub fn print(e: &Expr) -> String {
220    let mut s = String::new();
221    write_expr(&mut s, e, None);
222    s
223}
224
225/// Prints an expression in R1C1 notation relative to `at`.
226pub fn print_r1c1(e: &Expr, at: CellRef) -> String {
227    let mut s = String::new();
228    write_expr(&mut s, e, Some(at));
229    s
230}
231
232fn write_expr(s: &mut String, e: &Expr, r1c1: Option<CellRef>) {
233    match e {
234        Expr::Number(n) => s.push_str(&number(*n)),
235        Expr::Text(t) => {
236            s.push('"');
237            s.push_str(&t.replace('"', "\"\""));
238            s.push('"');
239        }
240        Expr::Bool(b) => s.push_str(if *b { "TRUE" } else { "FALSE" }),
241        Expr::Error(err) => s.push_str(err.as_str()),
242        Expr::Array(rows) => {
243            s.push('{');
244            for (ri, row) in rows.iter().enumerate() {
245                if ri > 0 {
246                    s.push(';');
247                }
248                for (ci, el) in row.iter().enumerate() {
249                    if ci > 0 {
250                        s.push(',');
251                    }
252                    write_expr(s, el, r1c1);
253                }
254            }
255            s.push('}');
256        }
257        Expr::Ref(r) => match r1c1 {
258            Some(at) => s.push_str(&reference_r1c1(r, at)),
259            None => push_range_a1(s, r, ":"),
260        },
261        Expr::Name(n) => match sheet_split(n) {
262            // A sheet-qualified name is stored with the bare sheet name (`My Sheet!Total`): quote it so it parses back.
263            Some((sheet, name)) => {
264                s.push_str(&quote_sheet(sheet));
265                s.push('!');
266                s.push_str(name);
267            }
268            _ => s.push_str(n),
269        },
270        Expr::Struct(st) => s.push_str(&struct_ref(st)),
271        Expr::Unary(op, x) => match op {
272            UnOp::Neg => {
273                s.push('-');
274                write_expr(s, x, r1c1);
275            }
276            UnOp::Plus => {
277                s.push('+');
278                write_expr(s, x, r1c1);
279            }
280            UnOp::At => {
281                s.push('@');
282                write_expr(s, x, r1c1);
283            }
284            UnOp::Percent => {
285                write_expr(s, x, r1c1);
286                s.push('%');
287            }
288            UnOp::Spill => {
289                write_expr(s, x, r1c1);
290                // A reference that became #REF! (its sheet or cell was deleted) stays `#REF!`.
291                if !matches!(**x, Expr::Error(_)) {
292                    s.push('#');
293                }
294            }
295            UnOp::Trim(mode) => match &**x {
296                Expr::Ref(r) if !matches!(r.kind, RefKind::Cell(_)) => match r1c1 {
297                    Some(at) => s.push_str(&range_r1c1(r, at, mode.symbol())),
298                    None => push_range_a1(s, r, mode.symbol()),
299                },
300                Expr::Binary(BinOp::Range, a, b) => {
301                    write_expr(s, a, r1c1);
302                    s.push_str(mode.symbol());
303                    write_expr(s, b, r1c1);
304                }
305                // A reference that became #REF! stays `#REF!`.
306                Expr::Error(_) => write_expr(s, x, r1c1),
307                // No operator form (e.g. a range shrunk to one cell): the equivalent call.
308                other => {
309                    s.push_str("TRIMRANGE(");
310                    write_expr(s, other, r1c1);
311                    let _ = write!(s, ",{0},{0})", mode.code());
312                }
313            },
314        },
315        Expr::Binary(..) => {
316            // Walk the left spine iteratively: a flat chain such as `1+1+1+...` parses into a
317            // tree one level deeper per operator, and recursing on it could exhaust the stack.
318            let mut rest: Vec<(&BinOp, &Expr)> = Vec::new();
319            let mut first = e;
320            while let Expr::Binary(op, a, b) = first {
321                rest.push((op, b));
322                first = a;
323            }
324            write_expr(s, first, r1c1);
325            for (op, right) in rest.into_iter().rev() {
326                s.push_str(op.symbol());
327                write_expr(s, right, r1c1);
328            }
329        }
330        Expr::Call(name, args) => {
331            s.push_str(name);
332            s.push('(');
333            for (i, a) in args.iter().enumerate() {
334                if i > 0 {
335                    s.push(',');
336                }
337                write_expr(s, a, r1c1);
338            }
339            s.push(')');
340        }
341        Expr::Invoke(c, args) => {
342            write_expr(s, c, r1c1);
343            s.push('(');
344            for (i, a) in args.iter().enumerate() {
345                if i > 0 {
346                    s.push(',');
347                }
348                write_expr(s, a, r1c1);
349            }
350            s.push(')');
351        }
352        Expr::Missing => {}
353        Expr::Paren(x) => {
354            s.push('(');
355            write_expr(s, x, r1c1);
356            s.push(')');
357        }
358    }
359}