mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-24 10:46:50 -06:00
calc: one shared same_name for LET/LAMBDA names, with case folding
- gridcraft_formula::same_name lower-cases then upper-cases each character, so ß matches ẞ (to_uppercase alone maps ß to SS). - Formula-bar input uses it too, so a binding such as ſi is not translated to IF while the evaluator binds it. - Tests: ISOMITTED with a case variant, ß/ẞ, and the Spanish input case.
This commit is contained in:
@@ -256,7 +256,7 @@ impl<'h> Evaluator<'h> {
|
||||
}
|
||||
|
||||
fn name(&mut self, n: &str) -> Ev {
|
||||
if let Some((_, v)) = self.env.iter().rev().find(|(k, _)| same_name(k, n)) {
|
||||
if let Some((_, v)) = self.env.iter().rev().find(|(k, _)| gridcraft_formula::same_name(k, n)) {
|
||||
return v.clone();
|
||||
}
|
||||
let wb = self.host.workbook();
|
||||
@@ -772,7 +772,7 @@ impl<'h> Evaluator<'h> {
|
||||
Ev::L(Arc::new(Lambda { params, body: body.clone(), env: self.env.clone() }))
|
||||
}
|
||||
"ISOMITTED" => {
|
||||
let r = matches!(args.first(), Some(Expr::Name(n)) if self.env.iter().rev().find(|(k, _)| same_name(k, n)).is_some_and(|(_, v)| matches!(v, Ev::V(Value::Empty))));
|
||||
let r = matches!(args.first(), Some(Expr::Name(n)) if self.env.iter().rev().find(|(k, _)| gridcraft_formula::same_name(k, n)).is_some_and(|(_, v)| matches!(v, Ev::V(Value::Empty))));
|
||||
Ev::V(Value::Bool(r))
|
||||
}
|
||||
"MAP" | "REDUCE" | "SCAN" | "BYROW" | "BYCOL" | "MAKEARRAY" => self.higher_order(name, args),
|
||||
@@ -975,7 +975,7 @@ impl<'h> Evaluator<'h> {
|
||||
/// that name.
|
||||
fn function_by_name(&self, e: &Expr) -> Option<String> {
|
||||
let Expr::Name(n) = e else { return None };
|
||||
if self.env.iter().any(|(k, _)| same_name(k, n)) || self.host.workbook().name(n, self.sheet).is_some() {
|
||||
if self.env.iter().any(|(k, _)| gridcraft_formula::same_name(k, n)) || self.host.workbook().name(n, self.sheet).is_some() {
|
||||
return None;
|
||||
}
|
||||
let u = n.to_ascii_uppercase();
|
||||
@@ -1868,10 +1868,5 @@ pub fn is_known_function(n: &str) -> bool {
|
||||
is_special(&n.to_ascii_uppercase()) || gridcraft_functions::lookup(n).is_some()
|
||||
}
|
||||
|
||||
/// LET and LAMBDA names compare case-insensitively in any script, as in Excel (`dług` = `DŁUG`).
|
||||
fn same_name(a: &str, b: &str) -> bool {
|
||||
a.chars().flat_map(char::to_uppercase).eq(b.chars().flat_map(char::to_uppercase))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn unused(_: RefKind) {}
|
||||
|
||||
@@ -595,12 +595,17 @@ fn let_lambda_names_ignore_case_in_any_script() {
|
||||
let mut t = T::new();
|
||||
t.set("A1", "=LET(dług,100,DŁUG*2)");
|
||||
assert_eq!(t.num("A1"), 200.0);
|
||||
t.set("A2", "=LAMBDA(λ,ISOMITTED(Λ))()");
|
||||
assert_eq!(t.get("A2"), Value::Bool(true));
|
||||
t.set("A3", "=LAMBDA(ćma,ĆMA+1)(1)");
|
||||
assert_eq!(t.num("A3"), 2.0);
|
||||
t.set("A4", "=LET(ŻAR,LAMBDA(x,x*3),żar(2))");
|
||||
assert_eq!(t.num("A4"), 6.0);
|
||||
t.set("A5", "=LET(λ,LAMBDA(x,x+1),SUM(MAP({1,2},Λ)))");
|
||||
assert_eq!(t.num("A5"), 5.0);
|
||||
// `ß` upper-cases to `SS`, yet `ẞ` is its capital.
|
||||
t.set("A6", "=LET(ß,100,ẞ*2)");
|
||||
assert_eq!(t.num("A6"), 200.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -128,7 +128,7 @@ fn normalize(expr: &mut Expr, known_name: &impl Fn(&str) -> bool, bindings: &mut
|
||||
if depth > parser::MAX_DEPTH {
|
||||
return Err(ParseError { msg: "formula is nested too deeply".into(), pos: 0 });
|
||||
}
|
||||
let bound = |name: &str, bindings: &[String]| bindings.iter().rev().any(|n| n.eq_ignore_ascii_case(name)) || known_name(name);
|
||||
let bound = |name: &str, bindings: &[String]| bindings.iter().rev().any(|n| crate::same_name(n, name)) || known_name(name);
|
||||
match expr {
|
||||
Expr::Bool(value) if display => {
|
||||
let alias = if *value { "VERDADERO" } else { "FALSO" };
|
||||
|
||||
@@ -15,5 +15,13 @@ pub use input::{FormulaLocale, parse_input, print_input};
|
||||
pub use parser::{MAX_FORMULA_LEN, ParseError, normalize_function_name, parse};
|
||||
pub use printer::{print, print_r1c1, quote_sheet};
|
||||
|
||||
/// Whether two LET/LAMBDA names are the same: case-insensitive in any script, as in Excel
|
||||
/// (`dług` = `DŁUG`, `ß` = `ẞ`). Each character is lower-cased, then upper-cased, so letters whose
|
||||
/// upper case is not one character (`ß` -> `SS`) still match their capital.
|
||||
pub fn same_name(a: &str, b: &str) -> bool {
|
||||
let fold = |s: &str| s.chars().flat_map(char::to_lowercase).flat_map(char::to_uppercase).collect::<String>();
|
||||
a == b || fold(a) == fold(b)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -98,3 +98,12 @@ fn spanish_display_round_trips_text_containing_separators() {
|
||||
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));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_names_match_aliases_in_any_case_like_the_evaluator() {
|
||||
// `ſi` (long s) is `SI` case-insensitively, so the call is the LAMBDA, not IF.
|
||||
assert_eq!(input("LET(ſi;LAMBDA(x;x*2);SI(3))"), "LET(ſi,LAMBDA(x,x*2),SI(3))");
|
||||
assert!(gridcraft_formula::same_name("ß", "ẞ"));
|
||||
assert!(gridcraft_formula::same_name("dług", "DŁUG"));
|
||||
assert!(!gridcraft_formula::same_name("a", "b"));
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ workbooks; results checked in as data, not Excel files) and fixing what it finds
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-19 | minor | LET and LAMBDA names compare case-insensitively in any script, as in Excel: `=LET(dług,100,DŁUG*2)` is 200 (was #NAME?); also LET-bound lambdas called or passed by name (`żar(2)`, `MAP(…,Λ)`) |
|
||||
| 2026-10-19 | minor | LET and LAMBDA names compare case-insensitively in any script, as in Excel: `=LET(dług,100,DŁUG*2)` is 200 (was #NAME?); also LET-bound lambdas called or passed by name (`żar(2)`, `MAP(…,Λ)`), `ISOMITTED`, and formula-bar alias translation (one shared `gridcraft_formula::same_name`, which also matches `ß` with `ẞ`) |
|
||||
| 2026-10-15 | minor | MEDIAN and AGGREGATE median use an overflow-safe midpoint (#1579); regressions cover finite extremes, tiny values and dependent recalculation |
|
||||
| 2026-10-13 | minor | Clear reports changed stored cells sparsely, including those beyond the first million coordinates; dependent formulas and removed spills update, while clearing empty merges still unblocks spills (#1032) |
|
||||
| 2026-10-12 | minor | USDOLLAR added as a Compatibility function (not on Microsoft's list, so the counts don't change); MIRR with no positive flow is -1, COUNTIF's bare `"<="` / `">="` skip empty strings, INDIRECT with `a1` = FALSE gives `#REF!` for A1-style text (#738) |
|
||||
|
||||
Reference in New Issue
Block a user