Skip to content

Commit f4a068b

Browse files
authored
This closes #2303, support 3D references across sheet ranges (#2310)
- Update unit tests
1 parent 7240c79 commit f4a068b

2 files changed

Lines changed: 149 additions & 0 deletions

File tree

calc.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1645,6 +1645,9 @@ func (cr *cellRange) prepareCellRange(col, row bool, cellRef cellRef) error {
16451645
// characters and default sheet name.
16461646
func (f *File) parseReference(ctx *calcContext, sheet, reference string) (formulaArg, error) {
16471647
reference = strings.ReplaceAll(reference, "$", "")
1648+
if parts := split3DReference(reference); len(parts) == 3 {
1649+
return f.parse3DReference(ctx, parts)
1650+
}
16481651
ranges, cellRanges, cellRefs := strings.Split(reference, ":"), list.New(), list.New()
16491652
if len(ranges) > 1 {
16501653
var cr cellRange
@@ -1684,6 +1687,89 @@ func (f *File) parseReference(ctx *calcContext, sheet, reference string) (formul
16841687
return f.rangeResolver(ctx, cellRefs, cellRanges)
16851688
}
16861689

1690+
// parse3DReference detects a 3D reference from a range reference and expands it
1691+
// across the workbook-order sheet range.
1692+
func (f *File) parse3DReference(ctx *calcContext, parts []string) (formulaArg, error) {
1693+
firstSheet, lastSheet, cellRef := parts[0], parts[1], parts[2]
1694+
sheets, err := f.expand3DSheetRange(firstSheet, lastSheet)
1695+
if err != nil {
1696+
return newErrorFormulaArg(formulaErrorREF, formulaErrorREF), err
1697+
}
1698+
var matrix [][]formulaArg
1699+
for _, sheet := range sheets {
1700+
result, err := f.parseReference(ctx, sheet, cellRef)
1701+
if err != nil {
1702+
return newErrorFormulaArg(formulaErrorNAME, formulaErrorNAME), err
1703+
}
1704+
switch result.Type {
1705+
case ArgMatrix:
1706+
matrix = append(matrix, result.Matrix...)
1707+
default:
1708+
matrix = append(matrix, []formulaArg{result})
1709+
}
1710+
}
1711+
return newMatrixFormulaArg(matrix), err
1712+
}
1713+
1714+
// split3DReference parses a reference string for a 3D reference of the form
1715+
// formula structure in FirstSheet:LastSheet!CellReference and returns the three
1716+
// components if valid, or an empty slice if not.
1717+
func split3DReference(reference string) []string {
1718+
var parts []string
1719+
idx := strings.Index(reference, "!")
1720+
if idx < 0 || idx == len(reference)-1 {
1721+
return parts
1722+
}
1723+
sheetsRef, cellRef := reference[:idx], reference[idx+1:]
1724+
firstSheet, rest, ok := readSheetToken(sheetsRef)
1725+
if !ok || len(rest) < 2 || rest[0] != ':' {
1726+
return parts
1727+
}
1728+
lastSheet, rest, ok := readSheetToken(rest[1:])
1729+
if !ok || rest != "" {
1730+
return parts
1731+
}
1732+
if firstSheet == "" || lastSheet == "" {
1733+
return parts
1734+
}
1735+
return []string{firstSheet, lastSheet, cellRef}
1736+
}
1737+
1738+
// readSheetToken reads an unquoted sheet name from the front of s and
1739+
// returns (name, remaining, ok). A `:` ends the name; a `!` or `'` is
1740+
// rejected as malformed for the 3D shape parser.
1741+
func readSheetToken(s string) (name, rest string, ok bool) {
1742+
if s == "" {
1743+
return "", "", false
1744+
}
1745+
for i, r := range s {
1746+
if r == ':' {
1747+
return s[:i], s[i:], true
1748+
}
1749+
if r == '!' || r == '\'' {
1750+
return "", "", false
1751+
}
1752+
}
1753+
return s, "", true
1754+
}
1755+
1756+
// expand3DSheetRange returns the workbook-order slice of sheet names
1757+
// from first sheet to last sheet inclusive.
1758+
func (f *File) expand3DSheetRange(sheet1, sheet2 string) ([]string, error) {
1759+
firstSheetIdx, err := f.GetSheetIndex(sheet1)
1760+
if err != nil || firstSheetIdx < 0 {
1761+
return nil, ErrSheetNotExist{sheet1}
1762+
}
1763+
lastSheetIdx, err := f.GetSheetIndex(sheet2)
1764+
if err != nil || lastSheetIdx < 0 {
1765+
return nil, ErrSheetNotExist{sheet2}
1766+
}
1767+
if firstSheetIdx > lastSheetIdx {
1768+
firstSheetIdx, lastSheetIdx = lastSheetIdx, firstSheetIdx
1769+
}
1770+
return f.GetSheetList()[firstSheetIdx : lastSheetIdx+1], err
1771+
}
1772+
16871773
// prepareValueRange prepare value range.
16881774
func prepareValueRange(cr cellRange, valueRange []int) {
16891775
if cr.From.Row < valueRange[0] || valueRange[0] == 0 {

calc_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7122,3 +7122,66 @@ func TestCalcImplicitIntersect(t *testing.T) {
71227122
newMatrixFormulaArg([][]formulaArg{{newNumberFormulaArg(1)}})).Type,
71237123
)
71247124
}
7125+
7126+
func TestCalc3DRef(t *testing.T) {
7127+
prepareCalcData := func() *File {
7128+
f := NewFile()
7129+
_, err := f.NewSheet("Sheet 2")
7130+
assert.NoError(t, err)
7131+
_, err = f.NewSheet("Sheet3")
7132+
assert.NoError(t, err)
7133+
_, err = f.NewSheet("Sheet4")
7134+
assert.NoError(t, err)
7135+
for i, sheet := range []string{"Sheet1", "Sheet 2", "Sheet3", "Sheet4"} {
7136+
base := int64(i + 1)
7137+
assert.NoError(t, f.SetCellValue(sheet, "A1", base*10))
7138+
assert.NoError(t, f.SetCellValue(sheet, "A2", base))
7139+
assert.NoError(t, f.SetCellValue(sheet, "B1", base*100))
7140+
}
7141+
return f
7142+
}
7143+
formulaList := map[string]string{
7144+
"SUM(Sheet1:Sheet4!A1)": "100",
7145+
"SUM(Sheet1:Sheet4!$A$1)": "100",
7146+
"SUM(Sheet1:Sheet3!A1)": "60",
7147+
"SUM('Sheet 2:Sheet4'!A1)": "90",
7148+
"SUM(Sheet1:Sheet1!A1)": "10",
7149+
"SUM(Sheet1:Sheet4!A1:A2)": "110",
7150+
"SUM(Sheet1:Sheet4!A1:B1)": "1100",
7151+
"AVERAGE(Sheet1:Sheet4!A1)": "25",
7152+
"MIN(Sheet1:Sheet4!A1)": "10",
7153+
"MAX(Sheet1:Sheet4!A1)": "40",
7154+
"COUNT(Sheet1:Sheet4!A1)": "4",
7155+
"COUNTA(Sheet1:Sheet4!A1)": "4",
7156+
"PRODUCT(Sheet1:Sheet4!A1)": "240000",
7157+
"SUM(Sheet4:Sheet1!A1)": "100",
7158+
}
7159+
for formula, expected := range formulaList {
7160+
f := prepareCalcData()
7161+
defer func() {
7162+
assert.NoError(t, f.Close())
7163+
}()
7164+
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
7165+
result, err := f.CalcCellValue("Sheet1", "C1")
7166+
assert.NoError(t, err)
7167+
assert.Equal(t, expected, result, formula)
7168+
}
7169+
calcError := map[string][]string{
7170+
"SUM(Sheet1:SheetN!A1)": {"#REF!", "sheet SheetN does not exist"},
7171+
"SUM(SheetN:Sheet3!A1)": {"#REF!", "sheet SheetN does not exist"},
7172+
"SUM(Sheet1:Sheet3!A)": {"#NAME?", newCoordinatesToCellNameError(1, -1).Error()},
7173+
}
7174+
for formula, expected := range calcError {
7175+
f := prepareCalcData()
7176+
defer func() {
7177+
assert.NoError(t, f.Close())
7178+
}()
7179+
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
7180+
result, err := f.CalcCellValue("Sheet1", "C1")
7181+
assert.EqualError(t, err, expected[1], formula)
7182+
assert.Equal(t, expected[0], result, formula)
7183+
}
7184+
assert.Empty(t, split3DReference("Sheet1:Sheet2:Sheet3!A1"))
7185+
assert.Empty(t, split3DReference(":Sheet1!A1"))
7186+
assert.Empty(t, split3DReference("!A1"))
7187+
}

0 commit comments

Comments
 (0)