Skip to content

Commit

Permalink
ref #65, support _xlfn.ANCHORARRAY formula function (#1784)
Browse files Browse the repository at this point in the history
- Initial formula array calculation support
- Update unit test and documentation
  • Loading branch information
3zmx authored Jan 18, 2024
1 parent 7926565 commit 50e23df
Show file tree
Hide file tree
Showing 62 changed files with 504 additions and 115 deletions.
154 changes: 129 additions & 25 deletions adjust.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later.
// data. This library needs Go version 1.18 or later.

package excelize

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

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

// adjustFormula provides a function to adjust formula reference and shared
// formula reference.
func (f *File) adjustFormula(sheet, sheetN string, formula *xlsxF, dir adjustDirection, num, offset int, si bool) error {
if formula == nil {
func (f *File) adjustFormula(sheet, sheetN string, cell *xlsxC, dir adjustDirection, num, offset int, si bool) error {
var err error
if cell.f != "" {
if cell.f, err = f.adjustFormulaRef(sheet, sheetN, cell.f, false, dir, num, offset); err != nil {
return err
}
}
if cell.F == nil {
return nil
}
var err error
if formula.Ref != "" && sheet == sheetN {
if formula.Ref, _, err = f.adjustCellRef(formula.Ref, dir, num, offset); err != nil {
if cell.F.Ref != "" && sheet == sheetN {
if cell.F.Ref, _, err = f.adjustCellRef(cell.F.Ref, dir, num, offset); err != nil {
return err
}
if si && formula.Si != nil {
formula.Si = intPtr(*formula.Si + 1)
if si && cell.F.Si != nil {
cell.F.Si = intPtr(*cell.F.Si + 1)
}
}
if formula.Content != "" {
if formula.Content, err = f.adjustFormulaRef(sheet, sheetN, formula.Content, false, dir, num, offset); err != nil {
if cell.F.Content != "" {
if cell.F.Content, err = f.adjustFormulaRef(sheet, sheetN, cell.F.Content, false, dir, num, offset); err != nil {
return err
}
}
return nil
}

// isFunctionStop provides a function to check if token is a function stop.
func isFunctionStop(token efp.Token) bool {
return token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStop
}

// isFunctionStart provides a function to check if token is a function start.
func isFunctionStart(token efp.Token) bool {
return token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStart
}

// escapeSheetName enclose sheet name in single quotation marks if the giving
// worksheet name includes spaces or non-alphabetical characters.
func escapeSheetName(name string) string {
Expand Down Expand Up @@ -442,11 +437,11 @@ func (f *File) adjustFormulaRef(sheet, sheetN, formula string, keepRelative bool
val += operand
continue
}
if isFunctionStart(token) {
if isFunctionStartToken(token) {
val += token.TValue + string(efp.ParenOpen)
continue
}
if isFunctionStop(token) {
if isFunctionStopToken(token) {
val += token.TValue + string(efp.ParenClose)
continue
}
Expand All @@ -459,6 +454,115 @@ func (f *File) adjustFormulaRef(sheet, sheetN, formula string, keepRelative bool
return val, nil
}

// arrayFormulaOperandToken defines meta fields for transforming the array
// formula to the normal formula.
type arrayFormulaOperandToken struct {
operandTokenIndex, topLeftCol, topLeftRow, bottomRightCol, bottomRightRow int
sheetName, sourceCellRef, targetCellRef string
}

// setCoordinates convert each corner cell reference in the array formula cell
// range to the coordinate number.
func (af *arrayFormulaOperandToken) setCoordinates() error {
for i, ref := range strings.Split(af.sourceCellRef, ":") {
cellRef, col, row, err := parseRef(ref)
if err != nil {
return err
}
var c, r int
if col {
if cellRef.Row = TotalRows; i == 1 {
cellRef.Row = 1
}
}
if row {
if cellRef.Col = MaxColumns; i == 1 {
cellRef.Col = 1
}
}
if c, r = cellRef.Col, cellRef.Row; cellRef.Sheet != "" {
af.sheetName = cellRef.Sheet + "!"
}
if af.topLeftCol == 0 || c < af.topLeftCol {
af.topLeftCol = c
}
if af.topLeftRow == 0 || r < af.topLeftRow {
af.topLeftRow = r
}
if c > af.bottomRightCol {
af.bottomRightCol = c
}
if r > af.bottomRightRow {
af.bottomRightRow = r
}
}
return nil
}

// transformArrayFormula transforms an array formula to the normal formula by
// giving a formula tokens list and formula operand tokens list.
func transformArrayFormula(tokens []efp.Token, afs []arrayFormulaOperandToken) string {
var val string
for i, token := range tokens {
var skip bool
for _, af := range afs {
if af.operandTokenIndex == i {
val += af.sheetName + af.targetCellRef
skip = true
break
}
}
if skip {
continue
}
if isFunctionStartToken(token) {
val += token.TValue + string(efp.ParenOpen)
continue
}
if isFunctionStopToken(token) {
val += token.TValue + string(efp.ParenClose)
continue
}
if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeText {
val += string(efp.QuoteDouble) + strings.ReplaceAll(token.TValue, "\"", "\"\"") + string(efp.QuoteDouble)
continue
}
val += token.TValue
}
return val
}

// getArrayFormulaTokens returns parsed formula token and operand related token
// list for in array formula.
func getArrayFormulaTokens(sheet, formula string, definedNames []DefinedName) ([]efp.Token, []arrayFormulaOperandToken, error) {
var (
ps = efp.ExcelParser()
tokens = ps.Parse(formula)
arrayFormulaOperandTokens []arrayFormulaOperandToken
)
for i, token := range tokens {
if token.TSubType == efp.TokenSubTypeRange && token.TType == efp.TokenTypeOperand {
tokenVal := token.TValue
for _, definedName := range definedNames {
if (definedName.Scope == "Workbook" || definedName.Scope == sheet) && definedName.Name == tokenVal {
tokenVal = definedName.RefersTo
}
}
if len(strings.Split(tokenVal, ":")) > 1 {
arrayFormulaOperandToken := arrayFormulaOperandToken{
operandTokenIndex: i,
sourceCellRef: tokenVal,
}
if err := arrayFormulaOperandToken.setCoordinates(); err != nil {
return tokens, arrayFormulaOperandTokens, err
}
arrayFormulaOperandTokens = append(arrayFormulaOperandTokens, arrayFormulaOperandToken)
}
}
}
return tokens, arrayFormulaOperandTokens, nil
}

// adjustHyperlinks provides a function to update hyperlinks when inserting or
// deleting rows or columns.
func (f *File) adjustHyperlinks(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset int) {
Expand Down
26 changes: 23 additions & 3 deletions adjust_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -557,9 +557,9 @@ func TestAdjustFormula(t *testing.T) {
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAdjustFormula.xlsx")))
assert.NoError(t, f.Close())

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

_, err := f.adjustFormulaRef("Sheet1", "Sheet1", "XFE1", false, columns, 0, 1)
assert.Equal(t, ErrColumnNumber, err)
Expand Down Expand Up @@ -940,6 +940,26 @@ func TestAdjustFormula(t *testing.T) {
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
})
t.Run("for_array_formula_cell", func(t *testing.T) {
f := NewFile()
assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]int{1, 2}))
assert.NoError(t, f.SetSheetRow("Sheet1", "A2", &[]int{3, 4}))
formulaType, ref := STCellFormulaTypeArray, "C1:C2"
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "A1:A2*B1:B2", FormulaOpts{Ref: &ref, Type: &formulaType}))
assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
result, err := f.CalcCellValue("Sheet1", "D2")
assert.NoError(t, err)
assert.Equal(t, "2", result)
result, err = f.CalcCellValue("Sheet1", "D3")
assert.NoError(t, err)
assert.Equal(t, "12", result)

// Test adjust array formula with invalid range reference
formulaType, ref = STCellFormulaTypeArray, "E1:E2"
assert.NoError(t, f.SetCellFormula("Sheet1", "E1", "XFD1:XFD1", FormulaOpts{Ref: &ref, Type: &formulaType}))
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")
})
}

func TestAdjustVolatileDeps(t *testing.T) {
Expand Down
54 changes: 48 additions & 6 deletions calc.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later.
// data. This library needs Go version 1.18 or later.

package excelize

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

// parseRef parse reference for a cell, column name or row number.
func (f *File) parseRef(ref string) (cellRef, bool, bool, error) {
func parseRef(ref string) (cellRef, bool, bool, error) {
var (
err, colErr, rowErr error
cr cellRef
Expand Down Expand Up @@ -1526,7 +1526,7 @@ func (f *File) parseReference(ctx *calcContext, sheet, reference string) (formul
if len(ranges) > 1 {
var cr cellRange
for i, ref := range ranges {
cellRef, col, row, err := f.parseRef(ref)
cellRef, col, row, err := parseRef(ref)
if err != nil {
return newErrorFormulaArg(formulaErrorNAME, "invalid reference"), errors.New("invalid reference")
}
Expand All @@ -1550,7 +1550,7 @@ func (f *File) parseReference(ctx *calcContext, sheet, reference string) (formul
cellRanges.PushBack(cr)
return f.rangeResolver(ctx, cellRefs, cellRanges)
}
cellRef, _, _, err := f.parseRef(reference)
cellRef, _, _, err := parseRef(reference)
if err != nil {
return newErrorFormulaArg(formulaErrorNAME, "invalid reference"), errors.New("invalid reference")
}
Expand Down Expand Up @@ -1601,7 +1601,7 @@ func (f *File) cellResolver(ctx *calcContext, sheet, cell string) (formulaArg, e
err error
)
ref := fmt.Sprintf("%s!%s", sheet, cell)
if formula, _ := f.GetCellFormula(sheet, cell); len(formula) != 0 {
if formula, _ := f.getCellFormula(sheet, cell, true); len(formula) != 0 {
ctx.mu.Lock()
if ctx.entry != ref {
if ctx.iterations[ref] <= f.options.MaxCalcIterations {
Expand Down Expand Up @@ -14505,6 +14505,48 @@ func (fn *formulaFuncs) ADDRESS(argsList *list.List) formulaArg {
return newStringFormulaArg(fmt.Sprintf("%s%s", sheetText, addr))
}

// ANCHORARRAY function returns the entire spilled range for the dynamic array
// in cell. The syntax of the function is:
//
// ANCHORARRAY(cell)
func (fn *formulaFuncs) ANCHORARRAY(argsList *list.List) formulaArg {
if argsList.Len() != 1 {
return newErrorFormulaArg(formulaErrorVALUE, "ANCHORARRAY requires 1 numeric argument")
}
ws, err := fn.f.workSheetReader(fn.sheet)
if err != nil {
return newErrorFormulaArg(formulaErrorVALUE, err.Error())
}
ref := argsList.Front().Value.(formulaArg).cellRefs.Front().Value.(cellRef)
cell := ws.SheetData.Row[ref.Row-1].C[ref.Col-1]
if cell.F == nil {
return newEmptyFormulaArg()
}
coordinates, err := rangeRefToCoordinates(cell.F.Ref)
if err != nil {
return newErrorFormulaArg(formulaErrorVALUE, err.Error())
}
_ = sortCoordinates(coordinates)
var mtx [][]formulaArg
for c := coordinates[0]; c <= coordinates[2]; c++ {
var row []formulaArg
for r := coordinates[1]; r <= coordinates[3]; r++ {
cellName, _ := CoordinatesToCellName(c, r)
result, err := fn.f.CalcCellValue(ref.Sheet, cellName, Options{RawCellValue: true})
if err != nil {
return newErrorFormulaArg(formulaErrorVALUE, err.Error())
}
arg := newStringFormulaArg(result)
if num := arg.ToNumber(); num.Type == ArgNumber {
arg = num
}
row = append(row, arg)
}
mtx = append(mtx, row)
}
return newMatrixFormulaArg(mtx)
}

// CHOOSE function returns a value from an array, that corresponds to a
// supplied index number (position). The syntax of the function is:
//
Expand Down
Loading

0 comments on commit 50e23df

Please sign in to comment.