FIX: Quote sheet names properly (#486)

Fixes #485
This commit is contained in:
Nicolás Hatcher Andrés
2025-10-29 23:26:18 +01:00
committed by GitHub
parent 1edfb2df1c
commit a890865eaf
5 changed files with 74 additions and 35 deletions

View File

@@ -259,15 +259,23 @@ pub fn is_valid_identifier(name: &str) -> bool {
fn name_needs_quoting(name: &str) -> bool { fn name_needs_quoting(name: &str) -> bool {
let chars = name.chars(); let chars = name.chars();
// it contains any of these characters: ()'$,;-+{} or space // it contains any of these characters: ()'$,;-+{} or space
for char in chars { for (i, char) in chars.enumerate() {
if [' ', '(', ')', '\'', '$', ',', ';', '-', '+', '{', '}'].contains(&char) { if [' ', '(', ')', '\'', '$', ',', ';', '-', '+', '{', '}'].contains(&char) {
return true; 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 false
} }
@@ -279,3 +287,32 @@ pub fn quote_name(name: &str) -> String {
}; };
name.to_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");
}
}

View File

@@ -75,6 +75,7 @@ mod test_log10;
mod test_networkdays; mod test_networkdays;
mod test_percentage; mod test_percentage;
mod test_set_functions_error_handling; mod test_set_functions_error_handling;
mod test_sheet_names;
mod test_today; mod test_today;
mod test_types; mod test_types;
mod user_model; mod user_model;

View File

@@ -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");
}

View File

@@ -5,7 +5,11 @@ use wasm_bindgen::{
}; };
use ironcalc_base::{ 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}, types::{CellType, Style},
worksheet::NavigationDirection, worksheet::NavigationDirection,
BorderArea, ClipboardData, UserModel as BaseModel, BorderArea, ClipboardData, UserModel as BaseModel,
@@ -31,6 +35,11 @@ pub fn column_name_from_number(column: i32) -> Result<String, JsError> {
} }
} }
#[wasm_bindgen(js_name = "quoteName")]
pub fn quote_name(name: &str) -> String {
quote_name_ic(name)
}
#[derive(Serialize)] #[derive(Serialize)]
struct DefinedName { struct DefinedName {
name: String, name: String,

View File

@@ -1,36 +1,12 @@
import type { Area, Cell } from "./types"; 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"; 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 * Returns true if the keypress should start editing
*/ */