Skip to content

Commit

Permalink
spreadsheet: start adding support for formula evaluation
Browse files Browse the repository at this point in the history
We can take advantage of cached formula results that Excel/LibreOffice
insert to ensure that our formula results match the expected results.
  • Loading branch information
tbaliance committed Sep 15, 2017
1 parent 111567b commit 17a365e
Show file tree
Hide file tree
Showing 36 changed files with 4,213 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ coverage*

**/.coverprofile
gover.coverprofile
spreadsheet/formula/y.output
36 changes: 36 additions & 0 deletions _examples/spreadsheet/formula-evaluation/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright 2017 Baliance. All rights reserved.
package main

import (
"fmt"

"baliance.com/gooxml/spreadsheet/formula"

"baliance.com/gooxml/spreadsheet"
)

func main() {
ss := spreadsheet.New()
sheet := ss.AddSheet()
sheet.Cell("A1").SetNumber(1.2)
sheet.Cell("A2").SetNumber(2.3)
sheet.Cell("A3").SetNumber(2.3)

formEv := formula.NewEvaluator()

// the formula context allows the formula evaluator to pull data from a
// sheet
a1Cell := sheet.FormulaContext().Cell("A1", formEv)
fmt.Println("A1 is", a1Cell.Value())

// So that when evaluating formulas, live workbook data is used. Formulas
// can be evaluated directly in the context of a sheet.
result := formEv.Eval(sheet.FormulaContext(), "SUM(A1:A3)")
fmt.Println("SUM(A1:A3) is", result.Value())

// Or, stored in a cell and the cell evaulated.
sheet.Cell("A4").SetFormulaRaw("SUM(A1:A3)+SUM(A1:A3)")
a4Value := formEv.Eval(sheet.FormulaContext(), "A4")
fmt.Println("A4 is", a4Value.Value())

}
31 changes: 31 additions & 0 deletions spreadsheet/cell.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,37 @@ func (c Cell) AddHyperlink(url string) {
}
}

// IsNumber returns true if the cell is a number type cell.
func (c Cell) IsNumber() bool {
return c.x.TAttr == sml.ST_CellTypeN
}

// IsBool returns true if the cell is a boolean type cell.
func (c Cell) IsBool() bool {
return c.x.TAttr == sml.ST_CellTypeB
}

// HasFormula returns true if the cell has an asoociated formula.
func (c Cell) HasFormula() bool {
return c.x.F != nil
}

// GetFormula returns the formula for a cell.
func (c Cell) GetFormula() string {
if c.x.F != nil {
return c.x.F.Content
}
return ""
}

// GetCachedFormulaResult returns the cached formula result if it exists.
func (c Cell) GetCachedFormulaResult() string {
if c.x.F != nil && c.x.V != nil {
return *c.x.V
}
return ""
}

func b2i(v bool) int {
if v {
return 1
Expand Down
48 changes: 48 additions & 0 deletions spreadsheet/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright 2017 Baliance. 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 by contacting [email protected].

package spreadsheet

import "baliance.com/gooxml/spreadsheet/formula"

func newEvalContext(s *Sheet) *evalContext {
return &evalContext{s, make(map[string]struct{})}
}

type evalContext struct {
s *Sheet
evaluating map[string]struct{}
}

func (e *evalContext) Cell(ref string, ev formula.Evaluator) formula.Result {
c := e.s.Cell(ref)

// if we have a formula, evaluate it
if c.HasFormula() {
if _, ok := e.evaluating[ref]; ok {
// recursively evaluating, so bail out
return formula.MakeErrorResult("recursion detected during evaluation of " + ref)
}
e.evaluating[ref] = struct{}{}
res := ev.Eval(e, c.GetFormula())
delete(e.evaluating, ref)
return res
}

if c.IsNumber() {
v, _ := c.GetValueAsNumber()
return formula.MakeNumberResult(v)
} else if c.IsBool() {
v, _ := c.GetValueAsBool()
return formula.MakeBoolResult(v)
} else {
v, _ := c.GetValue()
return formula.MakeStringResult(v)
}
// TODO: handle this properly
// return formula.MakeErrorResult()
}
130 changes: 130 additions & 0 deletions spreadsheet/formula/binaryexpr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright 2017 Baliance. 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 by contacting [email protected].

package formula

import "math"

// BinOpType is the binary operation operator type
//go:generate stringer -type=BinOpType
type BinOpType byte

// Operator type constants
const (
BinOpTypeUnknown BinOpType = iota
BinOpTypePlus
BinOpTypeMinus
BinOpTypeMult
BinOpTypeDiv
BinOpTypeExp
BinOpTypeLT
BinOpTypeGT
BinOpTypeEQ
BinOpTypeLEQ
BinOpTypeGEQ
BinOpTypeNE
BinOpTypeConcat // '&' in Excel
)

// BinaryExpr is a binary expression.
type BinaryExpr struct {
lhs, rhs Expression
op BinOpType
}

// NewBinaryExpr constructs a new binary expression with a given operator.
func NewBinaryExpr(lhs Expression, op BinOpType, rhs Expression) Expression {
return BinaryExpr{lhs, rhs, op}
}

// Eval evaluates the binary expression using the context given.
func (b BinaryExpr) Eval(ctx Context, ev Evaluator) Result {
lhs := b.lhs.Eval(ctx, ev)
rhs := b.rhs.Eval(ctx, ev)

// TODO: check for and add support for binary operators on boolean values
switch b.op {
case BinOpTypePlus:
if lhs.Type == rhs.Type {
if lhs.Type == ResultTypeNumber {
return MakeNumberResult(lhs.ValueNumber + rhs.ValueNumber)
}
}
case BinOpTypeMinus:
if lhs.Type == rhs.Type {
if lhs.Type == ResultTypeNumber {
return MakeNumberResult(lhs.ValueNumber - rhs.ValueNumber)
}
}
case BinOpTypeMult:
if lhs.Type == rhs.Type {
if lhs.Type == ResultTypeNumber {
return MakeNumberResult(lhs.ValueNumber * rhs.ValueNumber)
}
}
case BinOpTypeDiv:
if lhs.Type == rhs.Type {
if lhs.Type == ResultTypeNumber {
if rhs.ValueNumber == 0 {
return MakeErrorResultType(ErrorTypeDivideByZero, "divide by zero")
}
return MakeNumberResult(lhs.ValueNumber / rhs.ValueNumber)
}
}
case BinOpTypeExp:
if lhs.Type == rhs.Type {
if lhs.Type == ResultTypeNumber {
return MakeNumberResult(math.Pow(lhs.ValueNumber, rhs.ValueNumber))
}
}
case BinOpTypeLT:
if lhs.Type == rhs.Type {
if lhs.Type == ResultTypeNumber {
return MakeBoolResult(lhs.ValueNumber < rhs.ValueNumber)
}
}
case BinOpTypeGT:
if lhs.Type == rhs.Type {
if lhs.Type == ResultTypeNumber {
return MakeBoolResult(lhs.ValueNumber > rhs.ValueNumber)
}
}
case BinOpTypeEQ:
if lhs.Type == rhs.Type {
if lhs.Type == ResultTypeNumber {
// TODO: see what Excel does regarding floating point comparison
return MakeBoolResult(lhs.ValueNumber == rhs.ValueNumber)
}
}
case BinOpTypeNE:
if lhs.Type == rhs.Type {
if lhs.Type == ResultTypeNumber {
return MakeBoolResult(lhs.ValueNumber != rhs.ValueNumber)
}
}
case BinOpTypeLEQ:
if lhs.Type == rhs.Type {
if lhs.Type == ResultTypeNumber {
return MakeBoolResult(lhs.ValueNumber <= rhs.ValueNumber)
}
}
case BinOpTypeGEQ:
if lhs.Type == rhs.Type {
if lhs.Type == ResultTypeNumber {
return MakeBoolResult(lhs.ValueNumber >= rhs.ValueNumber)
}
}
case BinOpTypeConcat:
return MakeStringResult(lhs.Value() + rhs.Value())
}

return MakeErrorResult("unsupported binary op")
}

func (b BinaryExpr) Reference(ctx Context, ev Evaluator) Reference {
return ReferenceInvalid
}
16 changes: 16 additions & 0 deletions spreadsheet/formula/binoptype_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions spreadsheet/formula/bool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2017 Baliance. 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 by contacting [email protected].

package formula

import (
"strconv"
)

type Bool struct {
b bool
}

func NewBool(v string) Expression {
b, err := strconv.ParseBool(v)
if err != nil {
// TODO: report erro
}
return Bool{b}
}

func (b Bool) Eval(ctx Context, ev Evaluator) Result {
return MakeBoolResult(b.b)
}

func (b Bool) Reference(ctx Context, ev Evaluator) Reference {
return ReferenceInvalid
}
27 changes: 27 additions & 0 deletions spreadsheet/formula/cellref.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2017 Baliance. 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 by contacting [email protected].

package formula

// CellRef is a reference to a single cell
type CellRef struct {
s string
}

// NewCellRef constructs a new cell reference.
func NewCellRef(v string) Expression {
return CellRef{v}
}

// Eval evaluates and returns the result of the cell reference.
func (c CellRef) Eval(ctx Context, ev Evaluator) Result {
return ctx.Cell(c.s, ev)
}

func (c CellRef) Reference(ctx Context, ev Evaluator) Reference {
return Reference{Type: ReferenceTypeCell, Value: c.s}
}
15 changes: 15 additions & 0 deletions spreadsheet/formula/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright 2017 Baliance. 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 by contacting [email protected].

package formula

// Context is a formula execution context. Formula evaluation uses the context
// to retreive information from sheets.
type Context interface {
// Cell returns the result of evaluating a cell.
Cell(ref string, ev Evaluator) Result
}
24 changes: 24 additions & 0 deletions spreadsheet/formula/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2017 Baliance. 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 by contacting [email protected].

package formula

type Error struct {
s string
}

func NewError(v string) Expression {
return Error{v}
}

func (e Error) Eval(ctx Context, ev Evaluator) Result {
return MakeErrorResult(e.s)
}

func (e Error) Reference(ctx Context, ev Evaluator) Reference {
return ReferenceInvalid
}
Loading

0 comments on commit 17a365e

Please sign in to comment.