Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial formula array calculation support #1784

Merged
merged 1 commit into from
Jan 18, 2024
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
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
Loading