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
This commit is contained in:
Vyacheslav Zgordan 2020-02-11 22:47:08 +03:00 committed by GitHub
parent 280d692114
commit ddafaca850
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 867 additions and 3 deletions

View File

@ -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")
}

Binary file not shown.

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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
}