From ddafaca8503bcd2835ba8b99267046357941d05c Mon Sep 17 00:00:00 2001 From: Vyacheslav Zgordan Date: Tue, 11 Feb 2020 22:47:08 +0300 Subject: [PATCH] Spreadsheet: Remove columns feature (Issue #367) (#371) * Issue #376 fix - RemoveColumn * Removing of columns is forbidden when there are formula arrays in the area of removing, except 1-column wide arrays * Modifying named ranges, column ranges when deleting a column * Updating formulas when deleting a column * UpdateAction --- _examples/spreadsheet/remove-column/main.go | 38 +++ .../spreadsheet/remove-column/original.xlsx | Bin 0 -> 7158 bytes spreadsheet/formula/binaryexpr.go | 42 +++ spreadsheet/formula/bool.go | 15 + spreadsheet/formula/cellref.go | 38 +++ spreadsheet/formula/columnref.go | 22 ++ spreadsheet/formula/constarrayexpr.go | 12 + spreadsheet/formula/emptyexpr.go | 12 + spreadsheet/formula/error.go | 12 + spreadsheet/formula/expression.go | 4 + spreadsheet/formula/functioncall.go | 34 +++ spreadsheet/formula/horizontalrange.go | 12 + spreadsheet/formula/namedrangeref.go | 12 + spreadsheet/formula/negate.go | 12 + spreadsheet/formula/number.go | 11 + spreadsheet/formula/prefixexpr.go | 23 +- spreadsheet/formula/prefixhorizontalrange.go | 12 + spreadsheet/formula/prefixrangeexpr.go | 24 +- spreadsheet/formula/prefixverticalrange.go | 22 ++ spreadsheet/formula/range.go | 16 ++ spreadsheet/formula/sheetprefixexpr.go | 12 + spreadsheet/formula/string.go | 16 +- spreadsheet/formula/verticalrange.go | 21 ++ spreadsheet/reference/cellreference.go | 16 ++ spreadsheet/reference/columnreference.go | 80 ++++++ spreadsheet/reference/rangereference.go | 30 ++ spreadsheet/sheet.go | 256 ++++++++++++++++++ spreadsheet/sheet_test.go | 35 +++ spreadsheet/update/update_query.go | 31 +++ 29 files changed, 867 insertions(+), 3 deletions(-) create mode 100644 _examples/spreadsheet/remove-column/main.go create mode 100644 _examples/spreadsheet/remove-column/original.xlsx create mode 100644 spreadsheet/formula/columnref.go create mode 100644 spreadsheet/reference/columnreference.go create mode 100644 spreadsheet/update/update_query.go diff --git a/_examples/spreadsheet/remove-column/main.go b/_examples/spreadsheet/remove-column/main.go new file mode 100644 index 00000000..f3d57892 --- /dev/null +++ b/_examples/spreadsheet/remove-column/main.go @@ -0,0 +1,38 @@ +// Copyright 2017 FoxyUtils ehf. All rights reserved. +package main +// This example demonstrates flattening all formulas from an input Excel file and outputs the flattened values to a new xlsx. + +import ( + "log" + + "github.com/unidoc/unioffice/spreadsheet" +) + +func main() { + ss, err := spreadsheet.Open("original.xlsx") + if err != nil { + log.Fatalf("error opening document: %s", err) + } + + sheet0, err := ss.GetSheet("Cells") + if err != nil { + log.Fatalf("error opening sheet: %s", err) + } + + err = sheet0.RemoveColumn("D") + if err != nil { + log.Fatalf("error removing column: %s", err) + } + + sheet1, err := ss.GetSheet("MergedCells") + if err != nil { + log.Fatalf("error opening sheet: %s", err) + } + + err = sheet1.RemoveColumn("C") + if err != nil { + log.Fatalf("error removing column: %s", err) + } + + ss.SaveToFile("removed.xlsx") +} diff --git a/_examples/spreadsheet/remove-column/original.xlsx b/_examples/spreadsheet/remove-column/original.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..1fd771a98c354b97e082a3dfc661b8032b7189f2 GIT binary patch literal 7158 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 Kyp5@@fBheivv3{& literal 0 HcmV?d00001 diff --git a/spreadsheet/formula/binaryexpr.go b/spreadsheet/formula/binaryexpr.go index f392b637..79e065b5 100644 --- a/spreadsheet/formula/binaryexpr.go +++ b/spreadsheet/formula/binaryexpr.go @@ -10,6 +10,8 @@ package formula import ( "fmt" "math" + + "github.com/unidoc/unioffice/spreadsheet/update" ) // BinOpType is the binary operation operator type @@ -403,3 +405,43 @@ func listValueOp(op BinOpType, lhs []Result, rhs Result) Result { } return MakeListResult(res) } + +// Eval evaluates the binary expression using the context given. +func (b BinaryExpr) String() string { + opStr := "" + switch b.op { + case BinOpTypePlus: + opStr = "+" + case BinOpTypeMinus: + opStr = "-" + case BinOpTypeMult: + opStr = "*" + case BinOpTypeDiv: + opStr = "/" + case BinOpTypeExp: + opStr = "^" + case BinOpTypeLT: + opStr = "<" + case BinOpTypeGT: + opStr = ">" + case BinOpTypeEQ: + opStr = "=" + case BinOpTypeLEQ: + opStr = "<=" + case BinOpTypeGEQ: + opStr = ">=" + case BinOpTypeNE: + opStr = "<>" + case BinOpTypeConcat: + opStr = "&" + } + return b.lhs.String() + opStr + b.rhs.String() +} + +// Update updates references in the BinaryExpr after removing a row/column. +func (b BinaryExpr) Update(q *update.UpdateQuery) Expression { + new := b + new.lhs = b.lhs.Update(q) + new.rhs = b.rhs.Update(q) + return new +} diff --git a/spreadsheet/formula/bool.go b/spreadsheet/formula/bool.go index b650adec..f3c53646 100644 --- a/spreadsheet/formula/bool.go +++ b/spreadsheet/formula/bool.go @@ -11,6 +11,7 @@ import ( "strconv" "github.com/unidoc/unioffice" + "github.com/unidoc/unioffice/spreadsheet/update" ) // Bool is a boolean expression. @@ -36,3 +37,17 @@ func (b Bool) Eval(ctx Context, ev Evaluator) Result { func (b Bool) Reference(ctx Context, ev Evaluator) Reference { return ReferenceInvalid } + +// String returns a string representation for Bool. +func (b Bool) String() string { + if b.b { + return "TRUE" + } else { + return "FALSE" + } +} + +// Update returns the same object as updating sheet references does not affect Bool. +func (b Bool) Update(q *update.UpdateQuery) Expression { + return b +} diff --git a/spreadsheet/formula/cellref.go b/spreadsheet/formula/cellref.go index 13feb6a7..3a69a917 100644 --- a/spreadsheet/formula/cellref.go +++ b/spreadsheet/formula/cellref.go @@ -7,6 +7,11 @@ package formula +import ( + "github.com/unidoc/unioffice/spreadsheet/reference" + "github.com/unidoc/unioffice/spreadsheet/update" +) + // CellRef is a reference to a single cell type CellRef struct { s string @@ -26,3 +31,36 @@ func (c CellRef) Eval(ctx Context, ev Evaluator) Result { func (c CellRef) Reference(ctx Context, ev Evaluator) Reference { return Reference{Type: ReferenceTypeCell, Value: c.s} } + +// String returns a string representation of CellRef. +func (c CellRef) String() string { + return c.s +} + +// Update makes a reference to point to one of the neighboring cells after removing a row/column with respect to the update type. +func (c CellRef) Update(q *update.UpdateQuery) Expression { + if q.UpdateCurrentSheet { + c.s = updateRefStr(c.s, q) + } + return c +} + +// updateRefStr gets reference string representation like C1, parses it and makes a string representation of a new reference with respect to the update type (e.g. B1 if a column to the left of this reference was removed). +func updateRefStr(refStr string, q *update.UpdateQuery) string { + ref, err := reference.ParseCellReference(refStr) + if err != nil { + return "#REF!" + } + if q.UpdateType == update.UpdateActionRemoveColumn { + columnIdxToRemove := q.ColumnIdx + columnIdx := ref.ColumnIdx + if columnIdx < columnIdxToRemove { + return refStr + } else if columnIdx == columnIdxToRemove { + return "#REF!" + } else { + return ref.Update(update.UpdateActionRemoveColumn).String() + } + } + return refStr +} diff --git a/spreadsheet/formula/columnref.go b/spreadsheet/formula/columnref.go new file mode 100644 index 00000000..4ddc24c2 --- /dev/null +++ b/spreadsheet/formula/columnref.go @@ -0,0 +1,22 @@ +// Copyright 2017 FoxyUtils ehf. All rights reserved. +// +// Use of this source code is governed by the terms of the Affero GNU General +// Public License version 3.0 as published by the Free Software Foundation and +// appearing in the file LICENSE included in the packaging of this file. A +// commercial license can be purchased on https://unidoc.io. + +package formula + +import "github.com/unidoc/unioffice/spreadsheet/reference" + +// updateColumnToLeft gets a column reference string representation like JJ, parses it and makes a string representation of a new reference with respect to the update type in the case of a column to the left of this reference was removed (e.g. JI). +func updateColumnToLeft(column string, colIdxToRemove uint32) string { + colIdx := reference.ColumnToIndex(column) + if colIdx == colIdxToRemove { + return "#REF!" + } else if colIdx > colIdxToRemove { + return reference.IndexToColumn(colIdx - 1) + } else { + return column + } +} diff --git a/spreadsheet/formula/constarrayexpr.go b/spreadsheet/formula/constarrayexpr.go index 0f08da7a..550c7931 100644 --- a/spreadsheet/formula/constarrayexpr.go +++ b/spreadsheet/formula/constarrayexpr.go @@ -7,6 +7,8 @@ package formula +import "github.com/unidoc/unioffice/spreadsheet/update" + // ConstArrayExpr is a constant array expression. type ConstArrayExpr struct { data [][]Expression @@ -34,3 +36,13 @@ func (c ConstArrayExpr) Eval(ctx Context, ev Evaluator) Result { func (c ConstArrayExpr) Reference(ctx Context, ev Evaluator) Reference { return ReferenceInvalid } + +// String returns a string representation of ConstArrayExpr. +func (c ConstArrayExpr) String() string { + return "" // to do +} + +// Update returns the same object as updating sheet references does not affect ConstArrayExpr. +func (c ConstArrayExpr) Update(q *update.UpdateQuery) Expression { + return c +} diff --git a/spreadsheet/formula/emptyexpr.go b/spreadsheet/formula/emptyexpr.go index 5a50abd8..b08f348d 100644 --- a/spreadsheet/formula/emptyexpr.go +++ b/spreadsheet/formula/emptyexpr.go @@ -7,6 +7,8 @@ package formula +import "github.com/unidoc/unioffice/spreadsheet/update" + // EmptyExpr is an empty expression. type EmptyExpr struct { } @@ -25,3 +27,13 @@ func (e EmptyExpr) Eval(ctx Context, ev Evaluator) Result { func (e EmptyExpr) Reference(ctx Context, ev Evaluator) Reference { return ReferenceInvalid } + +// String returns an empty string for EmptyExpr. +func (e EmptyExpr) String() string { + return "" +} + +// Update returns the same object as updating sheet references does not affect EmptyExpr. +func (e EmptyExpr) Update(q *update.UpdateQuery) Expression { + return e +} diff --git a/spreadsheet/formula/error.go b/spreadsheet/formula/error.go index 37dd10c0..ba5707ac 100644 --- a/spreadsheet/formula/error.go +++ b/spreadsheet/formula/error.go @@ -7,6 +7,8 @@ package formula +import "github.com/unidoc/unioffice/spreadsheet/update" + // Error is an error expression. type Error struct { s string @@ -26,3 +28,13 @@ func (e Error) Eval(ctx Context, ev Evaluator) Result { func (e Error) Reference(ctx Context, ev Evaluator) Reference { return ReferenceInvalid } + +// String returns an empty string for Error. +func (e Error) String() string { + return "" +} + +// Update returns the same object as updating sheet references does not affect Error. +func (e Error) Update(q *update.UpdateQuery) Expression { + return e +} diff --git a/spreadsheet/formula/expression.go b/spreadsheet/formula/expression.go index 9b2a76f1..b152384c 100644 --- a/spreadsheet/formula/expression.go +++ b/spreadsheet/formula/expression.go @@ -7,7 +7,11 @@ package formula +import "github.com/unidoc/unioffice/spreadsheet/update" + type Expression interface { Eval(ctx Context, ev Evaluator) Result Reference(ctx Context, ev Evaluator) Reference + String() string + Update(updateQuery *update.UpdateQuery) Expression } diff --git a/spreadsheet/formula/functioncall.go b/spreadsheet/formula/functioncall.go index 494bb9f9..3386f551 100644 --- a/spreadsheet/formula/functioncall.go +++ b/spreadsheet/formula/functioncall.go @@ -7,6 +7,11 @@ package formula +import ( + "bytes" + "github.com/unidoc/unioffice/spreadsheet/update" +) + // FunctionCall is a function call expression. type FunctionCall struct { name string @@ -46,3 +51,32 @@ func (f FunctionCall) Eval(ctx Context, ev Evaluator) Result { func (f FunctionCall) Reference(ctx Context, ev Evaluator) Reference { return ReferenceInvalid } + +// String returns a string representation of FunctionCall expression. +func (f FunctionCall) String() string { + buf := bytes.Buffer{} + buf.WriteString(f.name) + buf.WriteString("(") + lastArgIndex := len(f.args) - 1 + for argIndex, arg := range f.args { + buf.WriteString(arg.String()) + if argIndex != lastArgIndex { + buf.WriteString(",") + } + } + buf.WriteString(")") + return buf.String() +} + +// Update updates the FunctionCall references after removing a row/column. +func (f FunctionCall) Update(q *update.UpdateQuery) Expression { + newArgs := []Expression{} + for _, arg := range f.args { + newArg := arg.Update(q) + newArgs = append(newArgs, newArg) + } + return FunctionCall{ + name: f.name, + args: newArgs, + } +} diff --git a/spreadsheet/formula/horizontalrange.go b/spreadsheet/formula/horizontalrange.go index 8583733f..d48f18a8 100644 --- a/spreadsheet/formula/horizontalrange.go +++ b/spreadsheet/formula/horizontalrange.go @@ -7,6 +7,8 @@ package formula +import "github.com/unidoc/unioffice/spreadsheet/update" + import ( "fmt" "strconv" @@ -56,3 +58,13 @@ func cellRefsFromHorizontalRange(ctx Context, rowFrom, rowTo int) (string, strin to := lastColumn + strconv.Itoa(rowTo) return from, to } + +// String returns a string representation of a horizontal range. +func (r HorizontalRange) String() string { + return r.horizontalRangeReference() +} + +// Update updates the horizontal range references after removing a row/column. +func (r HorizontalRange) Update(q *update.UpdateQuery) Expression { + return r +} diff --git a/spreadsheet/formula/namedrangeref.go b/spreadsheet/formula/namedrangeref.go index 01028de2..804e7229 100644 --- a/spreadsheet/formula/namedrangeref.go +++ b/spreadsheet/formula/namedrangeref.go @@ -10,6 +10,8 @@ package formula import ( "fmt" "strings" + + "github.com/unidoc/unioffice/spreadsheet/update" ) // NamedRangeRef is a reference to a named range. @@ -53,3 +55,13 @@ func (n NamedRangeRef) Eval(ctx Context, ev Evaluator) Result { func (n NamedRangeRef) Reference(ctx Context, ev Evaluator) Reference { return Reference{Type: ReferenceTypeNamedRange, Value: n.s} } + +// String returns a string representation of a named range. +func (n NamedRangeRef) String() string { + return n.s +} + +// Update returns the same object as updating sheet references does not affect named ranges. +func (n NamedRangeRef) Update(q *update.UpdateQuery) Expression { + return n +} diff --git a/spreadsheet/formula/negate.go b/spreadsheet/formula/negate.go index 8a1a8d6a..bc76b8c7 100644 --- a/spreadsheet/formula/negate.go +++ b/spreadsheet/formula/negate.go @@ -7,6 +7,8 @@ package formula +import "github.com/unidoc/unioffice/spreadsheet/update" + // Negate is a negate expression like -A1. type Negate struct { e Expression @@ -30,3 +32,13 @@ func (n Negate) Eval(ctx Context, ev Evaluator) Result { func (n Negate) Reference(ctx Context, ev Evaluator) Reference { return ReferenceInvalid } + +// String returns a string representation for Negate. +func (n Negate) String() string { + return "-" + n.e.String() +} + +// Update updates references in the Negate after removing a row/column. +func (n Negate) Update(q *update.UpdateQuery) Expression { + return Negate{n.e.Update(q)} +} diff --git a/spreadsheet/formula/number.go b/spreadsheet/formula/number.go index d122d0e6..82e62ec4 100644 --- a/spreadsheet/formula/number.go +++ b/spreadsheet/formula/number.go @@ -11,6 +11,7 @@ import ( "strconv" "github.com/unidoc/unioffice" + "github.com/unidoc/unioffice/spreadsheet/update" ) // Number is a nubmer expression. @@ -36,3 +37,13 @@ func (n Number) Eval(ctx Context, ev Evaluator) Result { func (n Number) Reference(ctx Context, ev Evaluator) Reference { return ReferenceInvalid } + +// String returns a string representation of Number. +func (n Number) String() string { + return strconv.FormatFloat(n.v, 'f', -1, 64) +} + +// Update returns the same object as updating sheet references does not affect Number. +func (n Number) Update(q *update.UpdateQuery) Expression { + return n +} diff --git a/spreadsheet/formula/prefixexpr.go b/spreadsheet/formula/prefixexpr.go index 3a4dc862..0b00e5d2 100644 --- a/spreadsheet/formula/prefixexpr.go +++ b/spreadsheet/formula/prefixexpr.go @@ -7,7 +7,11 @@ package formula -import "fmt" +import ( + "fmt" + + "github.com/unidoc/unioffice/spreadsheet/update" +) // PrefixExpr is an expression containing reference to another sheet like Sheet1!A1 (the value of the cell A1 from sheet 'Sheet1'). type PrefixExpr struct { @@ -41,3 +45,20 @@ func (p PrefixExpr) Reference(ctx Context, ev Evaluator) Reference { } return ReferenceInvalid } + +// String returns a string representation of PrefixExpr. +func (p PrefixExpr) String() string { + return fmt.Sprintf("%s!%s", p.pfx.String(), p.exp.String()) +} + +// Update updates references in the PrefixExpr after removing a row/column. +func (p PrefixExpr) Update(q *update.UpdateQuery) Expression { + new := p + sheetName := p.pfx.String() + if sheetName == q.SheetToUpdate { + newQ := *q + newQ.UpdateCurrentSheet = true + new.exp = p.exp.Update(&newQ) + } + return new +} diff --git a/spreadsheet/formula/prefixhorizontalrange.go b/spreadsheet/formula/prefixhorizontalrange.go index ca128590..1fc2aa2b 100644 --- a/spreadsheet/formula/prefixhorizontalrange.go +++ b/spreadsheet/formula/prefixhorizontalrange.go @@ -11,6 +11,8 @@ import ( "fmt" "strconv" "strings" + + "github.com/unidoc/unioffice/spreadsheet/update" ) // PrefixHorizontalRange is a range expression that when evaluated returns a list of Results from references like Sheet1!1:4 (all cells from rows 1 to 4 of sheet 'Sheet1'). @@ -58,3 +60,13 @@ func (r PrefixHorizontalRange) Reference(ctx Context, ev Evaluator) Reference { pfx := r.pfx.Reference(ctx, ev) return Reference{Type: ReferenceTypeHorizontalRange, Value: r.horizontalRangeReference(pfx.Value)} } + +// String returns a string representation of a horizontal range with prefix. +func (r PrefixHorizontalRange) String() string { + return fmt.Sprintf("%s!%d:%d", r.pfx.String(), r.rowFrom, r.rowTo) +} + +// Update updates references in the PrefixHorizontalRange after removing a row/column. +func (r PrefixHorizontalRange) Update(q *update.UpdateQuery) Expression { + return r +} diff --git a/spreadsheet/formula/prefixrangeexpr.go b/spreadsheet/formula/prefixrangeexpr.go index ede6cf10..ed1dfd27 100644 --- a/spreadsheet/formula/prefixrangeexpr.go +++ b/spreadsheet/formula/prefixrangeexpr.go @@ -7,7 +7,11 @@ package formula -import "fmt" +import ( + "fmt" + + "github.com/unidoc/unioffice/spreadsheet/update" +) // PrefixRangeExpr is a range expression that when evaluated returns a list of Results from a given sheet like Sheet1!A1:B4 (all cells from A1 to B4 from a sheet 'Sheet1'). type PrefixRangeExpr struct { @@ -56,3 +60,21 @@ func (p PrefixRangeExpr) Reference(ctx Context, ev Evaluator) Reference { } return ReferenceInvalid } + +// String returns a string representation of a range with prefix. +func (r PrefixRangeExpr) String() string { + return fmt.Sprintf("%s!%s:%s", r.pfx.String(), r.from.String(), r.to.String()) +} + +// Update updates references in the PrefixRangeExpr after removing a row/column. +func (r PrefixRangeExpr) Update(q *update.UpdateQuery) Expression { + new := r + sheetName := r.pfx.String() + if sheetName == q.SheetToUpdate { + newQ := *q + newQ.UpdateCurrentSheet = true + new.from = r.from.Update(&newQ) + new.to = r.to.Update(&newQ) + } + return new +} diff --git a/spreadsheet/formula/prefixverticalrange.go b/spreadsheet/formula/prefixverticalrange.go index a07ad53c..3308dcca 100644 --- a/spreadsheet/formula/prefixverticalrange.go +++ b/spreadsheet/formula/prefixverticalrange.go @@ -10,6 +10,8 @@ package formula import ( "fmt" "strings" + + "github.com/unidoc/unioffice/spreadsheet/update" ) // PrefixVerticalRange is a range expression that when evaluated returns a list of Results from references like Sheet1!AA:IJ (all cells from columns AA to IJ of sheet 'Sheet1'). @@ -55,3 +57,23 @@ func (r PrefixVerticalRange) Reference(ctx Context, ev Evaluator) Reference { pfx := r.pfx.Reference(ctx, ev) return Reference{Type: ReferenceTypeVerticalRange, Value: r.verticalRangeReference(pfx.Value)} } + +// String returns a string representation of a vertical range with prefix. +func (r PrefixVerticalRange) String() string { + return fmt.Sprintf("%s!%s:%s", r.pfx.String(), r.colFrom, r.colTo) +} + +// Update updates references in the PrefixVerticalRange after removing a row/column. +func (r PrefixVerticalRange) Update(q *update.UpdateQuery) Expression { + if q.UpdateType == update.UpdateActionRemoveColumn { + new := r + sheetName := r.pfx.String() + if sheetName == q.SheetToUpdate { + columnIdx := q.ColumnIdx + new.colFrom = updateColumnToLeft(r.colFrom, columnIdx) + new.colTo = updateColumnToLeft(r.colTo, columnIdx) + } + return new + } + return r +} diff --git a/spreadsheet/formula/range.go b/spreadsheet/formula/range.go index 6e579f2c..1a93851b 100644 --- a/spreadsheet/formula/range.go +++ b/spreadsheet/formula/range.go @@ -11,6 +11,7 @@ import ( "fmt" "github.com/unidoc/unioffice/spreadsheet/reference" + "github.com/unidoc/unioffice/spreadsheet/update" ) // Range is a range expression that when evaluated returns a list of Results. @@ -87,3 +88,18 @@ func resultFromCellRange(ctx Context, ev Evaluator, from, to string) Result { return MakeArrayResult(arr) } + +// String returns a string of a range. +func (r Range) String() string { + return fmt.Sprintf("%s:%s", r.from.String(), r.to.String()) +} + +// Update updates references in the Range after removing a row/column. +func (r Range) Update(q *update.UpdateQuery) Expression { + new := r + if q.UpdateCurrentSheet { + new.from = r.from.Update(q) + new.to = r.to.Update(q) + } + return new +} diff --git a/spreadsheet/formula/sheetprefixexpr.go b/spreadsheet/formula/sheetprefixexpr.go index 4805f2dd..a72f3b22 100644 --- a/spreadsheet/formula/sheetprefixexpr.go +++ b/spreadsheet/formula/sheetprefixexpr.go @@ -7,6 +7,8 @@ package formula +import "github.com/unidoc/unioffice/spreadsheet/update" + // SheetPrefixExpr is a reference to a sheet like Sheet1! (reference to sheet 'Sheet1'). type SheetPrefixExpr struct { sheet string @@ -26,3 +28,13 @@ func (s SheetPrefixExpr) Eval(ctx Context, ev Evaluator) Result { func (s SheetPrefixExpr) Reference(ctx Context, ev Evaluator) Reference { return Reference{Type: ReferenceTypeSheet, Value: s.sheet} } + +// String returns a string representation of SheetPrefixExpr. +func (s SheetPrefixExpr) String() string { + return s.sheet +} + +// Update returns the same object as updating sheet references does not affect SheetPrefixExpr. +func (s SheetPrefixExpr) Update(q *update.UpdateQuery) Expression { + return s +} diff --git a/spreadsheet/formula/string.go b/spreadsheet/formula/string.go index db0062c1..77512755 100644 --- a/spreadsheet/formula/string.go +++ b/spreadsheet/formula/string.go @@ -7,7 +7,11 @@ package formula -import "strings" +import ( + "strings" + + "github.com/unidoc/unioffice/spreadsheet/update" +) // String is a string expression. type String struct { @@ -30,3 +34,13 @@ func (s String) Eval(ctx Context, ev Evaluator) Result { func (s String) Reference(ctx Context, ev Evaluator) Reference { return ReferenceInvalid } + +// String returns a string representation of String. +func (s String) String() string { + return `"` + s.s + `"` +} + +// Update returns the same object as updating sheet references does not affect String. +func (s String) Update(q *update.UpdateQuery) Expression { + return s +} diff --git a/spreadsheet/formula/verticalrange.go b/spreadsheet/formula/verticalrange.go index 12ac4e22..150aa16a 100644 --- a/spreadsheet/formula/verticalrange.go +++ b/spreadsheet/formula/verticalrange.go @@ -11,6 +11,8 @@ import ( "fmt" "strconv" "strings" + + "github.com/unidoc/unioffice/spreadsheet/update" ) // VerticalRange is a range expression that when evaluated returns a list of Results from references like AA:IJ (all cells from columns AA to IJ). @@ -54,3 +56,22 @@ func cellRefsFromVerticalRange(ctx Context, colFrom, colTo string) (string, stri to := colTo + strconv.Itoa(lastRow) return from, to } + +// String returns a string representation of a vertical range. +func (r VerticalRange) String() string { + return r.verticalRangeReference() +} + +// Update updates references in the VerticalRange after removing a row/column. +func (r VerticalRange) Update(q *update.UpdateQuery) Expression { + if q.UpdateType == update.UpdateActionRemoveColumn { + new := r + if q.UpdateCurrentSheet { + columnIdx := q.ColumnIdx + new.colFrom = updateColumnToLeft(r.colFrom, columnIdx) + new.colTo = updateColumnToLeft(r.colTo, columnIdx) + } + return new + } + return r +} diff --git a/spreadsheet/reference/cellreference.go b/spreadsheet/reference/cellreference.go index b86b8d13..3606ca48 100644 --- a/spreadsheet/reference/cellreference.go +++ b/spreadsheet/reference/cellreference.go @@ -12,6 +12,8 @@ import ( "fmt" "strconv" "strings" + + "github.com/unidoc/unioffice/spreadsheet/update" ) // CellReference is a parsed reference to a cell. Input is of the form 'A1', @@ -25,6 +27,7 @@ type CellReference struct { SheetName string } +// String returns a string representation of CellReference. func (c CellReference) String() string { buf := make([]byte, 0, 4) if c.AbsoluteColumn { @@ -90,3 +93,16 @@ lfor: return r, nil } + +// Update updates reference to point one of the neighboring cells with respect to the update type after removing a row/column. +func (ref *CellReference) Update(updateType update.UpdateAction) *CellReference { + switch updateType { + case update.UpdateActionRemoveColumn: + newRef := ref + newRef.ColumnIdx = ref.ColumnIdx - 1 + newRef.Column = IndexToColumn(newRef.ColumnIdx) + return newRef + default: + return ref + } +} diff --git a/spreadsheet/reference/columnreference.go b/spreadsheet/reference/columnreference.go new file mode 100644 index 00000000..3c72aaf8 --- /dev/null +++ b/spreadsheet/reference/columnreference.go @@ -0,0 +1,80 @@ +// Copyright 2017 FoxyUtils ehf. All rights reserved. +// +// Use of this source code is governed by the terms of the Affero GNU General +// Public License version 3.0 as published by the Free Software Foundation and +// appearing in the file LICENSE included in the packaging of this file. A +// commercial license can be purchased on https://unidoc.io. + +package reference + +import ( + "errors" + "regexp" + "strings" + + "github.com/unidoc/unioffice/spreadsheet/update" +) + +// ColumnReference is a parsed reference to a column. Input is of the form 'A', +// '$C', etc. +type ColumnReference struct { + ColumnIdx uint32 + Column string + AbsoluteColumn bool + SheetName string +} + +// String returns a string representation of ColumnReference. +func (c ColumnReference) String() string { + buf := make([]byte, 0, 4) + if c.AbsoluteColumn { + buf = append(buf, '$') + } + buf = append(buf, c.Column...) + return string(buf) +} + +var reColumn = regexp.MustCompile(`^[a-zA-Z]([a-zA-Z]?)$`) + +// ParseColumnReference parses a column reference of the form 'Sheet1!A' and splits it +// into sheet name and column segments. +func ParseColumnReference(s string) (ColumnReference, error) { + s = strings.TrimSpace(s) + if len(s) < 1 { + return ColumnReference{}, errors.New("column reference must have at least one character") + } + + r := ColumnReference{} + sl := strings.Split(s, "!") + if len(sl) == 2 { + r.SheetName = sl[0] + s = sl[1] + } + // check for absolute column + if s[0] == '$' { + r.AbsoluteColumn = true + s = s[1:] + } + + if !reColumn.MatchString(s) { + return ColumnReference{}, errors.New("column reference must be between A and ZZ") + } + + r.Column = s + + r.ColumnIdx = ColumnToIndex(r.Column) + return r, nil +} + +// Update updates reference to point one of the neighboring columns with respect to the update type after removing a row/column. +func (ref *ColumnReference) Update(updateType update.UpdateAction) *ColumnReference { + switch updateType { + case update.UpdateActionRemoveColumn: + newRef := ref + newRef.ColumnIdx = ref.ColumnIdx - 1 + newRef.Column = IndexToColumn(newRef.ColumnIdx) + return newRef + default: + return ref + } +} diff --git a/spreadsheet/reference/rangereference.go b/spreadsheet/reference/rangereference.go index 08a4a9b0..50db0ad9 100644 --- a/spreadsheet/reference/rangereference.go +++ b/spreadsheet/reference/rangereference.go @@ -41,3 +41,33 @@ func ParseRangeReference(s string) (from, to CellReference, err error) { } return fromRef, toRef, nil } + +// ParseColumnRangeReference splits a range reference of the form "A:B" into its +// components. +func ParseColumnRangeReference(s string) (from, to ColumnReference, err error) { + sheetName := "" + sp0 := strings.Split(s, "!") + if len(sp0) == 2 { + sheetName = sp0[0] + s = sp0[1] + } + sp := strings.Split(s, ":") + if len(sp) != 2 { + return ColumnReference{}, ColumnReference{}, errors.New("invalid range format") + } + + if sheetName != "" { + sp[0] = sheetName + "!" + sp[0] + sp[1] = sheetName + "!" + sp[1] + } + fromRef, err := ParseColumnReference(sp[0]) + if err != nil { + return ColumnReference{}, ColumnReference{}, err + } + + toRef, err := ParseColumnReference(sp[1]) + if err != nil { + return ColumnReference{}, ColumnReference{}, err + } + return fromRef, toRef, nil +} diff --git a/spreadsheet/sheet.go b/spreadsheet/sheet.go index 28cb4b35..6a45a52d 100644 --- a/spreadsheet/sheet.go +++ b/spreadsheet/sheet.go @@ -8,6 +8,7 @@ package spreadsheet import ( + "errors" "fmt" "log" "sort" @@ -15,6 +16,7 @@ import ( "github.com/unidoc/unioffice/spreadsheet/formula" "github.com/unidoc/unioffice/spreadsheet/reference" + "github.com/unidoc/unioffice/spreadsheet/update" "github.com/unidoc/unioffice" "github.com/unidoc/unioffice/common" @@ -871,3 +873,257 @@ func (s *Sheet) Sort(column string, firstRow uint32, order SortOrder) { } } } + +// RemoveColumn removes column from the sheet and moves all columns to the right of the removed column one step left. +func (s *Sheet) RemoveColumn(column string) error { + cellsInFormulaArrays, err := s.getAllCellsInFormulaArraysForColumn() + if err != nil { + return err + } + columnIdx := reference.ColumnToIndex(column) + for _, row := range s.Rows() { + ref := fmt.Sprintf("%s%d", column, *row.X().RAttr) + if _, ok := cellsInFormulaArrays[ref]; ok { + return nil + } + } + for _, row := range s.Rows() { + cells := row.x.C + for ic, cell := range cells { + ref, err := reference.ParseCellReference(*cell.RAttr) + if err != nil { + return err + } + if ref.ColumnIdx == columnIdx { + row.x.C = append(cells[:ic], s.slideCellsLeft(cells[ic+1:])...) + break + } else if ref.ColumnIdx > columnIdx { + row.x.C = append(cells[:ic], s.slideCellsLeft(cells[ic:])...) + break + } + } + } + + err = s.updateAfterRemove(columnIdx, update.UpdateActionRemoveColumn) + if err != nil { + return err + } + + err = s.removeColumnFromNamedRanges(columnIdx) + if err != nil { + return err + } + + err = s.removeColumnFromMergedCells(columnIdx) + if err != nil { + return err + } + + for _, sheet := range s.w.Sheets() { + sheet.RecalculateFormulas() + } + return nil +} + +func (s *Sheet) updateAfterRemove(columnIdx uint32, updateType update.UpdateAction) error { + ownSheetName := s.Name() + q := &update.UpdateQuery{ + UpdateType: updateType, + ColumnIdx: columnIdx, + SheetToUpdate: ownSheetName, + } + for _, sheet := range s.w.Sheets() { + q.UpdateCurrentSheet = ownSheetName == sheet.Name() + for _, r := range sheet.Rows() { + for _, c := range r.Cells() { + if c.X().F != nil { + formStr := c.X().F.Content + expr := formula.ParseString(formStr) + if expr == nil { + c.SetError("#REF!") + } else { + newExpr := expr.Update(q) + c.X().F.Content = fmt.Sprintf("=%s", newExpr.String()) + } + } + } + } + } + return nil +} + +func (s *Sheet) slideCellsLeft(cells []*sml.CT_Cell) []*sml.CT_Cell { + for _, cell := range cells { + ref, err := reference.ParseCellReference(*cell.RAttr) + if err != nil { + return cells + } + newColumnIdx := ref.ColumnIdx - 1 + newRefStr := reference.IndexToColumn(newColumnIdx) + fmt.Sprintf("%d", ref.RowIdx) + cell.RAttr = &newRefStr + } + return cells +} + +func (s *Sheet) removeColumnFromMergedCells(columnIdx uint32) error { + if s.x.MergeCells == nil || s.x.MergeCells.MergeCell == nil { + return nil + } + newMergedCells := []*sml.CT_MergeCell{} + for _, mc := range s.MergedCells() { + newRefStr := moveRangeLeft(mc.Reference(), columnIdx, true) + if newRefStr != "" { + mc.SetReference(newRefStr) + newMergedCells = append(newMergedCells, mc.X()) + } + } + s.x.MergeCells.MergeCell = newMergedCells + return nil +} + +func (s *Sheet) removeColumnFromNamedRanges(columnIdx uint32) error { + for _, dn := range s.w.DefinedNames() { + name := dn.Name() + content := dn.Content() + sp := strings.Split(content, "!") + if len(sp) != 2 { + return errors.New("Incorrect named range:" + content) + } + sheetName := sp[0] + if s.Name() == sheetName { + err := s.w.RemoveDefinedName(dn) + if err != nil { + return err + } + newRefStr := moveRangeLeft(sp[1], columnIdx, true) + if newRefStr != "" { + newContent := sheetName + "!" + newRefStr + s.w.AddDefinedName(name, newContent) + } + } + } + numTables := 0 + if s.x.TableParts != nil && s.x.TableParts.TablePart != nil { + numTables = len(s.x.TableParts.TablePart) + } + if numTables != 0 { + startFromTable := 0 + for _, sheet := range s.w.Sheets() { + if sheet.Name() == s.Name() { + break + } else { + if sheet.x.TableParts != nil && sheet.x.TableParts.TablePart != nil { + startFromTable += len(sheet.x.TableParts.TablePart) + } + } + } + sheetTables := s.w.tables[startFromTable:startFromTable + numTables] + for tblIndex, tbl := range sheetTables { + newTable := tbl + newTable.RefAttr = moveRangeLeft(newTable.RefAttr, columnIdx, false) + s.w.tables[startFromTable + tblIndex] = newTable + } + } + return nil +} + +func moveRangeLeft(ref string, columnIdx uint32, remove bool) string { + fromCell, toCell, err := reference.ParseRangeReference(ref) + if err == nil { + fromColIdx, toColIdx := fromCell.ColumnIdx, toCell.ColumnIdx + if columnIdx >= fromColIdx && columnIdx <= toColIdx { + if fromColIdx == toColIdx { + if remove { + return "" + } else { + return ref + } + } else { + newTo := toCell.Update(update.UpdateActionRemoveColumn) + return fmt.Sprintf("%s:%s", fromCell.String(), newTo.String()) + } + } else if columnIdx < fromColIdx { + newFrom := fromCell.Update(update.UpdateActionRemoveColumn) + newTo := toCell.Update(update.UpdateActionRemoveColumn) + return fmt.Sprintf("%s:%s", newFrom.String(), newTo.String()) + } + } else { + fromColumn, toColumn, err := reference.ParseColumnRangeReference(ref) + if err != nil { + return "" + } + fromColIdx, toColIdx := fromColumn.ColumnIdx, toColumn.ColumnIdx + if columnIdx >= fromColIdx && columnIdx <= toColIdx { + if fromColIdx == toColIdx { + if remove { + return "" + } else { + return ref + } + } else { + newTo := toColumn.Update(update.UpdateActionRemoveColumn) + return fmt.Sprintf("%s:%s", fromColumn.String(), newTo.String()) + } + } else if columnIdx < fromColIdx { + newFrom := fromColumn.Update(update.UpdateActionRemoveColumn) + newTo := toColumn.Update(update.UpdateActionRemoveColumn) + return fmt.Sprintf("%s:%s", newFrom.String(), newTo.String()) + } + } + return "" +} + +func (s *Sheet) getAllCellsInFormulaArraysForColumn() (map[string]bool, error) { + return s.getAllCellsInFormulaArrays(false) +} + +// getAllCellsInFormulaArrays returns all cells of the sheet that are covered by formula arrays. It is a helper for checking when removing rows and columns and skips all arrays of length 1 column when removing columns and all arrays of length 1 row when removing rows. +func (s *Sheet) getAllCellsInFormulaArrays(isRow bool) (map[string]bool, error) { + ev := formula.NewEvaluator() + ctx := s.FormulaContext() + cellsInFormulaArrays := map[string]bool{} + for _, r := range s.Rows() { + for _, c := range r.Cells() { + if c.X().F != nil { + formStr := c.X().F.Content + if c.X().F.TAttr == sml.ST_CellFormulaTypeArray { + res := ev.Eval(ctx, formStr).AsString() + if res.Type == formula.ResultTypeError { + unioffice.Log("error evaulating formula %s: %s", formStr, res.ErrorMessage) + c.X().V = nil + } + if res.Type == formula.ResultTypeArray { + cref, err := reference.ParseCellReference(c.Reference()) + if err != nil { + return map[string]bool{}, err + } + if (isRow && len(res.ValueArray) == 1) || (!isRow && len(res.ValueArray[0]) == 1) { + continue + } + for ir, row := range res.ValueArray { + rowIdx := cref.RowIdx + uint32(ir) + for ic := range row { + column := reference.IndexToColumn(cref.ColumnIdx + uint32(ic)) + cellsInFormulaArrays[fmt.Sprintf("%s%d", column, rowIdx)] = true + } + } + } else if res.Type == formula.ResultTypeList { + cref, err := reference.ParseCellReference(c.Reference()) + if err != nil { + return map[string]bool{}, err + } + if isRow || len(res.ValueList) == 1 { + continue + } + rowIdx := cref.RowIdx + for ic := range res.ValueList { + column := reference.IndexToColumn(cref.ColumnIdx + uint32(ic)) + cellsInFormulaArrays[fmt.Sprintf("%s%d", column, rowIdx)] = true + } + } + } + } + } + } + return cellsInFormulaArrays, nil +} diff --git a/spreadsheet/sheet_test.go b/spreadsheet/sheet_test.go index 1ad8a241..5d5cd089 100644 --- a/spreadsheet/sheet_test.go +++ b/spreadsheet/sheet_test.go @@ -15,6 +15,7 @@ import ( "github.com/unidoc/unioffice" "github.com/unidoc/unioffice/spreadsheet" + "github.com/unidoc/unioffice/spreadsheet/reference" ) func TestRowNumIncreases(t *testing.T) { @@ -162,6 +163,16 @@ func TestMergedCell(t *testing.T) { t.Errorf("expected merged cell content to be '%s', got '%s'", expContent, mc.Cell().GetString()) } + sheet.RemoveColumn("B") + if mc.Cell().GetString() != expContent { + t.Errorf("expected merged cell content to be '%s', got '%s'", expContent, mc.Cell().GetString()) + } + + sheet.RemoveColumn("A") + if mc.Cell().GetString() != "" { + t.Errorf("expected merged cell content to be '%s', got '%s'", expContent, mc.Cell().GetString()) + } + sheet.RemoveMergedCell(mc) if len(sheet.MergedCells()) != 0 { t.Errorf("after removal, sheet should have no merged cells") @@ -334,3 +345,27 @@ func TestSortStrings(t *testing.T) { } } } + +func TestRemoveColumn(t *testing.T) { + wb := spreadsheet.New() + sheet := wb.AddSheet() + sheet.Cell("A1").SetNumber(5) + sheet.Cell("B1").SetNumber(4) + sheet.Cell("C1").SetNumber(3) + sheet.Cell("D1").SetNumber(2) + sheet.Cell("E1").SetNumber(1) + + sheet.RemoveColumn("C") + + expected := []float64{5,4,2,1,0} + + for i := 0; i <= 4; i++ { + column := reference.IndexToColumn(uint32(i)) + ref := fmt.Sprintf("%s1", column) + got, _ := sheet.Cell(ref).GetValueAsNumber() + exp := expected[i] + if got != exp { + t.Errorf("expected %f in %s, got %f", exp, ref, got) + } + } +} diff --git a/spreadsheet/update/update_query.go b/spreadsheet/update/update_query.go new file mode 100644 index 00000000..85b39c3d --- /dev/null +++ b/spreadsheet/update/update_query.go @@ -0,0 +1,31 @@ +// Copyright 2017 FoxyUtils ehf. All rights reserved. +// +// Use of this source code is governed by the terms of the Affero GNU General +// Public License version 3.0 as published by the Free Software Foundation and +// appearing in the file LICENSE included in the packaging of this file. A +// commercial license can be purchased on https://unidoc.io. + +// Package update contains definitions needed for updating references after removing rows/columns. +package update + +// UpdateAction is the type for update types constants. +type UpdateAction byte +const ( + // UpdateActionRemoveColumn means updating references after removing a column. + UpdateActionRemoveColumn UpdateAction = iota +) + +// UpdateQuery contains terms of how to update references after removing row/column. +type UpdateQuery struct { + // UpdateType is one of the update types like UpdateActionRemoveColumn. + UpdateType UpdateAction + + // ColumnIdx is the index of the column removed. + ColumnIdx uint32 + + // SheetToUpdate contains the name of the sheet on which removing happened. + SheetToUpdate string + + // UpdateCurrentSheet is true if references without sheet prefix should be updated as well. + UpdateCurrentSheet bool +}