#2012: chart names and macro examples unquote sheet names like formulas do

Three places still split a `Sheet!Name` reference and stripped the
sheet's quotes by hand: the chart resolver (is a series name a defined
name?), the XLSX chart writer (the name's cache) and the macro editor's
example references. The first two left a doubled quote in the name, so
a series named `'Bob''s'!Title` (a name on the sheet "Bob's") wasn't
found and showed that text instead of the name's value; they also split
at the first `!`, so a quoted sheet holding one broke. All three now use
core's split_sheet, the one reading formulas use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hector VillarrealandClaude Opus 5.5 committed 2026-11-03 01:49:01 -06:00
1 parent 178f3eb745
commit e89ba799e9
4 files changed
+55 -11

No files matched your search

+4 -3
View File
@@ -179,9 +179,10 @@ fn category_table(wb: &Workbook, sheet: usize, formula: &str, values: &str) -> (
/// Whether `t` names a defined name seen from `sheet` (`Name`, or `Sheet1!Name` looked up on
/// that sheet), the way the evaluator looks names up.
fn is_defined_name(wb: &Workbook, sheet: usize, t: &str) -> bool {
match t.split_once('!') {
Some((s, bare)) => wb.sheet_index(s.trim_matches('\'')).is_some_and(|i| wb.name(bare, i).is_some()),
None => wb.name(t, sheet).is_some(),
// `'Bob''s'!Name`: the sheet is unquoted as everywhere else (#2012).
match gridcraft_core::split_sheet(t) {
(Some(s), bare) => wb.sheet_index(&s).is_some_and(|i| wb.name(bare, i).is_some()),
(None, _) => wb.name(t, sheet).is_some(),
}
}
+36
View File
@@ -1258,3 +1258,39 @@ fn box_whisker_draws_each_series_quartile_method() {
// The boxes span 3.5 and 2.5 data units on the same value axis.
assert!((heights[0] / heights[1] - 1.4).abs() < 0.001);
}
#[test]
fn a_series_named_by_a_name_on_a_sheet_with_an_apostrophe_resolves() {
// #2012: `'Bob''s'!Title` kept the doubled quote, so the sheet wasn't found and the series
// was named by the literal text.
let mut wb = Workbook::new();
{
let s = wb.sheet_mut(0).unwrap();
s.name = "Bob's".into();
s.set_value(CellRef::new(0, 0), Value::from("Takings"));
s.set_value(CellRef::new(1, 0), Value::Number(4.0));
s.set_value(CellRef::new(2, 0), Value::Number(9.0));
}
wb.names.push(gridcraft_model::DefinedName {
name: "Title".into(),
scope: Some(0),
formula: "'Bob''s'!$A$1".into(),
comment: String::new(),
hidden: false,
});
let mut c = chart(ChartKind::ColumnClustered);
c.series = vec![Series {
name: Some("'Bob''s'!Title".into()),
categories: None,
values: "'Bob''s'!$A$2:$A$3".into(),
bubble_sizes: None,
color: None,
secondary: false,
kind: None,
quartiles: Default::default(),
smooth: false,
trendlines: Vec::new(),
}];
let d = resolve(&wb, 0, &c);
assert_eq!(d.series.first().map(|s| s.name.as_str()), Some("Takings"));
}
+10 -4
View File
@@ -459,10 +459,7 @@ pub fn parse_refs(text: &str) -> Vec<(Option<String>, RangeRef)> {
text.split(',')
.filter_map(|part| {
let part = part.trim();
let (sheet, body) = match part.rsplit_once('!') {
Some((s, b)) => (Some(s.trim().trim_matches('\'').replace("''", "'")), b),
None => (None, part),
};
let (sheet, body) = gridcraft_engine::core::split_sheet(part);
RangeRef::parse(&body.replace('$', "")).map(|r| (sheet, r))
})
.take(500)
@@ -2399,6 +2396,15 @@ pub fn paint_overlay(p: &egui::Painter, geo: &crate::grid::Geo, sh: &gridcraft_e
mod tests {
use super::*;
#[test]
fn example_references_unquote_sheet_names_as_formulas_do() {
// #2012: one shared reading of `'Bob''s'!A1` and `'a!b'!A1`.
let refs = parse_refs("'Bob''s'!$A$1:$B$2, 'a!b'!C3, D4");
let sheets: Vec<Option<String>> = refs.iter().map(|(s, _)| s.clone()).collect();
assert_eq!(sheets, [Some("Bob's".to_string()), Some("a!b".to_string()), None]);
assert_eq!(refs.iter().map(|(_, r)| r.a1()).collect::<Vec<_>>(), ["A1:B2", "C3", "D4"]);
}
#[test]
fn an_edited_input_keeps_a_shape_a_macro_may_declare() {
let range = NamedInput { rows: [2, 10], cols: [3, 3], header: true, col_types: vec![ColType::Number; 3], ..NamedInput::default() };
+5 -4
View File
@@ -366,12 +366,13 @@ fn is_ref(f: &str) -> bool {
/// when it isn't a name, or names a sheet the workbook doesn't have.
fn chart_name<'a>(wb: &'a Workbook, si: usize, f: &str) -> Option<(Option<usize>, String, Option<&'a DefinedName>)> {
let Ok(Expr::Name(n)) = gridcraft_formula::parse(f.trim()) else { return None };
match n.split_once('!') {
Some((s, bare)) => {
let i = wb.sheet_index(s.trim_matches('\''))?;
// `'Bob''s'!Name`: the sheet is unquoted as everywhere else (#2012).
match gridcraft_core::split_sheet(&n) {
(Some(s), bare) => {
let i = wb.sheet_index(&s)?;
Some((Some(i), bare.to_string(), wb.name(bare, i)))
}
None => {
(None, _) => {
let def = wb.name(&n, si);
Some((None, n, def))
}