Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions calc.go
Original file line number Diff line number Diff line change
Expand Up @@ -1645,6 +1645,9 @@ func (cr *cellRange) prepareCellRange(col, row bool, cellRef cellRef) error {
// characters and default sheet name.
func (f *File) parseReference(ctx *calcContext, sheet, reference string) (formulaArg, error) {
reference = strings.ReplaceAll(reference, "$", "")
if parts := split3DReference(reference); len(parts) == 3 {
return f.parse3DReference(ctx, parts)
}
ranges, cellRanges, cellRefs := strings.Split(reference, ":"), list.New(), list.New()
if len(ranges) > 1 {
var cr cellRange
Expand Down Expand Up @@ -1684,6 +1687,89 @@ func (f *File) parseReference(ctx *calcContext, sheet, reference string) (formul
return f.rangeResolver(ctx, cellRefs, cellRanges)
}

// parse3DReference detects a 3D reference from a range reference and expands it
// across the workbook-order sheet range.
func (f *File) parse3DReference(ctx *calcContext, parts []string) (formulaArg, error) {
firstSheet, lastSheet, cellRef := parts[0], parts[1], parts[2]
sheets, err := f.expand3DSheetRange(firstSheet, lastSheet)
if err != nil {
return newErrorFormulaArg(formulaErrorREF, formulaErrorREF), err
}
var matrix [][]formulaArg
for _, sheet := range sheets {
result, err := f.parseReference(ctx, sheet, cellRef)
if err != nil {
return newErrorFormulaArg(formulaErrorNAME, formulaErrorNAME), err
}
switch result.Type {
case ArgMatrix:
matrix = append(matrix, result.Matrix...)
default:
matrix = append(matrix, []formulaArg{result})
}
}
return newMatrixFormulaArg(matrix), err
}

// split3DReference parses a reference string for a 3D reference of the form
// formula structure in FirstSheet:LastSheet!CellReference and returns the three
// components if valid, or an empty slice if not.
func split3DReference(reference string) []string {
var parts []string
idx := strings.Index(reference, "!")
if idx < 0 || idx == len(reference)-1 {
return parts
}
sheetsRef, cellRef := reference[:idx], reference[idx+1:]
firstSheet, rest, ok := readSheetToken(sheetsRef)
if !ok || len(rest) < 2 || rest[0] != ':' {
return parts
}
lastSheet, rest, ok := readSheetToken(rest[1:])
if !ok || rest != "" {
return parts
}
if firstSheet == "" || lastSheet == "" {
return parts
}
return []string{firstSheet, lastSheet, cellRef}
}

// readSheetToken reads an unquoted sheet name from the front of s and
// returns (name, remaining, ok). A `:` ends the name; a `!` or `'` is
// rejected as malformed for the 3D shape parser.
func readSheetToken(s string) (name, rest string, ok bool) {
if s == "" {
return "", "", false
}
for i, r := range s {
if r == ':' {
return s[:i], s[i:], true
}
if r == '!' || r == '\'' {
return "", "", false
}
}
return s, "", true
}

// expand3DSheetRange returns the workbook-order slice of sheet names
// from first sheet to last sheet inclusive.
func (f *File) expand3DSheetRange(sheet1, sheet2 string) ([]string, error) {
firstSheetIdx, err := f.GetSheetIndex(sheet1)
if err != nil || firstSheetIdx < 0 {
return nil, ErrSheetNotExist{sheet1}
}
lastSheetIdx, err := f.GetSheetIndex(sheet2)
if err != nil || lastSheetIdx < 0 {
return nil, ErrSheetNotExist{sheet2}
}
if firstSheetIdx > lastSheetIdx {
firstSheetIdx, lastSheetIdx = lastSheetIdx, firstSheetIdx
}
return f.GetSheetList()[firstSheetIdx : lastSheetIdx+1], err
}

// prepareValueRange prepare value range.
func prepareValueRange(cr cellRange, valueRange []int) {
if cr.From.Row < valueRange[0] || valueRange[0] == 0 {
Expand Down
63 changes: 63 additions & 0 deletions calc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7122,3 +7122,66 @@ func TestCalcImplicitIntersect(t *testing.T) {
newMatrixFormulaArg([][]formulaArg{{newNumberFormulaArg(1)}})).Type,
)
}

func TestCalc3DRef(t *testing.T) {
prepareCalcData := func() *File {
f := NewFile()
_, err := f.NewSheet("Sheet 2")
assert.NoError(t, err)
_, err = f.NewSheet("Sheet3")
assert.NoError(t, err)
_, err = f.NewSheet("Sheet4")
assert.NoError(t, err)
for i, sheet := range []string{"Sheet1", "Sheet 2", "Sheet3", "Sheet4"} {
base := int64(i + 1)
assert.NoError(t, f.SetCellValue(sheet, "A1", base*10))
assert.NoError(t, f.SetCellValue(sheet, "A2", base))
assert.NoError(t, f.SetCellValue(sheet, "B1", base*100))
}
return f
}
formulaList := map[string]string{
"SUM(Sheet1:Sheet4!A1)": "100",
"SUM(Sheet1:Sheet4!$A$1)": "100",
"SUM(Sheet1:Sheet3!A1)": "60",
"SUM('Sheet 2:Sheet4'!A1)": "90",
"SUM(Sheet1:Sheet1!A1)": "10",
"SUM(Sheet1:Sheet4!A1:A2)": "110",
"SUM(Sheet1:Sheet4!A1:B1)": "1100",
"AVERAGE(Sheet1:Sheet4!A1)": "25",
"MIN(Sheet1:Sheet4!A1)": "10",
"MAX(Sheet1:Sheet4!A1)": "40",
"COUNT(Sheet1:Sheet4!A1)": "4",
"COUNTA(Sheet1:Sheet4!A1)": "4",
"PRODUCT(Sheet1:Sheet4!A1)": "240000",
"SUM(Sheet4:Sheet1!A1)": "100",
}
for formula, expected := range formulaList {
f := prepareCalcData()
defer func() {
assert.NoError(t, f.Close())
}()
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
result, err := f.CalcCellValue("Sheet1", "C1")
assert.NoError(t, err)
assert.Equal(t, expected, result, formula)
}
calcError := map[string][]string{
"SUM(Sheet1:SheetN!A1)": {"#REF!", "sheet SheetN does not exist"},
"SUM(SheetN:Sheet3!A1)": {"#REF!", "sheet SheetN does not exist"},
"SUM(Sheet1:Sheet3!A)": {"#NAME?", newCoordinatesToCellNameError(1, -1).Error()},
}
for formula, expected := range calcError {
f := prepareCalcData()
defer func() {
assert.NoError(t, f.Close())
}()
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
result, err := f.CalcCellValue("Sheet1", "C1")
assert.EqualError(t, err, expected[1], formula)
assert.Equal(t, expected[0], result, formula)
}
assert.Empty(t, split3DReference("Sheet1:Sheet2:Sheet3!A1"))
assert.Empty(t, split3DReference(":Sheet1!A1"))
assert.Empty(t, split3DReference("!A1"))
}
Loading