Files
gridcraft/crates/formula/tests/spanish_input.rs
T
Harrison BoothandClaude Opus 5.5 2aeb21d4d3 Formulas: flat operator chains as long as Excel allows (#916)
along: =1+1+... with 4,096 operands (8,192 characters, exactly Excel's
limit, which Excel accepts and calculates) overflowed the stack when
entered, because the derived Clone of Expr recursed once per operand
down the chain's left spine. Saving hit the same in the XLSX writer.

- Expr: Clone, PartialEq, walk and map follow the left spine in a loop
  and recurse only into right operands and real nesting.
- XLSX: _xlpm. prefixing and the implicit-intersection scan follow the
  spine too. The scan's depth limit of 64 wrote any chain of more than
  about 64 operands as a dynamic-array formula; such sums are plain
  formulas again (ranges in the chain still get array evaluation).
- Evaluate Formula borrows sub-expressions instead of cloning each
  operator's subtree (quadratic memory on a long chain).
- Spanish formula entry and display no longer refuse chains of more
  than 256 operands; the left spine does not count as nesting.

Tests run on a 1 MiB stack (the Windows main thread) with Excel's
observed results: formula (clone/eq/walk/map/adjust at 4,096), calc
(401 and 4,096 operands, -, *, /, ^, &, %, comparisons, 1,500 refs),
xlsx (long chains written as scalar formulas), engine (cell.set, fill,
copy/paste, insert rows/columns, undo/redo, Evaluate Formula, XLSX save
and reopen), Spanish entry. Each fails without the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:54:30 -05:00

122 lines
5.6 KiB
Rust

use gridcraft_formula::{Expr, FormulaLocale, parse, parse_input, print, print_input};
fn input(src: &str) -> String {
print(&parse_input(src, FormulaLocale::Es, |_| false).unwrap())
}
#[test]
fn spanish_aliases_and_argument_separators() {
for (src, expected) in [
("=suma(A1:A10)", "SUM(A1:A10)"),
("PROMEDIO(A1:A10)", "AVERAGE(A1:A10)"),
("SI(A1>0;\"Sí\";\"No\")", "IF(A1>0,\"Sí\",\"No\")"),
("CONTAR(A1:A10)", "COUNT(A1:A10)"),
("SUMAR.SI(A1:A10;\">0\")", "SUMIF(A1:A10,\">0\")"),
("CONTAR.SI(A1:A10;\">0\")", "COUNTIF(A1:A10,\">0\")"),
("BUSCARV(A2;D:E;2;FALSO)", "VLOOKUP(A2,D:E,2,FALSE)"),
("HOY()", "TODAY()"),
("=verdadero", "TRUE"),
("=falso", "FALSE"),
("VERDADERO()", "TRUE()"),
("FALSO()", "FALSE()"),
("SUMA(1;SUM(2;3);PROMEDIO(4;6))", "SUM(1,SUM(2,3),AVERAGE(4,6))"),
("SI(VERDADERO;;)", "IF(TRUE,,)"),
("LAMBDA(x;x+1)(2)", "LAMBDA(x,x+1)(2)"),
] {
assert_eq!(input(src), expected, "{src}");
}
}
#[test]
fn spanish_input_preserves_reference_and_array_syntax() {
for (src, expected) in [
("SUMA({1\\2;3\\4};5)", "SUM({1,2;3,4},5)"),
("{VERDADERO\\FALSO;TRUE\\FALSE}", "{TRUE,FALSE;TRUE,FALSE}"),
("SUM((A1:A3;C1:C3))", "SUM((A1:A3,C1:C3))"),
("\"SUMA;FALSO\"&\"a\"\"b\"", "\"SUMA;FALSO\"&\"a\"\"b\""),
("'SUMA;FALSO'!A1+SUMA!B2+SUMA!FALSO", "'SUMA;FALSO'!A1+SUMA!B2+SUMA!FALSO"),
("SUMA[CONTAR.SI]+FALSO[SI]+CONTAR", "SUMA[CONTAR.SI]+FALSO[SI]+CONTAR"),
] {
assert_eq!(input(src), expected, "{src}");
}
for bad in [r"SUMA({1;2\3})", "{SUMA(A1)}", "{FALSO;}"] {
assert!(parse_input(bad, FormulaLocale::Es, |_| false).is_err(), "{bad}");
}
assert_eq!(print(&parse("SUMA(1,2)+FALSO").unwrap()), "SUMA(1,2)+FALSO");
assert!(parse("SUM(1;2)").is_err());
assert!(parse("{VERDADERO}").is_err());
// Real nesting past the parser's limit is still refused.
let nested = "(".repeat(300) + "1" + &")".repeat(300);
assert!(parse_input(&nested, FormulaLocale::Es, |_| false).is_err());
let deep = (0..300).fold(Expr::Number(1.0), |e, _| Expr::Paren(Box::new(e)));
assert!(print_input(&deep, FormulaLocale::Es, |_| false).is_err());
}
#[test]
fn spanish_input_accepts_flat_chains_as_long_as_excel() {
// A flat chain nests on its left without nesting in the text. Excel accepts 4,096
// operands (8,192 characters); Spanish entry refused more than 256 (#916). On a 1 MiB
// stack, as on the Windows main thread.
std::thread::Builder::new()
.stack_size(1 << 20)
.spawn(|| {
let long_chain = "FALSO+".repeat(1360) + "VERDADERO";
let english = "FALSE+".repeat(1360) + "TRUE";
assert_eq!(input(&long_chain), english);
assert_eq!(print_input(&parse(&english).unwrap(), FormulaLocale::Es, |_| false).unwrap(), long_chain);
let sums = vec!["SUMA(1;2)"; 800].join("+");
assert_eq!(input(&sums), vec!["SUM(1,2)"; 800].join("+"));
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn spanish_input_preserves_defined_and_local_names() {
let known = |name: &str| ["SUMA", "FALSO"].iter().any(|n| n.eq_ignore_ascii_case(name));
let expr = parse_input("SUMA(2;3)+FALSO+SI(VERDADERO;4;5)", FormulaLocale::Es, known).unwrap();
assert_eq!(print(&expr), "SUMA(2,3)+FALSO+IF(TRUE,4,5)");
for (src, expected) in [
("LET(FALSO;7;FALSO)", "LET(FALSO,7,FALSO)"),
("LET(falso;FALSO;SI(falso;1;2))", "LET(falso,FALSE,IF(falso,1,2))"),
("LET(SUMA;LAMBDA(x;x+1);SUMA(2))", "LET(SUMA,LAMBDA(x,x+1),SUMA(2))"),
("LET(falso;7;valor;FALSO;valor)", "LET(falso,7,valor,FALSO,valor)"),
("LAMBDA(FALSO;FALSO)(7)", "LAMBDA(FALSO,FALSO)(7)"),
("LAMBDA(SUMA;SUMA(2))(LAMBDA(x;x+1))", "LAMBDA(SUMA,SUMA(2))(LAMBDA(x,x+1))"),
("LET(FALSO;7;FALSO)+FALSO", "LET(FALSO,7,FALSO)+FALSE"),
("LET(FALSO;7;LAMBDA(x;FALSO+x)(1))", "LET(FALSO,7,LAMBDA(x,FALSO+x)(1))"),
] {
assert_eq!(input(src), expected, "{src}");
}
}
#[test]
fn locale_controls_decimal_list_array_separators_and_display() {
let es = FormulaLocale::Es;
let en = FormulaLocale::En;
let expr = parse_input("SI(VERDADERO;SUMA(1,5;2,25);0)", es, |_| false).unwrap();
assert_eq!(print(&expr), "IF(TRUE,SUM(1.5,2.25),0)");
assert_eq!(print_input(&expr, es, |_| false).unwrap(), "SI(VERDADERO;SUMA(1,5;2,25);0)");
assert_eq!(print_input(&expr, en, |_| false).unwrap(), print(&expr));
assert!(parse_input("SUM(1;2)", en, |_| false).is_err());
assert_eq!(print(&parse_input("SUMA(1,2)+FALSO", en, |_| false).unwrap()), "SUMA(1,2)+FALSO");
assert!(parse_input("SUMA(1.5;2)", es, |_| false).is_err());
let expr = parse("SUM({1.5,2.25;3,4})+SUM((A1:A3,C1:C3))").unwrap();
let shown = print_input(&expr, es, |_| false).unwrap();
assert_eq!(print(&parse_input(&shown, es, |_| false).unwrap()), print(&expr));
let expr = parse("LET(SUMA,LAMBDA(x,x+1),SUM(SUMA(2),FALSE))").unwrap();
let shown = print_input(&expr, es, |n| n == "FALSO").unwrap();
assert_eq!(shown, "LET(SUMA;LAMBDA(x;x+1);SUM(SUMA(2);FALSE))");
assert_eq!(print(&parse_input(&shown, es, |n| n == "FALSO").unwrap()), print(&expr));
}
#[test]
fn spanish_display_round_trips_text_containing_separators() {
let es = FormulaLocale::Es;
let expr = parse(r#"IF(A1="a,b;c",CONCAT("1.5",",",";"),SUM(0.25,A1:A3 B2))"#).unwrap();
let shown = print_input(&expr, es, |_| false).unwrap();
assert_eq!(shown, r#"SI(A1="a,b;c";CONCAT("1.5";",";";");SUMA(0,25;A1:A3 B2))"#);
assert_eq!(print(&parse_input(&shown, es, |_| false).unwrap()), print(&expr));
}