Skip to content

Commit 50e23df

Browse files
authored
ref #65, support _xlfn.ANCHORARRAY formula function (#1784)
- Initial formula array calculation support - Update unit test and documentation
1 parent 7926565 commit 50e23df

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+504
-115
lines changed

adjust.go

Lines changed: 129 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
88
// Supports complex components by high compatibility, and provided streaming
99
// API for generating or reading data from a worksheet with huge amounts of
10-
// data. This library needs Go version 1.16 or later.
10+
// data. This library needs Go version 1.18 or later.
1111

1212
package excelize
1313

@@ -165,7 +165,7 @@ func (f *File) adjustColDimensions(sheet string, ws *xlsxWorksheet, col, offset
165165
worksheet.SheetData.Row[rowIdx].C[colIdx].R, _ = CoordinatesToCellName(newCol, cellRow)
166166
}
167167
}
168-
if err := f.adjustFormula(sheet, sheetN, worksheet.SheetData.Row[rowIdx].C[colIdx].F, columns, col, offset, false); err != nil {
168+
if err := f.adjustFormula(sheet, sheetN, &worksheet.SheetData.Row[rowIdx].C[colIdx], columns, col, offset, false); err != nil {
169169
return err
170170
}
171171
}
@@ -228,8 +228,8 @@ func (r *xlsxRow) adjustSingleRowDimensions(offset int) {
228228

229229
// adjustSingleRowFormulas provides a function to adjust single row formulas.
230230
func (f *File) adjustSingleRowFormulas(sheet, sheetN string, r *xlsxRow, num, offset int, si bool) error {
231-
for _, col := range r.C {
232-
if err := f.adjustFormula(sheet, sheetN, col.F, rows, num, offset, si); err != nil {
231+
for i := 0; i < len(r.C); i++ {
232+
if err := f.adjustFormula(sheet, sheetN, &r.C[i], rows, num, offset, si); err != nil {
233233
return err
234234
}
235235
}
@@ -273,37 +273,32 @@ func (f *File) adjustCellRef(ref string, dir adjustDirection, num, offset int) (
273273

274274
// adjustFormula provides a function to adjust formula reference and shared
275275
// formula reference.
276-
func (f *File) adjustFormula(sheet, sheetN string, formula *xlsxF, dir adjustDirection, num, offset int, si bool) error {
277-
if formula == nil {
276+
func (f *File) adjustFormula(sheet, sheetN string, cell *xlsxC, dir adjustDirection, num, offset int, si bool) error {
277+
var err error
278+
if cell.f != "" {
279+
if cell.f, err = f.adjustFormulaRef(sheet, sheetN, cell.f, false, dir, num, offset); err != nil {
280+
return err
281+
}
282+
}
283+
if cell.F == nil {
278284
return nil
279285
}
280-
var err error
281-
if formula.Ref != "" && sheet == sheetN {
282-
if formula.Ref, _, err = f.adjustCellRef(formula.Ref, dir, num, offset); err != nil {
286+
if cell.F.Ref != "" && sheet == sheetN {
287+
if cell.F.Ref, _, err = f.adjustCellRef(cell.F.Ref, dir, num, offset); err != nil {
283288
return err
284289
}
285-
if si && formula.Si != nil {
286-
formula.Si = intPtr(*formula.Si + 1)
290+
if si && cell.F.Si != nil {
291+
cell.F.Si = intPtr(*cell.F.Si + 1)
287292
}
288293
}
289-
if formula.Content != "" {
290-
if formula.Content, err = f.adjustFormulaRef(sheet, sheetN, formula.Content, false, dir, num, offset); err != nil {
294+
if cell.F.Content != "" {
295+
if cell.F.Content, err = f.adjustFormulaRef(sheet, sheetN, cell.F.Content, false, dir, num, offset); err != nil {
291296
return err
292297
}
293298
}
294299
return nil
295300
}
296301

297-
// isFunctionStop provides a function to check if token is a function stop.
298-
func isFunctionStop(token efp.Token) bool {
299-
return token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStop
300-
}
301-
302-
// isFunctionStart provides a function to check if token is a function start.
303-
func isFunctionStart(token efp.Token) bool {
304-
return token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStart
305-
}
306-
307302
// escapeSheetName enclose sheet name in single quotation marks if the giving
308303
// worksheet name includes spaces or non-alphabetical characters.
309304
func escapeSheetName(name string) string {
@@ -442,11 +437,11 @@ func (f *File) adjustFormulaRef(sheet, sheetN, formula string, keepRelative bool
442437
val += operand
443438
continue
444439
}
445-
if isFunctionStart(token) {
440+
if isFunctionStartToken(token) {
446441
val += token.TValue + string(efp.ParenOpen)
447442
continue
448443
}
449-
if isFunctionStop(token) {
444+
if isFunctionStopToken(token) {
450445
val += token.TValue + string(efp.ParenClose)
451446
continue
452447
}
@@ -459,6 +454,115 @@ func (f *File) adjustFormulaRef(sheet, sheetN, formula string, keepRelative bool
459454
return val, nil
460455
}
461456

457+
// arrayFormulaOperandToken defines meta fields for transforming the array
458+
// formula to the normal formula.
459+
type arrayFormulaOperandToken struct {
460+
operandTokenIndex, topLeftCol, topLeftRow, bottomRightCol, bottomRightRow int
461+
sheetName, sourceCellRef, targetCellRef string
462+
}
463+
464+
// setCoordinates convert each corner cell reference in the array formula cell
465+
// range to the coordinate number.
466+
func (af *arrayFormulaOperandToken) setCoordinates() error {
467+
for i, ref := range strings.Split(af.sourceCellRef, ":") {
468+
cellRef, col, row, err := parseRef(ref)
469+
if err != nil {
470+
return err
471+
}
472+
var c, r int
473+
if col {
474+
if cellRef.Row = TotalRows; i == 1 {
475+
cellRef.Row = 1
476+
}
477+
}
478+
if row {
479+
if cellRef.Col = MaxColumns; i == 1 {
480+
cellRef.Col = 1
481+
}
482+
}
483+
if c, r = cellRef.Col, cellRef.Row; cellRef.Sheet != "" {
484+
af.sheetName = cellRef.Sheet + "!"
485+
}
486+
if af.topLeftCol == 0 || c < af.topLeftCol {
487+
af.topLeftCol = c
488+
}
489+
if af.topLeftRow == 0 || r < af.topLeftRow {
490+
af.topLeftRow = r
491+
}
492+
if c > af.bottomRightCol {
493+
af.bottomRightCol = c
494+
}
495+
if r > af.bottomRightRow {
496+
af.bottomRightRow = r
497+
}
498+
}
499+
return nil
500+
}
501+
502+
// transformArrayFormula transforms an array formula to the normal formula by
503+
// giving a formula tokens list and formula operand tokens list.
504+
func transformArrayFormula(tokens []efp.Token, afs []arrayFormulaOperandToken) string {
505+
var val string
506+
for i, token := range tokens {
507+
var skip bool
508+
for _, af := range afs {
509+
if af.operandTokenIndex == i {
510+
val += af.sheetName + af.targetCellRef
511+
skip = true
512+
break
513+
}
514+
}
515+
if skip {
516+
continue
517+
}
518+
if isFunctionStartToken(token) {
519+
val += token.TValue + string(efp.ParenOpen)
520+
continue
521+
}
522+
if isFunctionStopToken(token) {
523+
val += token.TValue + string(efp.ParenClose)
524+
continue
525+
}
526+
if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeText {
527+
val += string(efp.QuoteDouble) + strings.ReplaceAll(token.TValue, "\"", "\"\"") + string(efp.QuoteDouble)
528+
continue
529+
}
530+
val += token.TValue
531+
}
532+
return val
533+
}
534+
535+
// getArrayFormulaTokens returns parsed formula token and operand related token
536+
// list for in array formula.
537+
func getArrayFormulaTokens(sheet, formula string, definedNames []DefinedName) ([]efp.Token, []arrayFormulaOperandToken, error) {
538+
var (
539+
ps = efp.ExcelParser()
540+
tokens = ps.Parse(formula)
541+
arrayFormulaOperandTokens []arrayFormulaOperandToken
542+
)
543+
for i, token := range tokens {
544+
if token.TSubType == efp.TokenSubTypeRange && token.TType == efp.TokenTypeOperand {
545+
tokenVal := token.TValue
546+
for _, definedName := range definedNames {
547+
if (definedName.Scope == "Workbook" || definedName.Scope == sheet) && definedName.Name == tokenVal {
548+
tokenVal = definedName.RefersTo
549+
}
550+
}
551+
if len(strings.Split(tokenVal, ":")) > 1 {
552+
arrayFormulaOperandToken := arrayFormulaOperandToken{
553+
operandTokenIndex: i,
554+
sourceCellRef: tokenVal,
555+
}
556+
if err := arrayFormulaOperandToken.setCoordinates(); err != nil {
557+
return tokens, arrayFormulaOperandTokens, err
558+
}
559+
arrayFormulaOperandTokens = append(arrayFormulaOperandTokens, arrayFormulaOperandToken)
560+
}
561+
}
562+
}
563+
return tokens, arrayFormulaOperandTokens, nil
564+
}
565+
462566
// adjustHyperlinks provides a function to update hyperlinks when inserting or
463567
// deleting rows or columns.
464568
func (f *File) adjustHyperlinks(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset int) {

adjust_test.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -557,9 +557,9 @@ func TestAdjustFormula(t *testing.T) {
557557
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAdjustFormula.xlsx")))
558558
assert.NoError(t, f.Close())
559559

560-
assert.NoError(t, f.adjustFormula("Sheet1", "Sheet1", nil, rows, 0, 0, false))
561-
assert.Equal(t, newCellNameToCoordinatesError("-", newInvalidCellNameError("-")), f.adjustFormula("Sheet1", "Sheet1", &xlsxF{Ref: "-"}, rows, 0, 0, false))
562-
assert.Equal(t, ErrColumnNumber, f.adjustFormula("Sheet1", "Sheet1", &xlsxF{Ref: "XFD1:XFD1"}, columns, 0, 1, false))
560+
assert.NoError(t, f.adjustFormula("Sheet1", "Sheet1", &xlsxC{}, rows, 0, 0, false))
561+
assert.Equal(t, newCellNameToCoordinatesError("-", newInvalidCellNameError("-")), f.adjustFormula("Sheet1", "Sheet1", &xlsxC{F: &xlsxF{Ref: "-"}}, rows, 0, 0, false))
562+
assert.Equal(t, ErrColumnNumber, f.adjustFormula("Sheet1", "Sheet1", &xlsxC{F: &xlsxF{Ref: "XFD1:XFD1"}}, columns, 0, 1, false))
563563

564564
_, err := f.adjustFormulaRef("Sheet1", "Sheet1", "XFE1", false, columns, 0, 1)
565565
assert.Equal(t, ErrColumnNumber, err)
@@ -940,6 +940,26 @@ func TestAdjustFormula(t *testing.T) {
940940
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
941941
assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
942942
})
943+
t.Run("for_array_formula_cell", func(t *testing.T) {
944+
f := NewFile()
945+
assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]int{1, 2}))
946+
assert.NoError(t, f.SetSheetRow("Sheet1", "A2", &[]int{3, 4}))
947+
formulaType, ref := STCellFormulaTypeArray, "C1:C2"
948+
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "A1:A2*B1:B2", FormulaOpts{Ref: &ref, Type: &formulaType}))
949+
assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
950+
assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
951+
result, err := f.CalcCellValue("Sheet1", "D2")
952+
assert.NoError(t, err)
953+
assert.Equal(t, "2", result)
954+
result, err = f.CalcCellValue("Sheet1", "D3")
955+
assert.NoError(t, err)
956+
assert.Equal(t, "12", result)
957+
958+
// Test adjust array formula with invalid range reference
959+
formulaType, ref = STCellFormulaTypeArray, "E1:E2"
960+
assert.NoError(t, f.SetCellFormula("Sheet1", "E1", "XFD1:XFD1", FormulaOpts{Ref: &ref, Type: &formulaType}))
961+
assert.EqualError(t, f.InsertCols("Sheet1", "A", 1), "the column number must be greater than or equal to 1 and less than or equal to 16384")
962+
})
943963
}
944964

945965
func TestAdjustVolatileDeps(t *testing.T) {

calc.go

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
88
// Supports complex components by high compatibility, and provided streaming
99
// API for generating or reading data from a worksheet with huge amounts of
10-
// data. This library needs Go version 1.16 or later.
10+
// data. This library needs Go version 1.18 or later.
1111

1212
package excelize
1313

@@ -838,7 +838,7 @@ func (f *File) CalcCellValue(sheet, cell string, opts ...Options) (result string
838838
// reference.
839839
func (f *File) calcCellValue(ctx *calcContext, sheet, cell string) (result formulaArg, err error) {
840840
var formula string
841-
if formula, err = f.GetCellFormula(sheet, cell); err != nil {
841+
if formula, err = f.getCellFormula(sheet, cell, true); err != nil {
842842
return
843843
}
844844
ps := efp.ExcelParser()
@@ -1467,7 +1467,7 @@ func (f *File) parseToken(ctx *calcContext, sheet string, token efp.Token, opdSt
14671467
}
14681468

14691469
// parseRef parse reference for a cell, column name or row number.
1470-
func (f *File) parseRef(ref string) (cellRef, bool, bool, error) {
1470+
func parseRef(ref string) (cellRef, bool, bool, error) {
14711471
var (
14721472
err, colErr, rowErr error
14731473
cr cellRef
@@ -1526,7 +1526,7 @@ func (f *File) parseReference(ctx *calcContext, sheet, reference string) (formul
15261526
if len(ranges) > 1 {
15271527
var cr cellRange
15281528
for i, ref := range ranges {
1529-
cellRef, col, row, err := f.parseRef(ref)
1529+
cellRef, col, row, err := parseRef(ref)
15301530
if err != nil {
15311531
return newErrorFormulaArg(formulaErrorNAME, "invalid reference"), errors.New("invalid reference")
15321532
}
@@ -1550,7 +1550,7 @@ func (f *File) parseReference(ctx *calcContext, sheet, reference string) (formul
15501550
cellRanges.PushBack(cr)
15511551
return f.rangeResolver(ctx, cellRefs, cellRanges)
15521552
}
1553-
cellRef, _, _, err := f.parseRef(reference)
1553+
cellRef, _, _, err := parseRef(reference)
15541554
if err != nil {
15551555
return newErrorFormulaArg(formulaErrorNAME, "invalid reference"), errors.New("invalid reference")
15561556
}
@@ -1601,7 +1601,7 @@ func (f *File) cellResolver(ctx *calcContext, sheet, cell string) (formulaArg, e
16011601
err error
16021602
)
16031603
ref := fmt.Sprintf("%s!%s", sheet, cell)
1604-
if formula, _ := f.GetCellFormula(sheet, cell); len(formula) != 0 {
1604+
if formula, _ := f.getCellFormula(sheet, cell, true); len(formula) != 0 {
16051605
ctx.mu.Lock()
16061606
if ctx.entry != ref {
16071607
if ctx.iterations[ref] <= f.options.MaxCalcIterations {
@@ -14505,6 +14505,48 @@ func (fn *formulaFuncs) ADDRESS(argsList *list.List) formulaArg {
1450514505
return newStringFormulaArg(fmt.Sprintf("%s%s", sheetText, addr))
1450614506
}
1450714507

14508+
// ANCHORARRAY function returns the entire spilled range for the dynamic array
14509+
// in cell. The syntax of the function is:
14510+
//
14511+
// ANCHORARRAY(cell)
14512+
func (fn *formulaFuncs) ANCHORARRAY(argsList *list.List) formulaArg {
14513+
if argsList.Len() != 1 {
14514+
return newErrorFormulaArg(formulaErrorVALUE, "ANCHORARRAY requires 1 numeric argument")
14515+
}
14516+
ws, err := fn.f.workSheetReader(fn.sheet)
14517+
if err != nil {
14518+
return newErrorFormulaArg(formulaErrorVALUE, err.Error())
14519+
}
14520+
ref := argsList.Front().Value.(formulaArg).cellRefs.Front().Value.(cellRef)
14521+
cell := ws.SheetData.Row[ref.Row-1].C[ref.Col-1]
14522+
if cell.F == nil {
14523+
return newEmptyFormulaArg()
14524+
}
14525+
coordinates, err := rangeRefToCoordinates(cell.F.Ref)
14526+
if err != nil {
14527+
return newErrorFormulaArg(formulaErrorVALUE, err.Error())
14528+
}
14529+
_ = sortCoordinates(coordinates)
14530+
var mtx [][]formulaArg
14531+
for c := coordinates[0]; c <= coordinates[2]; c++ {
14532+
var row []formulaArg
14533+
for r := coordinates[1]; r <= coordinates[3]; r++ {
14534+
cellName, _ := CoordinatesToCellName(c, r)
14535+
result, err := fn.f.CalcCellValue(ref.Sheet, cellName, Options{RawCellValue: true})
14536+
if err != nil {
14537+
return newErrorFormulaArg(formulaErrorVALUE, err.Error())
14538+
}
14539+
arg := newStringFormulaArg(result)
14540+
if num := arg.ToNumber(); num.Type == ArgNumber {
14541+
arg = num
14542+
}
14543+
row = append(row, arg)
14544+
}
14545+
mtx = append(mtx, row)
14546+
}
14547+
return newMatrixFormulaArg(mtx)
14548+
}
14549+
1450814550
// CHOOSE function returns a value from an array, that corresponds to a
1450914551
// supplied index number (position). The syntax of the function is:
1451014552
//

0 commit comments

Comments
 (0)