mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-02 07:06:48 -06:00
Formulas: keep _xludf. so user-defined calls stay user-defined (fixes #726)
The parser stripped _xludf. like _xlfn., but the two mean opposite things: _xludf. marks a name Excel must not treat as a built-in. Keeping it in the call name makes the call an unknown function (#NAME?, as in Excel) and the writer emits it unchanged, so _xludf.EOMONTH() no longer saves as an EOMONTH() call Excel repairs away.
This commit is contained in:
@@ -167,6 +167,11 @@ fn errors_and_cycles() {
|
||||
assert!(t.get("B1").is_error());
|
||||
t.set("C1", "=NOSUCHFN(1)");
|
||||
assert_eq!(t.get("C1"), Value::Error(CellError::Name));
|
||||
// `_xludf.` marks a user-defined function, never the built-in of the same name (#726).
|
||||
for f in ["=_xludf.CONCAT(\"a\",\"b\")", "=_xludf.SUM(1,2)", "=_xludf.EOMONTH()"] {
|
||||
t.set("C3", f);
|
||||
assert_eq!(t.get("C3"), Value::Error(CellError::Name), "{f}");
|
||||
}
|
||||
t.set("C2", "=1+");
|
||||
assert_eq!(t.get("C2"), Value::Error(CellError::Name));
|
||||
}
|
||||
|
||||
@@ -88,15 +88,19 @@ fn word_is_cell(w: &str) -> Option<Anchor> {
|
||||
Some(Anchor { row, col, row_abs, col_abs })
|
||||
}
|
||||
|
||||
/// `_xlfn.XLOOKUP` → `XLOOKUP`.
|
||||
/// `_xlfn.XLOOKUP` → `XLOOKUP`. `_xludf.` is kept (`_xludf.SUM`): it marks a user-defined
|
||||
/// function, never the built-in of the same name, and files need it back.
|
||||
pub fn normalize_function_name(name: &str) -> String {
|
||||
let u = name.to_ascii_uppercase();
|
||||
for p in ["_XLFN._XLWS.", "_XLFN.", "_XLWS.", "_XLUDF."] {
|
||||
for p in ["_XLFN._XLWS.", "_XLFN.", "_XLWS."] {
|
||||
if let Some(r) = u.strip_prefix(p) {
|
||||
return r.to_string();
|
||||
}
|
||||
}
|
||||
u
|
||||
match u.strip_prefix("_XLUDF.") {
|
||||
Some(r) => format!("_xludf.{r}"),
|
||||
None => u,
|
||||
}
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
|
||||
@@ -48,6 +48,7 @@ fn normalises() {
|
||||
assert_eq!(rt("=sum(a1:b2)"), "SUM(A1:B2)");
|
||||
assert_eq!(rt("= 1 + 2"), "1+2");
|
||||
assert_eq!(rt("_xlfn.XLOOKUP(1,A:A,B:B)"), "XLOOKUP(1,A:A,B:B)");
|
||||
assert_eq!(rt("_XLUDF.concat(\"a\",\"b\")"), "_xludf.CONCAT(\"a\",\"b\")");
|
||||
assert_eq!(rt("true"), "TRUE");
|
||||
assert_eq!(rt("'Sheet1'!A1"), "Sheet1!A1");
|
||||
}
|
||||
|
||||
@@ -197,7 +197,8 @@ fn registry() -> &'static (Vec<FnSpec>, HashMap<&'static str, usize>) {
|
||||
})
|
||||
}
|
||||
|
||||
/// Looks a function up by name (case-insensitive; `_xlfn.`, `_xlws.` and `_xludf.`-free names).
|
||||
/// Looks a function up by name (case-insensitive; `_xlfn.`- and `_xlws.`-free names). A
|
||||
/// `_xludf.` name is user-defined and never found.
|
||||
pub fn lookup(name: &str) -> Option<&'static FnSpec> {
|
||||
let mut n = name.trim().to_ascii_uppercase();
|
||||
loop {
|
||||
|
||||
@@ -495,3 +495,28 @@ fn text_box_formatting_is_read_and_written() {
|
||||
assert_eq!(rgb(g.text_outline.map(|o| o.0)), rgb(f.text_outline.map(|o| o.0)));
|
||||
assert_eq!(g.text_outline.map(|o| o.1), Some(1.5));
|
||||
}
|
||||
|
||||
/// `_xludf.` marks a user-defined call even when a built-in has the same name; dropping it turns
|
||||
/// the call into the built-in, and `_xludf.EOMONTH()` into a call Excel repairs away (#726).
|
||||
#[test]
|
||||
fn user_defined_function_prefix_round_trips() {
|
||||
let cases =
|
||||
[("A1", "_xludf.EOMONTH()"), ("A2", "_xludf.CONCAT(\"a\",\"b\")"), ("A3", "_xludf.SUM(1,2)"), ("A4", "MYUDF(1)"), ("A5", "_xludf.MYUDF(1)")];
|
||||
let rows: String = cases
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (a, f))| format!(r#"<row r="{}"><c r="{a}"><f>{}</f></c></row>"#, i + 1, f.replace('"', """)))
|
||||
.collect();
|
||||
let (wb, _) = read_xlsx(&minimal(&format!("<sheetData>{rows}</sheetData>"), &[], "", "")).unwrap();
|
||||
let s = wb.sheet(0).unwrap();
|
||||
for (a, f) in cases {
|
||||
assert_eq!(s.cell(at(a)).unwrap().formula.as_ref().unwrap().text, f, "read {a}");
|
||||
}
|
||||
let bytes = crate::write_xlsx(&wb).unwrap();
|
||||
let mut pkg = crate::package::Package::open(&bytes).unwrap();
|
||||
let sheet = pkg.read_xml("xl/worksheets/sheet1.xml").unwrap().unwrap();
|
||||
for (a, f) in cases {
|
||||
let cell = sheet.child("sheetData").unwrap().kids("row").flat_map(|r| r.kids("c")).find(|c| c.attr("r") == Some(a)).unwrap();
|
||||
assert_eq!(cell.child("f").unwrap().text, f, "written {a}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user