forked from unidoc/unioffice
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
spreadsheet: start adding support for formula evaluation
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
Showing
36 changed files
with
4,213 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,3 +11,4 @@ coverage* | |
|
||
**/.coverprofile | ||
gover.coverprofile | ||
spreadsheet/formula/y.output |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.