From 63bc6f33b6e46212e321405cf69cf63894c8134e Mon Sep 17 00:00:00 2001 From: James Pozdena Date: Fri, 23 Jan 2026 10:26:54 -0700 Subject: [PATCH 1/2] fix(calc): strip quotes from sheet names Excel wraps sheet names containing spaces or special characters in single quotes. The calculation engine previously failed to resolve these references because it included literal quotes in the sheet lookup. Strip surrounding quotes during parsing to ensure formulas correctly resolve data across sheets with spaces, hyphens, or other special characters. --- calc.go | 5 ++ calc_test.go | 172 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+) diff --git a/calc.go b/calc.go index 96bfdf11d7..b8fda7f072 100644 --- a/calc.go +++ b/calc.go @@ -1534,6 +1534,11 @@ func parseRef(ref string) (cellRef, bool, bool, error) { ) if len(tokens) == 2 { // have a worksheet cr.Sheet, cell = tokens[0], tokens[1] + // Strip surrounding single quotes from sheet name if present + // Excel requires quotes around sheet names with spaces/special chars: 'Sheet Name'!A1 + if len(cr.Sheet) >= 2 && cr.Sheet[0] == '\'' && cr.Sheet[len(cr.Sheet)-1] == '\'' { + cr.Sheet = cr.Sheet[1 : len(cr.Sheet)-1] + } } if cr.Col, cr.Row, err = CellNameToCoordinates(cell); err != nil { if cr.Col, colErr = ColumnNameToNumber(cell); colErr == nil { // cast to column diff --git a/calc_test.go b/calc_test.go index 3d29afbfec..ae3a3ed241 100644 --- a/calc_test.go +++ b/calc_test.go @@ -6821,3 +6821,175 @@ func TestCalcTrendGrowthRegression(t *testing.T) { mtx := [][]float64{} calcTrendGrowthRegression(false, false, 0, 0, 0, 0, 0, mtx, mtx, mtx, mtx) } + +func TestCalcCellValueWithNamedRangesInFormula(t *testing.T) { + f := NewFile() + defer func() { + assert.NoError(t, f.Close()) + }() + + sheetName := "Test - Sheet" + idx, err := f.NewSheet(sheetName) + assert.NoError(t, err) + f.SetActiveSheet(idx) + + // Create cells with values + assert.NoError(t, f.SetCellValue(sheetName, "A1", 100)) + assert.NoError(t, f.SetCellValue(sheetName, "A2", 20)) + assert.NoError(t, f.SetCellValue(sheetName, "A3", 30)) + assert.NoError(t, f.SetCellValue(sheetName, "A4", 5)) + + // Define named ranges with periods in names to test dot notation support + assert.NoError(t, f.SetDefinedName(&DefinedName{ + Name: "value.first", + RefersTo: "'Test - Sheet'!$A$1", + Scope: "Workbook", + })) + assert.NoError(t, f.SetDefinedName(&DefinedName{ + Name: "value_second", + RefersTo: "'Test - Sheet'!$A$2", + Scope: "Workbook", + })) + assert.NoError(t, f.SetDefinedName(&DefinedName{ + Name: "value_third", + RefersTo: "'Test - Sheet'!$A$3", + Scope: "Workbook", + })) + assert.NoError(t, f.SetDefinedName(&DefinedName{ + Name: "value_fourth", + RefersTo: "'Test - Sheet'!$A$4", + Scope: "Workbook", + })) + + // Set formula that references multiple named ranges in arithmetic operations + assert.NoError(t, f.SetCellFormula(sheetName, "B1", "=value.first-value_second-value_third-value_fourth")) + + // Define a named range for the result cell + assert.NoError(t, f.SetDefinedName(&DefinedName{ + Name: "result", + RefersTo: "'Test - Sheet'!$B$1", + Scope: "Workbook", + })) + + // This should calculate to 100 - 20 - 30 - 5 = 45 + result, err := f.CalcCellValue(sheetName, "B1") + assert.NoError(t, err) + assert.NotEmpty(t, result, "Formula with named ranges should return a calculated value") + assert.Equal(t, "45", result, "Formula should calculate correctly: 100 - 20 - 30 - 5") +} + +func TestCalcWithNamedRangesVariousScenarios(t *testing.T) { + // Test 1: Named ranges on sheet without special chars + t.Run("SimpleSheetName", func(t *testing.T) { + f := NewFile() + defer func() { + assert.NoError(t, f.Close()) + }() + + assert.NoError(t, f.SetCellValue("Sheet1", "A1", 100)) + assert.NoError(t, f.SetCellValue("Sheet1", "A2", 50)) + + assert.NoError(t, f.SetDefinedName(&DefinedName{ + Name: "Value1", + RefersTo: "Sheet1!$A$1", + Scope: "Workbook", + })) + assert.NoError(t, f.SetDefinedName(&DefinedName{ + Name: "Value2", + RefersTo: "Sheet1!$A$2", + Scope: "Workbook", + })) + + assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "=Value1+Value2")) + result, err := f.CalcCellValue("Sheet1", "B1") + assert.NoError(t, err) + assert.Equal(t, "150", result) + }) + + // Test 2: Named ranges in functions + t.Run("NamedRangesInFunctions", func(t *testing.T) { + f := NewFile() + defer func() { + assert.NoError(t, f.Close()) + }() + + assert.NoError(t, f.SetCellValue("Sheet1", "A1", 10)) + assert.NoError(t, f.SetCellValue("Sheet1", "A2", 20)) + assert.NoError(t, f.SetCellValue("Sheet1", "A3", 30)) + + assert.NoError(t, f.SetDefinedName(&DefinedName{ + Name: "DataRange", + RefersTo: "Sheet1!$A$1:$A$3", + Scope: "Workbook", + })) + + assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "=SUM(DataRange)")) + result, err := f.CalcCellValue("Sheet1", "B1") + assert.NoError(t, err) + assert.Equal(t, "60", result) + }) + + // Test 3: Named ranges with sheet names containing various special chars + t.Run("SheetWithHyphens", func(t *testing.T) { + f := NewFile() + defer func() { + assert.NoError(t, f.Close()) + }() + + sheetName := "Data-Sheet-2025" + idx, err := f.NewSheet(sheetName) + assert.NoError(t, err) + f.SetActiveSheet(idx) + + assert.NoError(t, f.SetCellValue(sheetName, "C5", 42)) + + assert.NoError(t, f.SetDefinedName(&DefinedName{ + Name: "SpecialValue", + RefersTo: "'Data-Sheet-2025'!$C$5", + Scope: "Workbook", + })) + + assert.NoError(t, f.SetCellFormula(sheetName, "D5", "=SpecialValue*2")) + result, err := f.CalcCellValue(sheetName, "D5") + assert.NoError(t, err) + assert.Equal(t, "84", result) + }) + + // Test 4: Multiple arithmetic operations with named ranges + t.Run("ComplexArithmetic", func(t *testing.T) { + f := NewFile() + defer func() { + assert.NoError(t, f.Close()) + }() + + sheetName := "My Sheet" + idx, err := f.NewSheet(sheetName) + assert.NoError(t, err) + f.SetActiveSheet(idx) + + assert.NoError(t, f.SetCellValue(sheetName, "A1", 1000)) + assert.NoError(t, f.SetCellValue(sheetName, "A2", 200)) + assert.NoError(t, f.SetCellValue(sheetName, "A3", 50)) + + assert.NoError(t, f.SetDefinedName(&DefinedName{ + Name: "Total", + RefersTo: "'My Sheet'!$A$1", + Scope: "Workbook", + })) + assert.NoError(t, f.SetDefinedName(&DefinedName{ + Name: "Cost", + RefersTo: "'My Sheet'!$A$2", + Scope: "Workbook", + })) + assert.NoError(t, f.SetDefinedName(&DefinedName{ + Name: "Tax", + RefersTo: "'My Sheet'!$A$3", + Scope: "Workbook", + })) + + assert.NoError(t, f.SetCellFormula(sheetName, "B1", "=(Total-Cost)*Tax/100")) + result, err := f.CalcCellValue(sheetName, "B1") + assert.NoError(t, err) + assert.Equal(t, "400", result) + }) +} From 19c98e89319584b3a6d819703a96b273b8c5018b Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 24 Jan 2026 14:30:44 +0800 Subject: [PATCH 2/2] Simplify code and combine TestCalcCellValueWithNamedRangesInFormula test case into TestCalcWithDefinedName - Remove TestCalcWithNamedRangesVariousScenarios because the changes of calc.go already covered by TestCalcWithDefinedName --- calc.go | 7 +- calc_test.go | 194 ++++++--------------------------------------------- 2 files changed, 23 insertions(+), 178 deletions(-) diff --git a/calc.go b/calc.go index b8fda7f072..ffb3d9df34 100644 --- a/calc.go +++ b/calc.go @@ -1533,12 +1533,7 @@ func parseRef(ref string) (cellRef, bool, bool, error) { tokens = strings.Split(ref, "!") ) if len(tokens) == 2 { // have a worksheet - cr.Sheet, cell = tokens[0], tokens[1] - // Strip surrounding single quotes from sheet name if present - // Excel requires quotes around sheet names with spaces/special chars: 'Sheet Name'!A1 - if len(cr.Sheet) >= 2 && cr.Sheet[0] == '\'' && cr.Sheet[len(cr.Sheet)-1] == '\'' { - cr.Sheet = cr.Sheet[1 : len(cr.Sheet)-1] - } + cr.Sheet, cell = strings.TrimSuffix(strings.TrimPrefix(tokens[0], "'"), "'"), tokens[1] } if cr.Col, cr.Row, err = CellNameToCoordinates(cell); err != nil { if cr.Col, colErr = ColumnNameToNumber(cell); colErr == nil { // cast to column diff --git a/calc_test.go b/calc_test.go index ae3a3ed241..a371a38fca 100644 --- a/calc_test.go +++ b/calc_test.go @@ -4797,6 +4797,28 @@ func TestCalcWithDefinedName(t *testing.T) { result, err = f.CalcCellValue("Sheet1", "D1") assert.NoError(t, err) assert.Equal(t, "YES", result, "IF(\"B1_as_string\"=defined_name1,\"YES\",\"NO\")") + + t.Run("for_sheet_name_with_space", func(t *testing.T) { + f := NewFile() + defer func() { + assert.NoError(t, f.Close()) + }() + assert.NoError(t, f.SetSheetName("Sheet1", "Sheet 1")) + cells := []string{"A1", "A2", "A3", "A4"} + names := []string{"val1", "val2", "val3", "val4"} + for idx, v := range []interface{}{100, 20, 30, 5} { + assert.NoError(t, f.SetCellValue("Sheet 1", cells[idx], v)) + } + for idx, cell := range cells { + assert.NoError(t, f.SetDefinedName(&DefinedName{ + Name: names[idx], RefersTo: "'Sheet 1'!" + cell, + })) + } + assert.NoError(t, f.SetCellFormula("Sheet 1", "B1", "=val1-val2-val3-val4")) + result, err := f.CalcCellValue("Sheet 1", "B1") + assert.NoError(t, err) + assert.Equal(t, "45", result) + }) } func TestCalcISBLANK(t *testing.T) { @@ -6821,175 +6843,3 @@ func TestCalcTrendGrowthRegression(t *testing.T) { mtx := [][]float64{} calcTrendGrowthRegression(false, false, 0, 0, 0, 0, 0, mtx, mtx, mtx, mtx) } - -func TestCalcCellValueWithNamedRangesInFormula(t *testing.T) { - f := NewFile() - defer func() { - assert.NoError(t, f.Close()) - }() - - sheetName := "Test - Sheet" - idx, err := f.NewSheet(sheetName) - assert.NoError(t, err) - f.SetActiveSheet(idx) - - // Create cells with values - assert.NoError(t, f.SetCellValue(sheetName, "A1", 100)) - assert.NoError(t, f.SetCellValue(sheetName, "A2", 20)) - assert.NoError(t, f.SetCellValue(sheetName, "A3", 30)) - assert.NoError(t, f.SetCellValue(sheetName, "A4", 5)) - - // Define named ranges with periods in names to test dot notation support - assert.NoError(t, f.SetDefinedName(&DefinedName{ - Name: "value.first", - RefersTo: "'Test - Sheet'!$A$1", - Scope: "Workbook", - })) - assert.NoError(t, f.SetDefinedName(&DefinedName{ - Name: "value_second", - RefersTo: "'Test - Sheet'!$A$2", - Scope: "Workbook", - })) - assert.NoError(t, f.SetDefinedName(&DefinedName{ - Name: "value_third", - RefersTo: "'Test - Sheet'!$A$3", - Scope: "Workbook", - })) - assert.NoError(t, f.SetDefinedName(&DefinedName{ - Name: "value_fourth", - RefersTo: "'Test - Sheet'!$A$4", - Scope: "Workbook", - })) - - // Set formula that references multiple named ranges in arithmetic operations - assert.NoError(t, f.SetCellFormula(sheetName, "B1", "=value.first-value_second-value_third-value_fourth")) - - // Define a named range for the result cell - assert.NoError(t, f.SetDefinedName(&DefinedName{ - Name: "result", - RefersTo: "'Test - Sheet'!$B$1", - Scope: "Workbook", - })) - - // This should calculate to 100 - 20 - 30 - 5 = 45 - result, err := f.CalcCellValue(sheetName, "B1") - assert.NoError(t, err) - assert.NotEmpty(t, result, "Formula with named ranges should return a calculated value") - assert.Equal(t, "45", result, "Formula should calculate correctly: 100 - 20 - 30 - 5") -} - -func TestCalcWithNamedRangesVariousScenarios(t *testing.T) { - // Test 1: Named ranges on sheet without special chars - t.Run("SimpleSheetName", func(t *testing.T) { - f := NewFile() - defer func() { - assert.NoError(t, f.Close()) - }() - - assert.NoError(t, f.SetCellValue("Sheet1", "A1", 100)) - assert.NoError(t, f.SetCellValue("Sheet1", "A2", 50)) - - assert.NoError(t, f.SetDefinedName(&DefinedName{ - Name: "Value1", - RefersTo: "Sheet1!$A$1", - Scope: "Workbook", - })) - assert.NoError(t, f.SetDefinedName(&DefinedName{ - Name: "Value2", - RefersTo: "Sheet1!$A$2", - Scope: "Workbook", - })) - - assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "=Value1+Value2")) - result, err := f.CalcCellValue("Sheet1", "B1") - assert.NoError(t, err) - assert.Equal(t, "150", result) - }) - - // Test 2: Named ranges in functions - t.Run("NamedRangesInFunctions", func(t *testing.T) { - f := NewFile() - defer func() { - assert.NoError(t, f.Close()) - }() - - assert.NoError(t, f.SetCellValue("Sheet1", "A1", 10)) - assert.NoError(t, f.SetCellValue("Sheet1", "A2", 20)) - assert.NoError(t, f.SetCellValue("Sheet1", "A3", 30)) - - assert.NoError(t, f.SetDefinedName(&DefinedName{ - Name: "DataRange", - RefersTo: "Sheet1!$A$1:$A$3", - Scope: "Workbook", - })) - - assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "=SUM(DataRange)")) - result, err := f.CalcCellValue("Sheet1", "B1") - assert.NoError(t, err) - assert.Equal(t, "60", result) - }) - - // Test 3: Named ranges with sheet names containing various special chars - t.Run("SheetWithHyphens", func(t *testing.T) { - f := NewFile() - defer func() { - assert.NoError(t, f.Close()) - }() - - sheetName := "Data-Sheet-2025" - idx, err := f.NewSheet(sheetName) - assert.NoError(t, err) - f.SetActiveSheet(idx) - - assert.NoError(t, f.SetCellValue(sheetName, "C5", 42)) - - assert.NoError(t, f.SetDefinedName(&DefinedName{ - Name: "SpecialValue", - RefersTo: "'Data-Sheet-2025'!$C$5", - Scope: "Workbook", - })) - - assert.NoError(t, f.SetCellFormula(sheetName, "D5", "=SpecialValue*2")) - result, err := f.CalcCellValue(sheetName, "D5") - assert.NoError(t, err) - assert.Equal(t, "84", result) - }) - - // Test 4: Multiple arithmetic operations with named ranges - t.Run("ComplexArithmetic", func(t *testing.T) { - f := NewFile() - defer func() { - assert.NoError(t, f.Close()) - }() - - sheetName := "My Sheet" - idx, err := f.NewSheet(sheetName) - assert.NoError(t, err) - f.SetActiveSheet(idx) - - assert.NoError(t, f.SetCellValue(sheetName, "A1", 1000)) - assert.NoError(t, f.SetCellValue(sheetName, "A2", 200)) - assert.NoError(t, f.SetCellValue(sheetName, "A3", 50)) - - assert.NoError(t, f.SetDefinedName(&DefinedName{ - Name: "Total", - RefersTo: "'My Sheet'!$A$1", - Scope: "Workbook", - })) - assert.NoError(t, f.SetDefinedName(&DefinedName{ - Name: "Cost", - RefersTo: "'My Sheet'!$A$2", - Scope: "Workbook", - })) - assert.NoError(t, f.SetDefinedName(&DefinedName{ - Name: "Tax", - RefersTo: "'My Sheet'!$A$3", - Scope: "Workbook", - })) - - assert.NoError(t, f.SetCellFormula(sheetName, "B1", "=(Total-Cost)*Tax/100")) - result, err := f.CalcCellValue(sheetName, "B1") - assert.NoError(t, err) - assert.Equal(t, "400", result) - }) -}