diff --git a/crates/engine/src/cmd/formulas.rs b/crates/engine/src/cmd/formulas.rs index 2bea26cda..e72a654ab 100644 --- a/crates/engine/src/cmd/formulas.rs +++ b/crates/engine/src/cmd/formulas.rs @@ -218,15 +218,26 @@ fn valid_name(n: &str) -> bool { if !(first.is_alphabetic() || first == '_' || first == '\\') { return false; } - if n.len() > 255 || n.eq_ignore_ascii_case("R") || n.eq_ignore_ascii_case("C") { - return false; - } - if CellRef::parse(n).is_some() { + if n.len() > 255 || CellRef::parse(n).is_some() || is_r1c1(n) { return false; } n.chars().all(|c| c.is_alphanumeric() || matches!(c, '_' | '.' | '\\' | '?')) } +/// Looks like an R1C1 reference (`R`, `C`, `RC`, `R2`, `C3`, `R1C1`), which Excel doesn't +/// accept as a name. +fn is_r1c1(n: &str) -> bool { + let digits = |s: &str| s.bytes().take_while(u8::is_ascii_digit).count(); + let mut rest = n; + if let Some(r) = rest.strip_prefix(['R', 'r']) { + rest = r.get(digits(r)..).unwrap_or(""); + } + if let Some(c) = rest.strip_prefix(['C', 'c']) { + rest = c.get(digits(c)..).unwrap_or(""); + } + rest.is_empty() && !n.is_empty() +} + fn define_name(s: &mut Session, p: &Json) -> Result { let Some(name) = str_param(p, "name").map(str::to_string) else { s.ui_requests.push(crate::UiRequest::Dialog("defineName".into(), json!({}))); diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index f3e36cdf4..eb8bcbd55 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -121,6 +121,17 @@ fn tables_and_structured_refs() { assert_eq!(v(&s, "B4"), Value::Number(7.0)); } +#[test] +fn names_that_look_like_references_are_refused() { + let mut s = s(); + for bad in ["R1C1", "r2", "C3", "RC", "R", "c", "rc12", "A1", "XFD1048576", "1st"] { + assert!(s.execute("formulas.defineName", json!({"name": bad, "refersTo": "=1"})).is_err(), "{bad}"); + } + for good in ["R1C1X", "Rate", "RCx", "Rx", "_R1", "Cost"] { + s.execute("formulas.defineName", json!({"name": good, "refersTo": "=1"})).unwrap(); + } +} + #[test] fn sheets() { let mut s = s();