Files
gridcraft/crates/formula/src/printer.rs
T
Eduardo GarciaandClaude Sonnet 5.5 0b4cc5618f fix #1458: one Excel sheet-name quoting rule for formulas, pivot sources and ADDRESS
Sheet names starting with '.' or reading as R1C1 (R, C, RC, R1C1...) were saved
unquoted, so Excel could not open the file; PivotTable sources and ADDRESS left
cell-like names (A1) bare. The quoting rule, unquoting and sheet splitting now
live once in gridcraft_core::addr and replace the copies in the formula printer,
pivot, ADDRESS, chart, xlsx, Solver, Go To and the Name Box (which now reads ''
as ').

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 17:36:40 -03:00

280 lines
9.9 KiB
Rust

//! [`Expr`] → formula text (without `=`), A1 or R1C1.
use std::fmt::Write;
use gridcraft_core::{CellRef, col_to_letters, number_to_text};
use crate::ast::*;
pub use gridcraft_core::{quote_sheet, sheet_needs_quotes as needs_quotes};
fn sheet_prefix(s: &SheetSel) -> String {
match s {
SheetSel::Current => String::new(),
SheetSel::Named(n) => format!("{}!", quote_sheet(n)),
SheetSel::Span(a, b) => {
let joined = format!("{a}:{b}");
if needs_quotes(a) || needs_quotes(b) { format!("'{}'!", joined.replace('\'', "''")) } else { format!("{joined}!") }
}
}
}
fn anchor_a1(a: &Anchor) -> String {
format!("{}{}{}{}", if a.col_abs { "$" } else { "" }, col_to_letters(a.col), if a.row_abs { "$" } else { "" }, a.row as u64 + 1)
}
pub fn reference_a1(r: &Reference) -> String {
range_a1(r, ":")
}
/// A1 text with `sep` between the two ends of a range (`:` or a trim operator).
fn range_a1(r: &Reference, sep: &str) -> String {
let body = match &r.kind {
RefKind::Cell(a) => anchor_a1(a),
RefKind::Range(a, b) => format!("{}{sep}{}", anchor_a1(a), anchor_a1(b)),
RefKind::Rows(r0, a0, r1, a1) => format!("{}{}{sep}{}{}", if *a0 { "$" } else { "" }, r0 + 1, if *a1 { "$" } else { "" }, r1 + 1),
RefKind::Cols(c0, a0, c1, a1) => {
format!("{}{}{sep}{}{}", if *a0 { "$" } else { "" }, col_to_letters(*c0), if *a1 { "$" } else { "" }, col_to_letters(*c1))
}
};
format!("{}{}", sheet_prefix(&r.sheet), body)
}
fn r1c1_part(letter: char, v: u32, abs: bool, base: u32) -> String {
if abs {
format!("{letter}{}", v + 1)
} else {
let d = v as i64 - base as i64;
if d == 0 { letter.to_string() } else { format!("{letter}[{d}]") }
}
}
fn anchor_r1c1(a: &Anchor, at: CellRef) -> String {
format!("{}{}", r1c1_part('R', a.row, a.row_abs, at.row), r1c1_part('C', a.col, a.col_abs, at.col))
}
pub fn reference_r1c1(r: &Reference, at: CellRef) -> String {
range_r1c1(r, at, ":")
}
/// R1C1 text with `sep` between the two ends of a range; a trim operator never collapses
/// `R1:R1` to `R1`.
fn range_r1c1(r: &Reference, at: CellRef, sep: &str) -> String {
let body = match &r.kind {
RefKind::Cell(a) => anchor_r1c1(a, at),
RefKind::Range(a, b) => format!("{}{sep}{}", anchor_r1c1(a, at), anchor_r1c1(b, at)),
RefKind::Rows(r0, a0, r1, a1) => {
let (x, y) = (r1c1_part('R', *r0, *a0, at.row), r1c1_part('R', *r1, *a1, at.row));
if x == y && sep == ":" { x } else { format!("{x}{sep}{y}") }
}
RefKind::Cols(c0, a0, c1, a1) => {
let (x, y) = (r1c1_part('C', *c0, *a0, at.col), r1c1_part('C', *c1, *a1, at.col));
if x == y && sep == ":" { x } else { format!("{x}{sep}{y}") }
}
};
format!("{}{}", sheet_prefix(&r.sheet), body)
}
pub fn struct_ref(s: &StructRef) -> String {
write_struct_ref(s, true)
}
/// Structured reference as files spell it: `[@Col]` is only Excel's display shorthand, files
/// hold `[[#This Row],[Col]]` (and `[#This Row]` for `[@]`).
pub fn struct_ref_file(s: &StructRef) -> String {
write_struct_ref(s, false)
}
fn write_struct_ref(s: &StructRef, at_shorthand: bool) -> String {
let esc = |c: &str| c.replace('\'', "''").replace('[', "'[").replace(']', "']").replace('#', "'#");
let item = |i: &StructItem| match i {
StructItem::All => "#All",
StructItem::Data => "#Data",
StructItem::Headers => "#Headers",
StructItem::Totals => "#Totals",
StructItem::ThisRow => "#This Row",
};
let this_row = at_shorthand && s.specifiers == [StructItem::ThisRow];
let mut out = s.table.clone();
let simple_col = |c: &str| !c.chars().any(|ch| matches!(ch, ' ' | '[' | ']' | '#' | '\'' | ',' | ':'));
match (&s.col_start, &s.col_end) {
(None, None) if s.specifiers.is_empty() => out.push_str("[]"),
(None, None) if this_row => out.push_str("[@]"),
(None, None) if s.specifiers.len() == 1 => {
let _ = write!(out, "[{}]", s.specifiers.first().map(item).unwrap_or(""));
}
(Some(c), None) if s.specifiers.is_empty() => {
// Excel writes a name with spaces at its edges in the double-bracket form.
if c.trim() == c {
let _ = write!(out, "[{}]", esc(c));
} else {
let _ = write!(out, "[[{}]]", esc(c));
}
}
(Some(c), None) if this_row => {
if simple_col(c) {
let _ = write!(out, "[@{}]", esc(c));
} else {
let _ = write!(out, "[@[{}]]", esc(c));
}
}
_ => {
let mut parts: Vec<String> = s.specifiers.iter().map(|i| format!("[{}]", item(i))).collect();
match (&s.col_start, &s.col_end) {
(Some(a), Some(b)) => parts.push(format!("[{}]:[{}]", esc(a), esc(b))),
(Some(a), None) => parts.push(format!("[{}]", esc(a))),
_ => {}
}
let _ = write!(out, "[{}]", parts.join(","));
}
}
out
}
fn number(n: f64) -> String {
number_to_text(n)
}
/// Prints an expression in A1 notation.
pub fn print(e: &Expr) -> String {
let mut s = String::new();
write_expr(&mut s, e, None);
s
}
/// Prints an expression in R1C1 notation relative to `at`.
pub fn print_r1c1(e: &Expr, at: CellRef) -> String {
let mut s = String::new();
write_expr(&mut s, e, Some(at));
s
}
fn write_expr(s: &mut String, e: &Expr, r1c1: Option<CellRef>) {
match e {
Expr::Number(n) => s.push_str(&number(*n)),
Expr::Text(t) => {
s.push('"');
s.push_str(&t.replace('"', "\"\""));
s.push('"');
}
Expr::Bool(b) => s.push_str(if *b { "TRUE" } else { "FALSE" }),
Expr::Error(err) => s.push_str(err.as_str()),
Expr::Array(rows) => {
s.push('{');
for (ri, row) in rows.iter().enumerate() {
if ri > 0 {
s.push(';');
}
for (ci, el) in row.iter().enumerate() {
if ci > 0 {
s.push(',');
}
write_expr(s, el, r1c1);
}
}
s.push('}');
}
Expr::Ref(r) => match r1c1 {
Some(at) => s.push_str(&reference_r1c1(r, at)),
None => s.push_str(&reference_a1(r)),
},
Expr::Name(n) => match n.rsplit_once('!') {
// A sheet-qualified name is stored with the bare sheet name (`My Sheet!Total`): quote it so it parses back.
Some((sheet, name)) => {
s.push_str(&quote_sheet(sheet));
s.push('!');
s.push_str(name);
}
_ => s.push_str(n),
},
Expr::Struct(st) => s.push_str(&struct_ref(st)),
Expr::Unary(op, x) => match op {
UnOp::Neg => {
s.push('-');
write_expr(s, x, r1c1);
}
UnOp::Plus => {
s.push('+');
write_expr(s, x, r1c1);
}
UnOp::At => {
s.push('@');
write_expr(s, x, r1c1);
}
UnOp::Percent => {
write_expr(s, x, r1c1);
s.push('%');
}
UnOp::Spill => {
write_expr(s, x, r1c1);
// A reference that became #REF! (its sheet or cell was deleted) stays `#REF!`.
if !matches!(**x, Expr::Error(_)) {
s.push('#');
}
}
UnOp::Trim(mode) => match &**x {
Expr::Ref(r) if !matches!(r.kind, RefKind::Cell(_)) => match r1c1 {
Some(at) => s.push_str(&range_r1c1(r, at, mode.symbol())),
None => s.push_str(&range_a1(r, mode.symbol())),
},
Expr::Binary(BinOp::Range, a, b) => {
write_expr(s, a, r1c1);
s.push_str(mode.symbol());
write_expr(s, b, r1c1);
}
// A reference that became #REF! stays `#REF!`.
Expr::Error(_) => write_expr(s, x, r1c1),
// No operator form (e.g. a range shrunk to one cell): the equivalent call.
other => {
s.push_str("TRIMRANGE(");
write_expr(s, other, r1c1);
let _ = write!(s, ",{0},{0})", mode.code());
}
},
},
Expr::Binary(..) => {
// Walk the left spine iteratively: a flat chain such as `1+1+1+...` parses into a
// tree one level deeper per operator, and recursing on it could exhaust the stack.
let mut rest: Vec<(&BinOp, &Expr)> = Vec::new();
let mut first = e;
while let Expr::Binary(op, a, b) = first {
rest.push((op, b));
first = a;
}
write_expr(s, first, r1c1);
for (op, right) in rest.into_iter().rev() {
s.push_str(op.symbol());
write_expr(s, right, r1c1);
}
}
Expr::Call(name, args) => {
s.push_str(name);
s.push('(');
for (i, a) in args.iter().enumerate() {
if i > 0 {
s.push(',');
}
write_expr(s, a, r1c1);
}
s.push(')');
}
Expr::Invoke(c, args) => {
write_expr(s, c, r1c1);
s.push('(');
for (i, a) in args.iter().enumerate() {
if i > 0 {
s.push(',');
}
write_expr(s, a, r1c1);
}
s.push(')');
}
Expr::Missing => {}
Expr::Paren(x) => {
s.push('(');
write_expr(s, x, r1c1);
s.push(')');
}
}
}