Refactor formula for strict Clippy checks

This commit is contained in:
Dylan M. Taylor
2026-10-28 10:52:53 +00:00
parent fb98f5952b
commit 68e929b1d9
2 changed files with 363 additions and 349 deletions
+197 -198
View File
@@ -67,6 +67,198 @@ fn starts_range_end(c: Option<char>) -> bool {
c.is_some_and(|c| c.is_ascii_alphanumeric() || c == '$')
}
fn byte_pos(src: &str, chars: &[(usize, char)], index: usize) -> usize {
chars.get(index).map(|c| c.0).unwrap_or(src.len())
}
fn char_at(chars: &[(usize, char)], index: usize) -> Option<char> {
chars.get(index).map(|c| c.1)
}
fn emit(src: &str, chars: &[(usize, char)], out: &mut Vec<Token>, tok: Tok, start: usize, end_index: usize) {
out.push(Token { tok, start, end: byte_pos(src, chars, end_index) });
}
fn quoted(src: &str, chars: &[(usize, char)], i: usize, start: usize, out: &mut Vec<Token>) -> Result<usize, LexError> {
let quote = char_at(chars, i).unwrap_or_default();
let mut value = String::new();
let mut j = i + 1;
loop {
match char_at(chars, j) {
None => {
let msg = if quote == '"' { "unterminated text" } else { "unterminated sheet name" };
return Err(LexError { msg: msg.into(), pos: start });
}
Some(c) if c == quote && char_at(chars, j + 1) == Some(quote) => {
value.push(quote);
j += 2;
}
Some(c) if c == quote => {
j += 1;
break;
}
Some(c) => {
value.push(c);
j += 1;
}
}
}
if quote == '"' {
emit(src, chars, out, Tok::Text(value), start, j);
return Ok(j);
}
if char_at(chars, j) != Some('!') {
return Err(LexError { msg: "expected ! after sheet name".into(), pos: byte_pos(src, chars, j) });
}
let (first, last) = match value.split_once(':') {
Some((first, last)) => (first.to_string(), Some(last.to_string())),
None => (value, None),
};
emit(src, chars, out, Tok::Sheet(first, last), start, j + 1);
Ok(j + 1)
}
fn error_literal(src: &str, chars: &[(usize, char)], i: usize, start: usize, out: &mut Vec<Token>) -> Result<usize, LexError> {
let rest = src.get(start..).unwrap_or("");
let error = CellError::ALL
.iter()
.copied()
.find(|error| rest.get(..error.as_str().len()).is_some_and(|text| text.eq_ignore_ascii_case(error.as_str())))
.ok_or_else(|| LexError { msg: "unknown error literal".into(), pos: start })?;
let end = i + error.as_str().chars().count();
emit(src, chars, out, Tok::Error(error), start, end);
Ok(end)
}
fn structured_ref(src: &str, chars: &[(usize, char)], i: usize, start: usize, out: &mut Vec<Token>) -> Result<usize, LexError> {
let mut depth = 0;
let mut j = i;
loop {
match char_at(chars, j) {
None => return Err(LexError { msg: "unbalanced [".into(), pos: start }),
Some('\'') => j += 2,
Some('[') => {
depth += 1;
j += 1;
}
Some(']') => {
depth -= 1;
j += 1;
if depth == 0 {
break;
}
}
Some(_) => j += 1,
}
}
let inner = src.get(byte_pos(src, chars, i + 1)..byte_pos(src, chars, j.saturating_sub(1))).unwrap_or("").to_string();
emit(src, chars, out, Tok::Struct(inner), start, j);
Ok(j)
}
fn number(src: &str, chars: &[(usize, char)], locale: crate::FormulaLocale, i: usize, start: usize, out: &mut Vec<Token>) -> Result<usize, LexError> {
let ch = |index| char_at(chars, index);
let mut j = i;
while ch(j).is_some_and(|c| c.is_ascii_digit()) {
j += 1;
}
let is_row_ref = {
let mut k = j;
if ch(k) == Some('.') && ch(k + 1) == Some(':') {
k += 1;
}
ch(k) == Some(':') && {
k += 1;
if ch(k) == Some('.') {
k += 1;
}
if ch(k) == Some('$') {
k += 1;
}
ch(k).is_some_and(|c| c.is_ascii_digit())
}
};
if is_row_ref {
let word = chars.get(i..j).map(|s| s.iter().map(|c| c.1).collect()).unwrap_or_default();
emit(src, chars, out, Tok::Word(word), start, j);
return Ok(j);
}
if ch(j) == Some(locale.decimal_separator()) {
j += 1;
while ch(j).is_some_and(|c| c.is_ascii_digit()) {
j += 1;
}
}
if matches!(ch(j), Some('e' | 'E')) {
let mut exponent = j + 1;
if matches!(ch(exponent), Some('+' | '-')) {
exponent += 1;
}
if ch(exponent).is_some_and(|c| c.is_ascii_digit()) {
while ch(exponent).is_some_and(|c| c.is_ascii_digit()) {
exponent += 1;
}
j = exponent;
}
}
let text = src.get(start..byte_pos(src, chars, j)).unwrap_or("");
let value: f64 =
text.replace(locale.decimal_separator(), ".").parse().map_err(|_| LexError { msg: format!("bad number `{text}`"), pos: start })?;
if !value.is_finite() {
return Err(LexError { msg: format!("number too large `{text}`"), pos: start });
}
emit(src, chars, out, Tok::Number(value), start, j);
Ok(j)
}
fn word(src: &str, chars: &[(usize, char)], locale: crate::FormulaLocale, array_depth: usize, i: usize, start: usize, out: &mut Vec<Token>) -> usize {
let ch = |index| char_at(chars, index);
let mut j = i;
while ch(j).is_some_and(|c| is_word_char(c) && !(array_depth > 0 && c == locale.array_column_separator())) {
j += 1;
}
let value: String = chars.get(i..j).map(|s| s.iter().map(|c| c.1).collect()).unwrap_or_default();
if ch(j) == Some('!') {
emit(src, chars, out, Tok::Sheet(value, None), start, j + 1);
j + 1
} else if ch(j) == Some(':') && sheet_range_end(chars, locale, array_depth, &value, j, out) {
let mut end = j + 1;
while ch(end).is_some_and(|c| is_word_char(c) && !(array_depth > 0 && c == locale.array_column_separator())) {
end += 1;
}
let last: String = chars.get(j + 1..end).map(|s| s.iter().map(|c| c.1).collect()).unwrap_or_default();
emit(src, chars, out, Tok::Sheet(value, Some(last)), start, end + 1);
end + 1
} else if ch(j) == Some('(') {
emit(src, chars, out, Tok::Func(value), start, j + 1);
j + 1
} else if value.eq_ignore_ascii_case("TRUE") && ch(j) != Some('[') {
emit(src, chars, out, Tok::Bool(true), start, j);
j
} else if value.eq_ignore_ascii_case("FALSE") && ch(j) != Some('[') {
emit(src, chars, out, Tok::Bool(false), start, j);
j
} else if value.len() > 1 && value.ends_with('.') && ch(j) == Some(':') && is_range_end(value.get(..value.len() - 1).unwrap_or("")) {
emit(src, chars, out, Tok::Word(value[..value.len() - 1].to_string()), start, j - 1);
j - 1
} else {
emit(src, chars, out, Tok::Word(value), start, j);
j
}
}
fn sheet_range_end(chars: &[(usize, char)], locale: crate::FormulaLocale, array_depth: usize, value: &str, colon: usize, out: &[Token]) -> bool {
let ch = |index| char_at(chars, index);
let mut end = colon + 1;
while ch(end).is_some_and(|c| is_word_char(c) && !(array_depth > 0 && c == locale.array_column_separator())) {
end += 1;
}
let repeats_sheet = is_range_end(value)
&& matches!(out.last(), Some(Token { tok: Tok::Sheet(previous, None), .. })
if chars.get(colon + 1..end).is_some_and(|s| crate::same_name(&s.iter().map(|c| c.1).collect::<String>(), previous)));
end > colon + 1 && ch(end) == Some('!') && !repeats_sheet
}
/// Tokenizes formula text (without the leading `=`).
pub fn tokenize(src: &str) -> Result<Vec<Token>, LexError> {
tokenize_locale(src, crate::FormulaLocale::En)
@@ -94,107 +286,17 @@ pub fn tokenize_locale(src: &str, locale: crate::FormulaLocale) -> Result<Vec<To
push(&mut out, Tok::Space, j);
i = j;
}
'"' => {
let mut s = String::new();
let mut j = i + 1;
loop {
match ch(j) {
None => return Err(LexError { msg: "unterminated text".into(), pos: start }),
Some('"') if ch(j + 1) == Some('"') => {
s.push('"');
j += 2;
}
Some('"') => {
j += 1;
break;
}
Some(x) => {
s.push(x);
j += 1;
}
}
}
push(&mut out, Tok::Text(s), j);
i = j;
}
'\'' => {
// Quoted sheet name: 'My Sheet'! or 'A:B'!
let mut s = String::new();
let mut j = i + 1;
loop {
match ch(j) {
None => return Err(LexError { msg: "unterminated sheet name".into(), pos: start }),
Some('\'') if ch(j + 1) == Some('\'') => {
s.push('\'');
j += 2;
}
Some('\'') => {
j += 1;
break;
}
Some(x) => {
s.push(x);
j += 1;
}
}
}
if ch(j) != Some('!') {
return Err(LexError { msg: "expected ! after sheet name".into(), pos: pos_of(j) });
}
let (a, b) = match s.split_once(':') {
Some((a, b)) => (a.to_string(), Some(b.to_string())),
None => (s, None),
};
push(&mut out, Tok::Sheet(a, b), j + 1);
i = j + 1;
}
'"' | '\'' => i = quoted(src, &chars, i, start, &mut out)?,
'#' if out.last().is_some_and(|t: &Token| t.end == start && matches!(t.tok, Tok::Word(_))) => {
// Spill range operator right after a reference: `A1#`.
push(&mut out, Tok::Op("#"), i + 1);
i += 1;
}
'#' => {
// Error literal, or #-prefixed struct specifier (only inside [] which we lex as Struct).
let rest = src.get(start..).unwrap_or("");
let mut found = None;
for e in CellError::ALL {
let t = e.as_str();
if rest.len() >= t.len() && rest.get(..t.len()).is_some_and(|p| p.eq_ignore_ascii_case(t)) {
found = Some(e);
break;
}
}
let Some(e) = found else { return Err(LexError { msg: "unknown error literal".into(), pos: start }) };
let len = e.as_str().chars().count();
// `#REF!A1` style is not valid; `#REF!` is an error literal.
push(&mut out, Tok::Error(e), i + len);
i += len;
i = error_literal(src, &chars, i, start, &mut out)?;
}
'[' => {
// Structured reference body: balanced brackets.
let mut depth = 0;
let mut j = i;
loop {
match ch(j) {
None => return Err(LexError { msg: "unbalanced [".into(), pos: start }),
Some('\'') => j += 2, // escape char inside struct refs
Some('[') => {
depth += 1;
j += 1;
}
Some(']') => {
depth -= 1;
j += 1;
if depth == 0 {
break;
}
}
Some(_) => j += 1,
}
}
let inner = src.get(pos_of(i + 1)..pos_of(j.saturating_sub(1))).unwrap_or("").to_string();
push(&mut out, Tok::Struct(inner), j);
i = j;
i = structured_ref(src, &chars, i, start, &mut out)?;
}
'.' if ch(i + 1) == Some(':') => {
// Trim reference `A1.:E10` / `A1.:.E10` (the word before stopped at the dot).
@@ -207,64 +309,7 @@ pub fn tokenize_locale(src: &str, locale: crate::FormulaLocale) -> Result<Vec<To
}
}
c if c.is_ascii_digit() || (c == locale.decimal_separator() && ch(i + 1).is_some_and(|d| d.is_ascii_digit())) => {
// Number, unless it is a row range like 1:3 (handled by parser via Word) — numbers
// followed by ':' and digits are row refs: lex as Word.
let mut j = i;
while ch(j).is_some_and(|d| d.is_ascii_digit()) {
j += 1;
}
// Row ranges `1:3`, and the trim forms `1.:3`, `1:.3`, `1.:.3`.
let is_row_ref = {
let mut k = j;
if ch(k) == Some('.') && ch(k + 1) == Some(':') {
k += 1;
}
ch(k) == Some(':') && {
k += 1;
if ch(k) == Some('.') {
k += 1;
}
if ch(k) == Some('$') {
k += 1;
}
ch(k).is_some_and(|d| d.is_ascii_digit())
}
};
// A word like "1A" can't happen in valid formulas; numbers then.
if is_row_ref {
let w: String = chars.get(i..j).map(|s| s.iter().map(|c| c.1).collect()).unwrap_or_default();
push(&mut out, Tok::Word(w), j);
i = j;
continue;
}
if ch(j) == Some(locale.decimal_separator()) {
j += 1;
while ch(j).is_some_and(|d| d.is_ascii_digit()) {
j += 1;
}
}
if matches!(ch(j), Some('e' | 'E')) {
let mut k = j + 1;
if matches!(ch(k), Some('+' | '-')) {
k += 1;
}
if ch(k).is_some_and(|d| d.is_ascii_digit()) {
while ch(k).is_some_and(|d| d.is_ascii_digit()) {
k += 1;
}
j = k;
}
}
let text = src.get(start..pos_of(j)).unwrap_or("");
let v: f64 = text
.replace(locale.decimal_separator(), ".")
.parse()
.map_err(|_| LexError { msg: format!("bad number `{text}`"), pos: start })?;
if !v.is_finite() {
return Err(LexError { msg: format!("number too large `{text}`"), pos: start });
}
push(&mut out, Tok::Number(v), j);
i = j;
i = number(src, &chars, locale, i, start, &mut out)?;
}
'(' => {
push(&mut out, Tok::LParen, i + 1);
@@ -328,53 +373,7 @@ pub fn tokenize_locale(src: &str, locale: crate::FormulaLocale) -> Result<Vec<To
push(&mut out, Tok::Op(op), i + len);
i += len;
}
c if is_word_char(c) => {
let mut j = i;
while ch(j).is_some_and(|c| is_word_char(c) && !(array_depth > 0 && c == locale.array_column_separator())) {
j += 1;
}
let w: String = chars.get(i..j).map(|s| s.iter().map(|c| c.1).collect()).unwrap_or_default();
if ch(j) == Some('!') {
push(&mut out, Tok::Sheet(w, None), j + 1);
i = j + 1;
} else if ch(j) == Some(':') && {
// Sheet1:Sheet3!A1 — look ahead for word then '!'.
let mut k = j + 1;
while ch(k).is_some_and(|c| is_word_char(c) && !(array_depth > 0 && c == locale.array_column_separator())) {
k += 1;
}
// `Sheet1!A1:Sheet1!B2` is a range whose second end repeats the sheet, not a span.
let repeats_sheet = is_range_end(&w)
&& matches!(out.last(), Some(Token { tok: Tok::Sheet(p, None), .. })
if chars.get(j + 1..k).is_some_and(|s| crate::same_name(&s.iter().map(|c| c.1).collect::<String>(), p)));
k > j + 1 && ch(k) == Some('!') && !repeats_sheet
} {
let mut k = j + 1;
while ch(k).is_some_and(|c| is_word_char(c) && !(array_depth > 0 && c == locale.array_column_separator())) {
k += 1;
}
let w2: String = chars.get(j + 1..k).map(|s| s.iter().map(|c| c.1).collect()).unwrap_or_default();
push(&mut out, Tok::Sheet(w, Some(w2)), k + 1);
i = k + 1;
} else if ch(j) == Some('(') {
push(&mut out, Tok::Func(w), j + 1);
i = j + 1;
} else if w.eq_ignore_ascii_case("TRUE") && ch(j) != Some('[') {
push(&mut out, Tok::Bool(true), j);
i = j;
} else if w.eq_ignore_ascii_case("FALSE") && ch(j) != Some('[') {
push(&mut out, Tok::Bool(false), j);
i = j;
} else if w.len() > 1 && w.ends_with('.') && ch(j) == Some(':') && is_range_end(w.get(..w.len() - 1).unwrap_or("")) {
// `A1.:E10`: the dot belongs to the trim operator, lexed next.
let w = w.get(..w.len() - 1).unwrap_or("").to_string();
push(&mut out, Tok::Word(w), j - 1);
i = j - 1;
} else {
push(&mut out, Tok::Word(w), j);
i = j;
}
}
c if is_word_char(c) => i = word(src, &chars, locale, array_depth, i, start, &mut out),
other => return Err(LexError { msg: format!("unexpected character `{other}`"), pos: start }),
}
}
+166 -151
View File
@@ -2,7 +2,10 @@ use super::*;
use crate::adjust::{Axis, Edit, adjust, rename_sheet, shift_relative};
fn rt(src: &str) -> String {
print(&parse(src).unwrap_or_else(|e| panic!("{src}: {e}")))
match parse(src) {
Ok(e) => print(&e),
Err(e) => format!("{src}: {e}"),
}
}
#[test]
@@ -87,11 +90,11 @@ fn normalises() {
}
#[test]
fn spill_operator() {
fn spill_operator() -> Result<(), Box<dyn std::error::Error>> {
for f in ["A1#", "Sheet1!$D$1#", "SUM(B2#)", "'My Sheet'!A1#*2", "ROWS(A1#)+1", "IF(A1,#N/A,#REF!)"] {
assert_eq!(rt(f), f);
}
assert!(matches!(parse("A1#").unwrap(), Expr::Unary(UnOp::Spill, _)));
assert!(matches!(parse("A1#")?, Expr::Unary(UnOp::Spill, _)));
// Files spell it ANCHORARRAY.
assert_eq!(rt("_xlfn.ANCHORARRAY(Sheet1!$D$1)"), "Sheet1!$D$1#");
assert_eq!(rt("SUM(ANCHORARRAY(B2))"), "SUM(B2#)");
@@ -99,35 +102,35 @@ fn spill_operator() {
assert!(parse("A1:B2#").is_err());
assert!(parse("Name#").is_err());
// Deleting the anchor's sheet leaves #REF!.
assert_eq!(print(&crate::adjust::delete_sheet(parse("SUM(Data!A1#)").unwrap(), "Data", &[])), "SUM(#REF!)");
assert_eq!(print(&crate::adjust::delete_sheet(parse("SUM(Data!A1#)")?, "Data", &[])), "SUM(#REF!)");
Ok(())
}
#[test]
fn delete_sheet_shrinks_3d_span() {
fn delete_sheet_shrinks_3d_span() -> Result<(), Box<dyn std::error::Error>> {
let order: Vec<String> = ["Sheet1", "Sheet2", "Sheet3", "Results"].iter().map(|s| s.to_string()).collect();
let del = |f: &str, n: &str| print(&crate::adjust::delete_sheet(parse(f).unwrap(), n, &order));
assert_eq!(del("SUM(Sheet1:Sheet3!A1)", "Sheet1"), "SUM(Sheet2:Sheet3!A1)");
assert_eq!(del("SUM(Sheet1:Sheet3!A1)", "Sheet3"), "SUM(Sheet1:Sheet2!A1)");
assert_eq!(del("SUM(Sheet3:Sheet1!A1)", "Sheet1"), "SUM(Sheet3:Sheet2!A1)");
assert_eq!(del("SUM(Sheet1:Sheet3!A1)", "Sheet2"), "SUM(Sheet1:Sheet3!A1)");
assert_eq!(del("SUM(Sheet1:Sheet2!A1)", "Sheet1"), "SUM(Sheet2!A1)");
assert_eq!(del("Sheet1!A1+Sheet3!A1", "Sheet1"), "#REF!+Sheet3!A1");
let del = |f: &str, n: &str| parse(f).map(|e| print(&crate::adjust::delete_sheet(e, n, &order)));
assert_eq!(del("SUM(Sheet1:Sheet3!A1)", "Sheet1")?, "SUM(Sheet2:Sheet3!A1)");
assert_eq!(del("SUM(Sheet1:Sheet3!A1)", "Sheet3")?, "SUM(Sheet1:Sheet2!A1)");
assert_eq!(del("SUM(Sheet3:Sheet1!A1)", "Sheet1")?, "SUM(Sheet3:Sheet2!A1)");
assert_eq!(del("SUM(Sheet1:Sheet3!A1)", "Sheet2")?, "SUM(Sheet1:Sheet3!A1)");
assert_eq!(del("SUM(Sheet1:Sheet2!A1)", "Sheet1")?, "SUM(Sheet2!A1)");
assert_eq!(del("Sheet1!A1+Sheet3!A1", "Sheet1")?, "#REF!+Sheet3!A1");
Ok(())
}
#[test]
fn precedence() {
let e = parse("1+2*3").unwrap();
fn precedence() -> Result<(), Box<dyn std::error::Error>> {
let e = parse("1+2*3")?;
assert!(matches!(e, Expr::Binary(BinOp::Add, _, _)));
let e = parse("-2^2").unwrap();
let e = parse("-2^2")?;
assert!(matches!(e, Expr::Binary(BinOp::Pow, _, _)));
let e = parse("1&2=\"12\"").unwrap();
let e = parse("1&2=\"12\"")?;
assert!(matches!(e, Expr::Binary(BinOp::Eq, _, _)));
let e = parse("2^3^2").unwrap(); // left associative: (2^3)^2
if let Expr::Binary(BinOp::Pow, l, _) = e {
assert!(matches!(*l, Expr::Binary(BinOp::Pow, _, _)));
} else {
panic!()
}
let e = parse("2^3^2")?; // left associative: (2^3)^2
let Expr::Binary(BinOp::Pow, l, _) = e else { return Err("not a power".into()) };
assert!(matches!(*l, Expr::Binary(BinOp::Pow, _, _)));
Ok(())
}
#[test]
@@ -149,122 +152,130 @@ fn no_panic_on_garbage() {
}
#[test]
fn references() {
let e = parse("SUM(A1:B2)+Sheet2!C3*D4").unwrap();
fn references() -> Result<(), Box<dyn std::error::Error>> {
let e = parse("SUM(A1:B2)+Sheet2!C3*D4")?;
let refs: Vec<String> = e.references().iter().map(|r| printer::reference_a1(r)).collect();
assert_eq!(refs, ["A1:B2", "Sheet2!C3", "D4"]);
Ok(())
}
#[test]
fn copy_shift() {
let e = parse("A1+$A1+A$1+$A$1+SUM(B2:C3)").unwrap();
fn copy_shift() -> Result<(), Box<dyn std::error::Error>> {
let e = parse("A1+$A1+A$1+$A$1+SUM(B2:C3)")?;
assert_eq!(print(&shift_relative(e, 2, 1)), "B3+$A3+B$1+$A$1+SUM(C4:D5)");
let e = parse("A1").unwrap();
let e = parse("A1")?;
assert_eq!(print(&shift_relative(e, -1, 0)), "#REF!");
let e = parse("SUM(A:A)").unwrap();
let e = parse("SUM(A:A)")?;
assert_eq!(print(&shift_relative(e, 5, 1)), "SUM(B:B)");
Ok(())
}
#[test]
fn insert_delete() {
fn insert_delete() -> Result<(), Box<dyn std::error::Error>> {
let ins = Edit::Insert { axis: Axis::Rows, at: 2, count: 2 };
let e = parse("A1+A3+SUM(A2:A5)+Other!A3").unwrap();
let e = parse("A1+A3+SUM(A2:A5)+Other!A3")?;
assert_eq!(print(&adjust(e, "Sheet1", "Sheet1", &ins)), "A1+A5+SUM(A2:A7)+Other!A3");
let del = Edit::Delete { axis: Axis::Rows, at: 2, count: 2 }; // rows 3-4
let e = parse("A1+A3+SUM(A2:A5)+A6").unwrap();
let e = parse("A1+A3+SUM(A2:A5)+A6")?;
assert_eq!(print(&adjust(e, "Sheet1", "Sheet1", &del)), "A1+#REF!+SUM(A2:A3)+A4");
let del = Edit::Delete { axis: Axis::Cols, at: 0, count: 1 };
let e = parse("SUM(A1:C1)+B2").unwrap();
let e = parse("SUM(A1:C1)+B2")?;
assert_eq!(print(&adjust(e, "S", "S", &del)), "SUM(A1:B1)+A2");
let e = parse("Sheet1!B2").unwrap();
let e = parse("Sheet1!B2")?;
assert_eq!(print(&adjust(e, "Other", "Sheet1", &del)), "Sheet1!A2");
let e = parse("SUM(A:A)").unwrap();
let e = parse("SUM(A:A)")?;
assert_eq!(print(&adjust(e, "S", "S", &del)), "SUM(#REF!)");
Ok(())
}
#[test]
fn insert_delete_cells() {
let range = |a: &str| gridcraft_core::RangeRef::parse(a).unwrap();
let adj = |f: &str, e: &Edit| print(&adjust(parse(f).unwrap(), "S", "S", e));
fn insert_delete_cells() -> Result<(), Box<dyn std::error::Error>> {
let range = |a: &str| gridcraft_core::RangeRef::parse(a);
let adj = |f: &str, e: &Edit| parse(f).map(|x| print(&adjust(x, "S", "S", e)));
// Insert A3:A4, shifting down: column A moves, other columns and wider ranges don't.
let ins = Edit::InsertCells { axis: Axis::Rows, range: range("A3:A4") };
assert_eq!(adj("SUM(A1:A5)+A2+A3+$A$9+B3+SUM(A1:B5)+SUM(A:A)", &ins), "SUM(A1:A7)+A2+A5+$A$11+B3+SUM(A1:B5)+SUM(A:A)");
let ins = Edit::InsertCells { axis: Axis::Rows, range: range("A3:A4").ok_or("none")? };
assert_eq!(adj("SUM(A1:A5)+A2+A3+$A$9+B3+SUM(A1:B5)+SUM(A:A)", &ins)?, "SUM(A1:A7)+A2+A5+$A$11+B3+SUM(A1:B5)+SUM(A:A)");
// Insert B2 shifting right.
let ins = Edit::InsertCells { axis: Axis::Cols, range: range("B2") };
assert_eq!(adj("A2+B2+C2+B3+SUM(A2:C2)", &ins), "A2+C2+D2+B3+SUM(A2:D2)");
let ins = Edit::InsertCells { axis: Axis::Cols, range: range("B2").ok_or("none")? };
assert_eq!(adj("A2+B2+C2+B3+SUM(A2:C2)", &ins)?, "A2+C2+D2+B3+SUM(A2:D2)");
// Delete A3:A4 shifting up: references into it become #REF!, those below move up.
let del = Edit::DeleteCells { axis: Axis::Rows, range: range("A3:A4") };
assert_eq!(adj("A2+A3+A4+A5+B5+SUM(A1:A10)+SUM(A3:A4)+SUM(A1:B10)", &del), "A2+#REF!+#REF!+A3+B5+SUM(A1:A8)+SUM(#REF!)+SUM(A1:B10)");
let del = Edit::DeleteCells { axis: Axis::Rows, range: range("A3:A4").ok_or("none")? };
assert_eq!(adj("A2+A3+A4+A5+B5+SUM(A1:A10)+SUM(A3:A4)+SUM(A1:B10)", &del)?, "A2+#REF!+#REF!+A3+B5+SUM(A1:A8)+SUM(#REF!)+SUM(A1:B10)");
// Delete B1:C2 shifting left.
let del = Edit::DeleteCells { axis: Axis::Cols, range: range("B1:C2") };
assert_eq!(adj("A1+B1+D1+D2+D3+SUM(A1:E1)", &del), "A1+#REF!+B1+B2+D3+SUM(A1:C1)");
let del = Edit::DeleteCells { axis: Axis::Cols, range: range("B1:C2").ok_or("none")? };
assert_eq!(adj("A1+B1+D1+D2+D3+SUM(A1:E1)", &del)?, "A1+#REF!+B1+B2+D3+SUM(A1:C1)");
Ok(())
}
#[test]
fn moves_and_renames() {
let mv = Edit::Move { from: gridcraft_core::RangeRef::parse("A1:B2").unwrap(), to_row: 9, to_col: 3 };
let e = parse("A1+B2+C3+SUM(A1:B2)").unwrap();
fn moves_and_renames() -> Result<(), Box<dyn std::error::Error>> {
let mv = Edit::Move { from: gridcraft_core::RangeRef::parse("A1:B2").ok_or("none")?, to_row: 9, to_col: 3 };
let e = parse("A1+B2+C3+SUM(A1:B2)")?;
assert_eq!(print(&adjust(e, "S", "S", &mv)), "D10+E11+C3+SUM(D10:E11)");
let mv = Edit::Move { from: gridcraft_core::RangeRef::parse("B:C").unwrap(), to_row: 0, to_col: 4 };
assert_eq!(print(&adjust(parse("SUM(B:B)+SUM($C:$C)+D1").unwrap(), "S", "S", &mv)), "SUM(E:E)+SUM($F:$F)+D1");
let e = parse("Old!A1+'Old'!B2").unwrap();
let mv = Edit::Move { from: gridcraft_core::RangeRef::parse("B:C").ok_or("none")?, to_row: 0, to_col: 4 };
assert_eq!(print(&adjust(parse("SUM(B:B)+SUM($C:$C)+D1")?, "S", "S", &mv)), "SUM(E:E)+SUM($F:$F)+D1");
let e = parse("Old!A1+'Old'!B2")?;
assert_eq!(print(&rename_sheet(e, "Old", "New Name")), "'New Name'!A1+'New Name'!B2");
Ok(())
}
#[test]
fn rename_sheet_updates_qualified_names() {
let e = parse("Data!MyName+MyName+Other!MyName").unwrap();
fn rename_sheet_updates_qualified_names() -> Result<(), Box<dyn std::error::Error>> {
let e = parse("Data!MyName+MyName+Other!MyName")?;
assert_eq!(print(&rename_sheet(e, "data", "Numbers")), "Numbers!MyName+MyName+Other!MyName");
let e = parse("Data!MyName").unwrap();
let e = parse("Data!MyName")?;
assert_eq!(print(&rename_sheet(e, "Data", "New Name")), "'New Name'!MyName");
// Stored bare, like a parsed name, so the evaluator finds the sheet and a second rename still matches.
let e = rename_sheet(parse("Data!MyName").unwrap(), "Data", "It's");
let e = rename_sheet(parse("Data!MyName")?, "Data", "It's");
assert_eq!(e, Expr::Name("It's!MyName".into()));
assert_eq!(print(&rename_sheet(e, "It's", "Totals")), "Totals!MyName");
Ok(())
}
#[test]
fn moves_between_sheets() {
let mv = Edit::MoveToSheet { from: gridcraft_core::RangeRef::parse("A1:B2").unwrap(), to_sheet: "New Name".into(), to_row: 9, to_col: 3 };
fn moves_between_sheets() -> Result<(), Box<dyn std::error::Error>> {
let mv = Edit::MoveToSheet { from: gridcraft_core::RangeRef::parse("A1:B2").ok_or("none")?, to_sheet: "New Name".into(), to_row: 9, to_col: 3 };
for (host, input, expected) in [
("Source", "A1+$B$2+C3+SUM(A1:B2)", "'New Name'!D10+'New Name'!$E$11+C3+SUM('New Name'!D10:E11)"),
("Other", "A1+sOuRcE!$B2+Other!B2", "A1+'New Name'!$E11+Other!B2"),
("Source", "SUM(A1:C3)+SUM(Source:Other!A1)", "SUM(A1:C3)+SUM(Source:Other!A1)"),
] {
assert_eq!(print(&adjust(parse(input).unwrap(), host, "Source", &mv)), expected);
assert_eq!(print(&adjust(parse(input)?, host, "Source", &mv)), expected);
}
let edge = Edit::MoveToSheet {
from: gridcraft_core::RangeRef::parse("A1:B2").unwrap(),
from: gridcraft_core::RangeRef::parse("A1:B2").ok_or("none")?,
to_sheet: "New Name".into(),
to_row: gridcraft_core::MAX_ROWS - 1,
to_col: 3,
};
assert_eq!(print(&adjust(parse("B2").unwrap(), "Source", "Source", &edge)), "#REF!");
assert_eq!(print(&adjust(parse("B2")?, "Source", "Source", &edge)), "#REF!");
for (from, input, row, col, expected) in [("A:A", "$A:$A", 0, 2, "Dest!$C:$C"), ("1:1", "$1:$1", 2, 0, "Dest!$3:$3")] {
let mv = Edit::MoveToSheet { from: gridcraft_core::RangeRef::parse(from).unwrap(), to_sheet: "Dest".into(), to_row: row, to_col: col };
assert_eq!(print(&adjust(parse(input).unwrap(), "Source", "Source", &mv)), expected);
let mv = Edit::MoveToSheet { from: gridcraft_core::RangeRef::parse(from).ok_or("none")?, to_sheet: "Dest".into(), to_row: row, to_col: col };
assert_eq!(print(&adjust(parse(input)?, "Source", "Source", &mv)), expected);
}
Ok(())
}
#[test]
fn r1c1() {
let e = parse("A1+$B$2+SUM(C:C)").unwrap();
fn r1c1() -> Result<(), Box<dyn std::error::Error>> {
let e = parse("A1+$B$2+SUM(C:C)")?;
assert_eq!(print_r1c1(&e, gridcraft_core::CellRef::new(1, 1)), "R[-1]C[-1]+R2C2+SUM(C[1])");
Ok(())
}
#[test]
fn spans() {
let toks = lexer::tokenize("SUM(A1, B2)").unwrap();
let a1 = toks.iter().find(|t| t.tok == lexer::Tok::Word("A1".into())).unwrap();
fn spans() -> Result<(), Box<dyn std::error::Error>> {
let toks = lexer::tokenize("SUM(A1, B2)")?;
let a1 = toks.iter().find(|t| t.tok == lexer::Tok::Word("A1".into())).ok_or("none")?;
assert_eq!((a1.start, a1.end), (4, 6));
Ok(())
}
#[test]
fn unbracketed_this_row_column() {
fn unbracketed_this_row_column() -> Result<(), Box<dyn std::error::Error>> {
// Writers such as xlsxwriter spell `[@Units]` as `[[#This Row],Units]`.
let p = |f: &str| parse(f).unwrap_or_else(|e| panic!("{f}: {e}"));
let Expr::Binary(BinOp::Mul, a, _) = p("[[#This Row],Units]*[[#This Row],Price]") else { panic!() };
let Expr::Struct(s) = *a else { panic!() };
let Expr::Binary(BinOp::Mul, a, _) = parse("[[#This Row],Units]*[[#This Row],Price]")? else { return Err("not a product".into()) };
let Expr::Struct(s) = *a else { return Err("not a struct".into()) };
assert_eq!((s.specifiers.as_slice(), s.col_start.as_deref()), ([StructItem::ThisRow].as_slice(), Some("Units")));
assert_eq!(rt("[[#This Row],Units]*[[#This Row],Price]"), "[@Units]*[@Price]");
assert_eq!(rt("Sales[[#This Row],Units]"), "Sales[@Units]");
@@ -284,24 +295,26 @@ fn unbracketed_this_row_column() {
] {
assert!(parse(bad).is_err(), "{bad} should fail");
}
Ok(())
}
#[test]
fn struct_column_keeps_edge_spaces() {
fn struct_column_keeps_edge_spaces() -> Result<(), Box<dyn std::error::Error>> {
// The spaces are part of the column name: `Table1[Qty ]` names `Qty `, never `Qty`.
let Expr::Struct(s) = parse("Table1[Qty ]").unwrap() else { panic!() };
let Expr::Struct(s) = parse("Table1[Qty ]")? else { return Err("unexpected expression".into()) };
assert_eq!(s.col_start.as_deref(), Some("Qty "));
let Expr::Struct(s) = parse("Table1[ Qty]").unwrap() else { panic!() };
let Expr::Struct(s) = parse("Table1[ Qty]")? else { return Err("unexpected expression".into()) };
assert_eq!(s.col_start.as_deref(), Some(" Qty"));
// Excel writes such a column in the double-bracket form.
assert_eq!(rt("Table1[Qty ]"), "Table1[[Qty ]]");
for f in ["Table1[[Qty ]]", "Table1[[ Sum for Q2 ]]", "Table1[[#Totals],[Qty ]]", "Table1[@[Qty ]]", "Table1[Unit Price]"] {
assert_eq!(rt(f), f);
}
Ok(())
}
#[test]
fn trim_references() {
fn trim_references() -> Result<(), Box<dyn std::error::Error>> {
for f in [
"SUM(A1:.E10)",
"SUM(A1.:E10)",
@@ -319,15 +332,15 @@ fn trim_references() {
] {
assert_eq!(rt(f), f, "round trip of {f}");
}
let Expr::Call(_, args) = parse("SUM(A1:.E10)").unwrap() else { panic!() };
let Expr::Call(_, args) = parse("SUM(A1:.E10)")? else { return Err("unexpected expression".into()) };
assert!(matches!(args.as_slice(), [Expr::Unary(UnOp::Trim(TrimMode::Trailing), r)] if matches!(**r, Expr::Ref(_))));
// Decimals, row ranges and dotted names keep their meaning.
assert_eq!(parse("1.5+.5").unwrap(), Expr::Binary(BinOp::Add, Box::new(Expr::Number(1.5)), Box::new(Expr::Number(0.5))));
assert_eq!(parse("1.5+.5")?, Expr::Binary(BinOp::Add, Box::new(Expr::Number(1.5)), Box::new(Expr::Number(0.5))));
for f in ["SUM(1:5)", "Q1.2026!A1:B2", "my.name+1", "SUM(A1:B2)*1.5", "1.:5"] {
assert_eq!(rt(f), f);
}
assert_eq!(rt("'Q1.2026'!A1:B2"), "Q1.2026!A1:B2");
assert!(matches!(parse("Q1.2026!A1:B2").unwrap(), Expr::Ref(_)));
assert!(matches!(parse("Q1.2026!A1:B2")?, Expr::Ref(_)));
// File call forms.
assert_eq!(rt("SUM(_xlfn._TRO_TRAILING(A1:E10))"), "SUM(A1:.E10)");
assert_eq!(rt("_xlfn._TRO_LEADING(A:A)"), "A.:A");
@@ -338,77 +351,76 @@ fn trim_references() {
assert!(parse(bad).is_err(), "{bad} should fail");
}
// R1C1 keeps the operator.
assert_eq!(print_r1c1(&parse("SUM(A1:.B2)+ROWS(2.:2)").unwrap(), gridcraft_core::CellRef::new(0, 0)), "SUM(RC:.R[1]C[1])+ROWS(R[1].:R[1])");
assert_eq!(print_r1c1(&parse("SUM(A1:.B2)+ROWS(2.:2)")?, gridcraft_core::CellRef::new(0, 0)), "SUM(RC:.R[1]C[1])+ROWS(R[1].:R[1])");
Ok(())
}
#[test]
fn trim_references_adjust() {
fn trim_references_adjust() -> Result<(), Box<dyn std::error::Error>> {
let ins = Edit::Insert { axis: Axis::Rows, at: 2, count: 2 };
let e = parse("SUM(A1:.A5)+SUM(B:.B)+ROWS(2.:.4)").unwrap();
let e = parse("SUM(A1:.A5)+SUM(B:.B)+ROWS(2.:.4)")?;
assert_eq!(print(&adjust(e, "S", "S", &ins)), "SUM(A1:.A7)+SUM(B:.B)+ROWS(2.:.6)");
let del = Edit::Delete { axis: Axis::Cols, at: 0, count: 1 };
let e = parse("SUM(A1.:C1)+SUM(A:.A)").unwrap();
let e = parse("SUM(A1.:C1)+SUM(A:.A)")?;
assert_eq!(print(&adjust(e, "S", "S", &del)), "SUM(A1.:B1)+SUM(#REF!)");
assert_eq!(print(&shift_relative(parse("SUM(A1.:.B2)").unwrap(), 1, 1)), "SUM(B2.:.C3)");
let refs: Vec<String> = parse("SUM(Sheet2!A1:.B5)").unwrap().references().iter().map(|r| printer::reference_a1(r)).collect();
assert_eq!(print(&shift_relative(parse("SUM(A1.:.B2)")?, 1, 1)), "SUM(B2.:.C3)");
let refs: Vec<String> = parse("SUM(Sheet2!A1:.B5)")?.references().iter().map(|r| printer::reference_a1(r)).collect();
assert_eq!(refs, ["Sheet2!A1:B5"]);
Ok(())
}
#[test]
fn prints_long_flat_chain_without_deep_recursion() {
fn prints_long_flat_chain_without_deep_recursion() -> Result<(), Box<dyn std::error::Error>> {
// Small stack: a recursive printer overflows long before 20000 levels.
let handle = std::thread::Builder::new()
.stack_size(256 * 1024)
.spawn(|| {
let mut e = Expr::Number(1.0);
for _ in 0..20000 {
e = Expr::Binary(BinOp::Add, Box::new(e), Box::new(Expr::Number(1.0)));
}
let text = print(&e);
let r1c1 = print_r1c1(&e, gridcraft_core::CellRef { row: 0, col: 0 });
// Dropping the deep tree recurses; leak it so only the printer is under test.
std::mem::forget(e);
(text, r1c1)
})
.expect("spawn");
let (text, r1c1) = handle.join().expect("printer must not overflow the stack");
let handle = std::thread::Builder::new().stack_size(256 * 1024).spawn(|| {
let mut e = Expr::Number(1.0);
for _ in 0..20000 {
e = Expr::Binary(BinOp::Add, Box::new(e), Box::new(Expr::Number(1.0)));
}
let text = print(&e);
let r1c1 = print_r1c1(&e, gridcraft_core::CellRef { row: 0, col: 0 });
// Dropping the deep tree recurses; leak it so only the printer is under test.
std::mem::forget(e);
(text, r1c1)
})?;
let (text, r1c1) = handle.join().map_err(|_| "thread panicked")?;
assert_eq!(text, vec!["1"; 20001].join("+"));
assert_eq!(r1c1, text);
Ok(())
}
#[test]
fn hostile_formulas_are_errors_not_stack_overflows() {
fn hostile_formulas_are_errors_not_stack_overflows() -> Result<(), Box<dyn std::error::Error>> {
// A small stack: parsing must fail cleanly instead of building trees deeper than the nesting
// limit, which dropping, printing and evaluating walk recursively.
let handle = std::thread::Builder::new()
.stack_size(512 * 1024)
.spawn(|| {
let percents = format!("1{}", "%".repeat(1_000));
let invokes = format!("LAMBDA(x,x)(1){}", "(1)".repeat(1_000));
let long_chain = format!("1{}", "+1".repeat(100_000));
let huge_percents = format!("1{}", "%".repeat(1_000_000));
(parse(&percents).is_err(), parse(&invokes).is_err(), parse(&long_chain).is_err(), parse(&huge_percents).is_err())
})
.expect("spawn");
assert_eq!(handle.join().expect("parsing must not overflow the stack"), (true, true, true, true));
let handle = std::thread::Builder::new().stack_size(512 * 1024).spawn(|| {
let percents = format!("1{}", "%".repeat(1_000));
let invokes = format!("LAMBDA(x,x)(1){}", "(1)".repeat(1_000));
let long_chain = format!("1{}", "+1".repeat(100_000));
let huge_percents = format!("1{}", "%".repeat(1_000_000));
(parse(&percents).is_err(), parse(&invokes).is_err(), parse(&long_chain).is_err(), parse(&huge_percents).is_err())
})?;
assert_eq!(handle.join().map_err(|_| "thread panicked")?, (true, true, true, true));
// Within the limits these still parse.
assert!(parse(&format!("1{}", "%".repeat(100))).is_ok());
assert!(parse("LAMBDA(x,x)(1)(2)").is_ok());
Ok(())
}
#[test]
fn formulas_longer_than_excel_allows_are_refused() {
fn formulas_longer_than_excel_allows_are_refused() -> Result<(), Box<dyn std::error::Error>> {
let at_limit = format!("1{}", "+1".repeat((crate::MAX_FORMULA_LEN - 1) / 2));
assert_eq!(at_limit.chars().count(), crate::MAX_FORMULA_LEN - 1);
assert!(parse(&at_limit).is_ok());
assert!(parse(&format!("={at_limit}")).is_ok(), "the leading = does not count");
let over = format!("{at_limit}+1");
let err = parse(&over).unwrap_err();
let err = parse(&over).err().ok_or("none")?;
assert!(err.msg.contains("8192"), "{}", err.msg);
// Characters, not bytes: 8,000 two-byte characters in a text literal are within the limit.
let text = format!("\"{}\"", "é".repeat(8_000));
assert!(text.len() > crate::MAX_FORMULA_LEN);
assert!(parse(&text).is_ok());
Ok(())
}
#[test]
@@ -424,41 +436,40 @@ fn sheet_names_excel_quotes_stay_quoted() {
}
#[test]
fn longest_flat_chains_clone_compare_walk_and_adjust_without_deep_recursion() {
fn longest_flat_chains_clone_compare_walk_and_adjust_without_deep_recursion() -> Result<(), Box<dyn std::error::Error>> {
// `=1+1+…` with 4,096 operands is 8,192 characters, Excel's formula limit, and nests 4,095
// levels on its left. On a 1 MiB stack (the Windows main thread) the derived, recursive
// Clone overflowed when the engine stored such a formula (#916).
let handle = std::thread::Builder::new()
.stack_size(1 << 20)
.spawn(|| {
let plus = vec!["1"; 4096].join("+");
let e = parse(&plus).expect("parse");
let copy = e.clone();
assert!(copy == e);
let other = parse(&format!("{}-1", vec!["1"; 4095].join("+"))).expect("parse");
assert!(other != e);
let mut nodes = 0;
e.walk(&mut |_| nodes += 1);
assert_eq!(nodes, 2 * 4096 - 1);
assert_eq!(print(&copy.map(&mut |x| x)), plus);
// A chain of references: walk finds them in order, adjust rewrites every one.
let refs = (1..=1000).map(|r| format!("A{r}")).collect::<Vec<_>>().join("+");
let e = parse(&refs).expect("parse");
let found: Vec<String> = e.references().iter().map(|r| printer::reference_a1(r)).collect();
assert_eq!(found.first().map(String::as_str), Some("A1"));
assert_eq!(found.last().map(String::as_str), Some("A1000"));
assert_eq!(found.len(), 1000);
let ins = Edit::Insert { axis: Axis::Rows, at: 0, count: 1 };
let moved = print(&adjust(e.clone(), "S", "S", &ins));
assert_eq!(moved, (2..=1001).map(|r| format!("A{r}")).collect::<Vec<_>>().join("+"));
assert_eq!(print(&shift_relative(e, 0, 1)), (1..=1000).map(|r| format!("B{r}")).collect::<Vec<_>>().join("+"));
})
.expect("spawn");
handle.join().expect("long chains must not overflow the stack");
let handle = std::thread::Builder::new().stack_size(1 << 20).spawn(|| {
let plus = vec!["1"; 4096].join("+");
let e = parse(&plus)?;
let copy = e.clone();
assert!(copy == e);
let other = parse(&format!("{}-1", vec!["1"; 4095].join("+")))?;
assert!(other != e);
let mut nodes = 0;
e.walk(&mut |_| nodes += 1);
assert_eq!(nodes, 2 * 4096 - 1);
assert_eq!(print(&copy.map(&mut |x| x)), plus);
// A chain of references: walk finds them in order, adjust rewrites every one.
let refs = (1..=1000).map(|r| format!("A{r}")).collect::<Vec<_>>().join("+");
let e = parse(&refs)?;
let found: Vec<String> = e.references().iter().map(|r| printer::reference_a1(r)).collect();
assert_eq!(found.first().map(String::as_str), Some("A1"));
assert_eq!(found.last().map(String::as_str), Some("A1000"));
assert_eq!(found.len(), 1000);
let ins = Edit::Insert { axis: Axis::Rows, at: 0, count: 1 };
let moved = print(&adjust(e.clone(), "S", "S", &ins));
assert_eq!(moved, (2..=1001).map(|r| format!("A{r}")).collect::<Vec<_>>().join("+"));
assert_eq!(print(&shift_relative(e, 0, 1)), (1..=1000).map(|r| format!("B{r}")).collect::<Vec<_>>().join("+"));
Ok::<(), crate::ParseError>(())
})?;
handle.join().map_err(|_| "thread panicked")??;
Ok(())
}
#[test]
fn spanish_input_counts_left_operand_of_a_chain_at_the_same_depth() {
fn spanish_input_counts_left_operand_of_a_chain_at_the_same_depth() -> Result<(), Box<dyn std::error::Error>> {
// The leftmost operand of a flat chain is not nested, so `(((…1)))+1` is accepted whenever
// `(((…1)))` is, in both the pre-check and `normalize`.
let wrap = |n: usize| {
@@ -469,17 +480,18 @@ fn spanish_input_counts_left_operand_of_a_chain_at_the_same_depth() {
e
};
let es = |e: &Expr| crate::print_input(e, FormulaLocale::Es, |_| false);
let deepest = (0..400).take_while(|&n| es(&wrap(n)).is_ok()).last().expect("depth 0 is accepted");
let deepest = (0..400).take_while(|&n| es(&wrap(n)).is_ok()).last().ok_or("none")?;
assert!(deepest > 1 && deepest < 400);
let chain = Expr::Binary(BinOp::Add, Box::new(wrap(deepest)), Box::new(Expr::Number(1.0)));
assert!(es(&chain).is_ok());
Ok(())
}
#[test]
fn walk_is_preorder_and_map_is_postorder_left_to_right() {
fn walk_is_preorder_and_map_is_postorder_left_to_right() -> Result<(), Box<dyn std::error::Error>> {
// `(A1+B1)+SUM(C1,D1)-E1`: a chain with a call on its right. walk visits parents before
// children, map rewrites children (left to right) before their parent.
let e = parse("(A1+B1)+SUM(C1,D1)-E1").unwrap();
let e = parse("(A1+B1)+SUM(C1,D1)-E1")?;
let mut walked = Vec::new();
e.walk(&mut |n| walked.push(print(n)));
assert_eq!(walked, ["(A1+B1)+SUM(C1,D1)-E1", "(A1+B1)+SUM(C1,D1)", "(A1+B1)", "A1+B1", "A1", "B1", "SUM(C1,D1)", "C1", "D1", "E1"]);
@@ -490,19 +502,21 @@ fn walk_is_preorder_and_map_is_postorder_left_to_right() {
});
assert_eq!(mapped, ["A1", "B1", "A1+B1", "(A1+B1)", "C1", "D1", "SUM(C1,D1)", "(A1+B1)+SUM(C1,D1)", "E1", "(A1+B1)+SUM(C1,D1)-E1"]);
assert_eq!(print(&out), "(A1+B1)+SUM(C1,D1)-E1");
Ok(())
}
#[test]
fn missing_closing_parentheses_are_closed_and_errors_describe_the_typed_text() {
fn missing_closing_parentheses_are_closed_and_errors_describe_the_typed_text() -> Result<(), Box<dyn std::error::Error>> {
// #2030: `1+` reported "unexpected `)`", the error of the last retry with `)` appended.
let entry = |src: &str| parse_closing_parens(src, parse);
assert_eq!(entry("SUM(1,2").map(|e| print(&e)), Ok("SUM(1,2)".to_string()));
assert_eq!(entry("((1+2").map(|e| print(&e)), Ok("((1+2))".to_string()));
assert_eq!(entry("1+2").map(|e| print(&e)), Ok("1+2".to_string()));
for src in ["1+", "--", "SUM(1,+", "1+*2", &format!("{}1", "(".repeat(9))] {
assert_eq!(entry(src).unwrap_err(), parse(src).unwrap_err(), "{src}");
assert_eq!(entry(src).err().ok_or("none")?, parse(src).err().ok_or("none")?, "{src}");
}
assert_eq!(entry("1+").unwrap_err().msg, "unexpected end of formula");
assert_eq!(entry("1+").err().ok_or("none")?.msg, "unexpected end of formula");
Ok(())
}
#[test]
@@ -521,7 +535,7 @@ fn three_d_span_and_cell_start_still_lex_as_span() {
/// References print the same through the appending printer for every reference shape.
#[test]
fn reference_shapes_print_unchanged() {
fn reference_shapes_print_unchanged() -> Result<(), Box<dyn std::error::Error>> {
for text in [
"A1",
"XFD1048576",
@@ -536,10 +550,11 @@ fn reference_shapes_print_unchanged() {
"'Sheet 1:Sheet3'!B2",
"SUM(A1:.B3)+SUM(A.:.B)",
] {
let e = parse(text).unwrap();
let e = parse(text)?;
assert_eq!(print(&e), text);
if let Expr::Ref(r) = &e {
assert_eq!(printer::reference_a1(r), text);
}
}
Ok(())
}