From a890865eaf2d8ebc95660e07e90f5bbfe5a1d925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Hatcher=20Andr=C3=A9s?= Date: Wed, 29 Oct 2025 23:26:18 +0100 Subject: [PATCH] FIX: Quote sheet names properly (#486) Fixes #485 --- base/src/expressions/utils/mod.rs | 47 +++++++++++++++++++++++--- base/src/test/mod.rs | 1 + base/src/test/test_sheet_names.rs | 16 +++++++++ bindings/wasm/src/lib.rs | 11 +++++- webapp/IronCalc/src/components/util.ts | 34 +++---------------- 5 files changed, 74 insertions(+), 35 deletions(-) create mode 100644 base/src/test/test_sheet_names.rs diff --git a/base/src/expressions/utils/mod.rs b/base/src/expressions/utils/mod.rs index 6e52c40..3fee1fb 100644 --- a/base/src/expressions/utils/mod.rs +++ b/base/src/expressions/utils/mod.rs @@ -259,15 +259,23 @@ pub fn is_valid_identifier(name: &str) -> bool { fn name_needs_quoting(name: &str) -> bool { let chars = name.chars(); // it contains any of these characters: ()'$,;-+{} or space - for char in chars { + for (i, char) in chars.enumerate() { if [' ', '(', ')', '\'', '$', ',', ';', '-', '+', '{', '}'].contains(&char) { return true; } + // if it starts with a number + if i == 0 && char.is_ascii_digit() { + return true; + } + } + if parse_reference_a1(name).is_some() { + // cell reference in A1 notation, e.g. B1048576 is quoted, B1048577 is not + return true; + } + if parse_reference_r1c1(name).is_some() { + // cell reference in R1C1 notation, e.g. RC, RC2, R5C, R-4C, RC-8, R, C + return true; } - // TODO: - // cell reference in A1 notation, e.g. B1048576 is quoted, B1048577 is not - // cell reference in R1C1 notation, e.g. RC, RC2, R5C, R-4C, RC-8, R, C - // integers false } @@ -279,3 +287,32 @@ pub fn quote_name(name: &str) -> String { }; name.to_string() } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_quote_name() { + assert_eq!(quote_name("Sheet1"), "Sheet1"); + assert_eq!(quote_name("Sheet 1"), "'Sheet 1'"); + // escape and quote + assert_eq!(quote_name("Sheet1'"), "'Sheet1'''"); + assert_eq!(quote_name("Data(2024)"), "'Data(2024)'"); + assert_eq!(quote_name("Data$2024"), "'Data$2024'"); + assert_eq!(quote_name("Data-2024"), "'Data-2024'"); + assert_eq!(quote_name("Data+2024"), "'Data+2024'"); + assert_eq!(quote_name("Data,2024"), "'Data,2024'"); + assert_eq!(quote_name("Data;2024"), "'Data;2024'"); + assert_eq!(quote_name("Data{2024}"), "'Data{2024}'"); + + assert_eq!(quote_name("2024"), "'2024'"); + assert_eq!(quote_name("1Data"), "'1Data'"); + assert_eq!(quote_name("A1"), "'A1'"); + assert_eq!(quote_name("R1C1"), "'R1C1'"); + assert_eq!(quote_name("MySheet"), "MySheet"); + + assert_eq!(quote_name("B1048576"), "'B1048576'"); + assert_eq!(quote_name("B1048577"), "B1048577"); + } +} diff --git a/base/src/test/mod.rs b/base/src/test/mod.rs index 0905635..76def25 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -75,6 +75,7 @@ mod test_log10; mod test_networkdays; mod test_percentage; mod test_set_functions_error_handling; +mod test_sheet_names; mod test_today; mod test_types; mod user_model; diff --git a/base/src/test/test_sheet_names.rs b/base/src/test/test_sheet_names.rs new file mode 100644 index 0000000..adcb5fb --- /dev/null +++ b/base/src/test/test_sheet_names.rs @@ -0,0 +1,16 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn sheet_number_name() { + let mut model = new_empty_model(); + model.new_sheet(); + model._set("A1", "7"); + model._set("A2", "=Sheet2!C3"); + model.evaluate(); + model.rename_sheet("Sheet2", "2024").unwrap(); + model.evaluate(); + assert_eq!(model.workbook.get_worksheet_names(), ["Sheet1", "2024"]); + assert_eq!(model._get_text("A2"), "0"); +} diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index 8517b0a..d0481b9 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -5,7 +5,11 @@ use wasm_bindgen::{ }; use ironcalc_base::{ - expressions::{lexer::util::get_tokens as tokenizer, types::Area, utils::number_to_column}, + expressions::{ + lexer::util::get_tokens as tokenizer, + types::Area, + utils::{number_to_column, quote_name as quote_name_ic}, + }, types::{CellType, Style}, worksheet::NavigationDirection, BorderArea, ClipboardData, UserModel as BaseModel, @@ -31,6 +35,11 @@ pub fn column_name_from_number(column: i32) -> Result { } } +#[wasm_bindgen(js_name = "quoteName")] +pub fn quote_name(name: &str) -> String { + quote_name_ic(name) +} + #[derive(Serialize)] struct DefinedName { name: String, diff --git a/webapp/IronCalc/src/components/util.ts b/webapp/IronCalc/src/components/util.ts index 52b6dd7..c94b8fc 100644 --- a/webapp/IronCalc/src/components/util.ts +++ b/webapp/IronCalc/src/components/util.ts @@ -1,36 +1,12 @@ import type { Area, Cell } from "./types"; -import { type SelectedView, columnNameFromNumber } from "@ironcalc/wasm"; +import { + type SelectedView, + columnNameFromNumber, + quoteName, +} from "@ironcalc/wasm"; import { LAST_COLUMN, LAST_ROW } from "./WorksheetCanvas/constants"; -// FIXME: Use the `quoteName` function from the wasm module -function nameNeedsQuoting(name: string): boolean { - // it contains any of these characters: ()'$,;-+{} or space - for (const char of name) { - if (" ()'$,;-+{}".includes(char)) { - return true; - } - } - - // TODO: - // - cell reference in A1 notation, e.g. B1048576 is quoted, B1048577 is not - // - cell reference in R1C1 notation, e.g. RC, RC2, R5C, R-4C, RC-8, R, C - // - integers - - return false; -} - -/** - * Quotes a string sheet name if it needs to - * NOTE: Invalid characters in a sheet name: \, /, *, [, ], :, ? - */ -export function quoteName(name: string): string { - if (nameNeedsQuoting(name)) { - return `'${name.replace(/'/g, "''")}'`; - } - return name; -} - /** * Returns true if the keypress should start editing */