mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-25 11:16:49 -06:00
295 lines
10 KiB
Rust
295 lines
10 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::*;
|
|
|
|
/// Whether a sheet name needs quotes in a reference.
|
|
pub fn needs_quotes(name: &str) -> bool {
|
|
if name.is_empty() {
|
|
return true;
|
|
}
|
|
let first = name.chars().next().unwrap_or('_');
|
|
if first.is_ascii_digit() || name.chars().any(|c| !(c.is_alphanumeric() || c == '_' || c == '.')) {
|
|
return true;
|
|
}
|
|
// Looks like a cell ref (`A1`, `R1C1`) or a boolean.
|
|
gridcraft_core::CellRef::parse(name).is_some() || name.eq_ignore_ascii_case("TRUE") || name.eq_ignore_ascii_case("FALSE")
|
|
}
|
|
|
|
pub fn quote_sheet(name: &str) -> String {
|
|
if needs_quotes(name) { format!("'{}'", name.replace('\'', "''")) } else { name.to_string() }
|
|
}
|
|
|
|
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("e_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(')');
|
|
}
|
|
}
|
|
}
|