Spreadsheet formulas: Flattening files (#366)

* evaluation errors fixed
* allow underscores in sheet names
* allow wildcard in *LOOKUP functions
* horizontal and vertical ranges
* horizontal and vertical ranges for sheet references
* remove redundant recalculate
* caching
* fixed named ranges, made Evaluator more generic
* Memory usage shown in flatten example
* temporary file deleted
* ParseCellReference duplicate removed
* ISREF is fixed
This commit is contained in:
Vyacheslav Zgordan 2020-01-29 13:43:43 +03:00 committed by GitHub
parent 2cfb3539c7
commit a5ef37c0bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 3689 additions and 2705 deletions

Binary file not shown.

View File

@ -0,0 +1,79 @@
// 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"
"fmt"
"runtime"
"time"
"github.com/unidoc/unioffice/spreadsheet"
"github.com/unidoc/unioffice/spreadsheet/formula"
)
func main() {
ss, err := spreadsheet.Open("formulas.xlsx")
if err != nil {
log.Fatalf("error opening document: %s", err)
}
sheets := ss.Sheets()
start := time.Now().UnixNano()
formEv := formula.NewEvaluator()
for _, sheet := range sheets {
fmt.Println("Sheet name:", sheet.Name())
ctx := sheet.FormulaContext()
for _, row := range sheet.Rows() {
for _, cell := range row.Cells() {
c := ctx.Cell(cell.Reference(), formEv)
value := ""
if cell.X().V != nil {
value = *cell.X().V
}
cell.Clear()
setValue(cell, c, value)
}
}
}
finish := time.Now().UnixNano()
fmt.Printf("total time: %d ns\n", finish - start)
PrintMemUsage()
ss.SaveToFile("values.xlsx")
}
func setValue(cell spreadsheet.Cell, c formula.Result, value string) {
switch c.Type {
case formula.ResultTypeNumber:
if c.IsBoolean {
cell.SetBool(value != "0")
} else {
cell.SetNumber(c.ValueNumber)
}
case formula.ResultTypeString:
cell.SetString(c.ValueString)
case formula.ResultTypeList:
setValue(cell, c.ValueList[0], value)
case formula.ResultTypeArray:
setValue(cell, c.ValueArray[0][0], value)
case formula.ResultTypeError:
cell.SetError(c.ValueString)
}
}
func PrintMemUsage() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
// For info on each, see: https://golang.org/pkg/runtime/#MemStats
fmt.Println("Memory usage:")
fmt.Printf("Alloc = %v MiB", bToMb(m.Alloc))
fmt.Printf("\tTotalAlloc = %v MiB", bToMb(m.TotalAlloc))
fmt.Printf("\tSys = %v MiB", bToMb(m.Sys))
fmt.Printf("\tNumGC = %v\n", m.NumGC)
}
func bToMb(b uint64) uint64 {
return b / 1024 / 1024
}

View File

@ -161,7 +161,7 @@ func AbsoluteFilename(dt DocType, typ string, index int) string {
// SML
case WorksheetType, WorksheetTypeStrict, WorksheetContentType:
return fmt.Sprintf("xl/worksheets/sheet%d.xml", index)
case SharedStingsType, SharedStingsTypeStrict, SharedStringsContentType:
case SharedStringsType, SharedStringsTypeStrict, SharedStringsContentType:
return "xl/sharedStrings.xml"
// WML

View File

@ -58,7 +58,7 @@ func TestSMLFilenames(t *testing.T) {
{2, unioffice.CommentsType, "xl/comments2.xml"},
{15, unioffice.WorksheetType, "xl/worksheets/sheet15.xml"},
{2, unioffice.VMLDrawingType, "xl/drawings/vmlDrawing2.vml"},
{0, unioffice.SharedStingsType, "xl/sharedStrings.xml"},
{0, unioffice.SharedStringsType, "xl/sharedStrings.xml"},
{1, unioffice.ThemeType, "xl/theme/theme1.xml"},
{2, unioffice.ImageType, "xl/media/image2.png"},
}

View File

@ -23,9 +23,11 @@ const (
CustomXMLTypeStrict = "http://purl.oclc.org/ooxml/officeDocument/relationships/customXml"
// SML strict
WorksheetTypeStrict = "http://purl.oclc.org/ooxml/officeDocument/relationships/worksheet"
SharedStingsTypeStrict = "http://purl.oclc.org/ooxml/officeDocument/relationships/sharedStrings"
TableTypeStrict = "http://purl.oclc.org/ooxml/officeDocument/relationships/table"
WorksheetTypeStrict = "http://purl.oclc.org/ooxml/officeDocument/relationships/worksheet"
SharedStringsTypeStrict = "http://purl.oclc.org/ooxml/officeDocument/relationships/sharedStrings"
// Deprecated: Renamed to SharedStringsTypeStrict, will be removed in next major version.
SharedStingsTypeStrict = SharedStringsTypeStrict
TableTypeStrict = "http://purl.oclc.org/ooxml/officeDocument/relationships/table"
// WML strict
HeaderTypeStrict = "http://purl.oclc.org/ooxml/officeDocument/relationships/header"
@ -65,7 +67,9 @@ const (
// SML
WorksheetType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"
WorksheetContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"
SharedStingsType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"
SharedStringsType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"
// Deprecated: Renamed to SharedStringsType, will be removed in next major version.
SharedStingsType = SharedStringsType
SharedStringsContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"
SMLStyleSheetContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"
TableType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table"

View File

@ -142,6 +142,7 @@ func (c Cell) SetFormulaShared(formula string, rows, cols uint32) error {
// returning an ID from the shared strings table. To reuse a string, call
// SetStringByID with the ID returned.
func (c Cell) SetString(s string) int {
c.w.ensureSharedStringsRelationships()
c.clearValue()
id := c.w.SharedStrings.AddString(s)
c.x.V = unioffice.String(strconv.Itoa(id))
@ -152,6 +153,7 @@ func (c Cell) SetString(s string) int {
// SetStringByID sets the cell type to string, and the value a string in the
// shared strings table.
func (c Cell) SetStringByID(id int) {
c.w.ensureSharedStringsRelationships()
c.clearValue()
c.x.V = unioffice.String(strconv.Itoa(id))
c.x.TAttr = sml.ST_CellTypeS
@ -293,6 +295,13 @@ func (c Cell) SetBool(v bool) {
c.x.TAttr = sml.ST_CellTypeB
}
// SetError sets the cell type to error and the value to the given error message.
func (c Cell) SetError(msg string) {
c.clearValue()
c.x.V = unioffice.String(msg)
c.x.TAttr = sml.ST_CellTypeE
}
// GetValueAsBool retrieves the cell's value as a boolean
func (c Cell) GetValueAsBool() (bool, error) {
if c.x.TAttr != sml.ST_CellTypeB {
@ -539,6 +548,11 @@ func (c Cell) IsBool() bool {
return c.x.TAttr == sml.ST_CellTypeB
}
// IsError returns true if the cell is an error type cell.
func (c Cell) IsError() bool {
return c.x.TAttr == sml.ST_CellTypeE
}
// HasFormula returns true if the cell has an asoociated formula.
func (c Cell) HasFormula() bool {
return c.x.F != nil

View File

@ -10,6 +10,9 @@ package spreadsheet
import (
"fmt"
"time"
"regexp"
"strconv"
"strings"
"github.com/unidoc/unioffice/spreadsheet/formula"
"github.com/unidoc/unioffice/spreadsheet/reference"
@ -26,6 +29,13 @@ type evalContext struct {
}
func (e *evalContext) Cell(ref string, ev formula.Evaluator) formula.Result {
if !validateRef(ref) {
return formula.MakeErrorResultType(formula.ErrorTypeName, "")
}
fullRef := e.s.Name() + "!" + ref
if cached, found := ev.GetFromCache(fullRef); found {
return cached
}
cr, err := reference.ParseCellReference(ref)
if err != nil {
return formula.MakeErrorResult(fmt.Sprintf("error parsing %s: %s", ref, err))
@ -52,21 +62,37 @@ func (e *evalContext) Cell(ref string, ev formula.Evaluator) formula.Result {
e.evaluating[ref] = struct{}{}
res := ev.Eval(e, c.GetFormula())
delete(e.evaluating, ref)
ev.SetCache(fullRef, res)
return res
}
if c.IsEmpty() {
return formula.MakeEmptyResult()
res := formula.MakeEmptyResult()
ev.SetCache(fullRef, res)
return res
} else if c.IsNumber() {
v, _ := c.GetValueAsNumber()
return formula.MakeNumberResult(v)
res := formula.MakeNumberResult(v)
ev.SetCache(fullRef, res)
return res
} else if c.IsBool() {
v, _ := c.GetValueAsBool()
return formula.MakeBoolResult(v)
res := formula.MakeBoolResult(v)
ev.SetCache(fullRef, res)
return res
}
v, _ := c.GetRawValue()
return formula.MakeStringResult(v)
if c.IsError() {
errRes := formula.MakeErrorResult("")
errRes.ValueString = v
ev.SetCache(fullRef, errRes)
return errRes
}
res := formula.MakeStringResult(v)
ev.SetCache(fullRef, res)
return res
}
func (e *evalContext) Sheet(name string) formula.Context {
@ -86,7 +112,7 @@ func (e *evalContext) NamedRange(ref string) formula.Reference {
}
for _, tbl := range e.s.w.Tables() {
if tbl.Name() == ref {
return formula.MakeRangeReference(tbl.Reference())
return formula.MakeRangeReference(fmt.Sprintf("%s!%s", e.s.Name(), tbl.Reference()))
}
}
return formula.ReferenceInvalid
@ -173,3 +199,47 @@ func (e *evalContext) IsDBCS() bool {
}
return false
}
// LastColumn returns the name of last column which contains data in range of context sheet's given rows.
func (e *evalContext) LastColumn(rowFrom, rowTo int) string {
sheet := e.s
max := 1
for row := rowFrom; row <= rowTo; row++ {
l := len(sheet.Row(uint32(row)).Cells())
if l > max {
max = l
}
}
return reference.IndexToColumn(uint32(max-1))
}
// LastRow returns the name of last row which contains data in range of context sheet's given columns.
func (e *evalContext) LastRow(col string) int {
sheet := e.s
colIdx := int(reference.ColumnToIndex(col))
max := 1
for _, r := range sheet.x.SheetData.Row {
if r.RAttr != nil {
row := Row{sheet.w, sheet.x, r}
l := len(row.Cells())
if l > colIdx {
max = int(row.RowNumber())
}
}
}
return max
}
var refRegexp *regexp.Regexp = regexp.MustCompile(`^([a-z]+)([0-9]+)$`)
func validateRef(cr string) bool {
if submatch := refRegexp.FindStringSubmatch(strings.ToLower(cr)); len(submatch) > 2 {
col := submatch[1]
row, err := strconv.Atoi(submatch[2])
if err != nil { // for the case if the row number is bigger then int capacity
return false
}
return row <= 1048576 && col <= "zz"
}
return false
}

View File

@ -44,10 +44,23 @@ func NewBinaryExpr(lhs Expression, op BinOpType, rhs Expression) Expression {
return BinaryExpr{lhs, rhs, op}
}
func equalsToEmpty(res Result) bool {
if res.Type == ResultTypeString {
return res.ValueString == ""
}
return res.ValueNumber == 0
}
// Eval evaluates the binary expression using the context given.
func (b BinaryExpr) Eval(ctx Context, ev Evaluator) Result {
lhs := b.lhs.Eval(ctx, ev)
if lhs.Type == ResultTypeError {
return lhs
}
rhs := b.rhs.Eval(ctx, ev)
if rhs.Type == ResultTypeError {
return rhs
}
// peel off array/list ops first
if lhs.Type == rhs.Type {
@ -112,6 +125,17 @@ func (b BinaryExpr) Eval(ctx Context, ev Evaluator) Result {
if lhs.Type == ResultTypeString {
return MakeBoolResult(lhs.ValueString < rhs.ValueString)
}
if lhs.Type == ResultTypeEmpty { // empty always equals to empty but always less than string or number
return MakeBoolResult(false)
}
} else if lhs.Type == ResultTypeString && rhs.Type == ResultTypeNumber { // when comparing string to number, string is always greater than number
return MakeBoolResult(false)
} else if lhs.Type == ResultTypeNumber && rhs.Type == ResultTypeString {
return MakeBoolResult(true)
} else if lhs.Type == ResultTypeEmpty && (rhs.Type == ResultTypeNumber || rhs.Type == ResultTypeString) {
return MakeBoolResult(true)
} else if (lhs.Type == ResultTypeNumber || lhs.Type == ResultTypeString) && rhs.Type == ResultTypeEmpty {
return MakeBoolResult(false)
}
case BinOpTypeGT:
if lhs.Type == rhs.Type {
@ -121,6 +145,17 @@ func (b BinaryExpr) Eval(ctx Context, ev Evaluator) Result {
if lhs.Type == ResultTypeString {
return MakeBoolResult(lhs.ValueString > rhs.ValueString)
}
if lhs.Type == ResultTypeEmpty {
return MakeBoolResult(false)
}
} else if lhs.Type == ResultTypeString && rhs.Type == ResultTypeNumber {
return MakeBoolResult(true)
} else if lhs.Type == ResultTypeNumber && rhs.Type == ResultTypeString {
return MakeBoolResult(false)
} else if lhs.Type == ResultTypeEmpty && (rhs.Type == ResultTypeNumber || rhs.Type == ResultTypeString) {
return MakeBoolResult(false)
} else if (lhs.Type == ResultTypeNumber || lhs.Type == ResultTypeString) && rhs.Type == ResultTypeEmpty {
return MakeBoolResult(true)
}
case BinOpTypeEQ:
if lhs.Type == rhs.Type {
@ -131,6 +166,15 @@ func (b BinaryExpr) Eval(ctx Context, ev Evaluator) Result {
if lhs.Type == ResultTypeString {
return MakeBoolResult(lhs.ValueString == rhs.ValueString)
}
if lhs.Type == ResultTypeEmpty {
return MakeBoolResult(true)
}
} else if (lhs.Type == ResultTypeString && rhs.Type == ResultTypeNumber) || (lhs.Type == ResultTypeNumber && rhs.Type == ResultTypeString) { // string never equals to number
return MakeBoolResult(false)
} else if lhs.Type == ResultTypeEmpty && (rhs.Type == ResultTypeNumber || rhs.Type == ResultTypeString) {
return MakeBoolResult(equalsToEmpty(rhs))
} else if (lhs.Type == ResultTypeNumber || lhs.Type == ResultTypeString) && rhs.Type == ResultTypeEmpty {
return MakeBoolResult(equalsToEmpty(lhs))
}
case BinOpTypeNE:
if lhs.Type == rhs.Type {
@ -140,6 +184,15 @@ func (b BinaryExpr) Eval(ctx Context, ev Evaluator) Result {
if lhs.Type == ResultTypeString {
return MakeBoolResult(lhs.ValueString != rhs.ValueString)
}
if lhs.Type == ResultTypeEmpty {
return MakeBoolResult(false)
}
} else if (lhs.Type == ResultTypeString && rhs.Type == ResultTypeNumber) || (lhs.Type == ResultTypeNumber && rhs.Type == ResultTypeString) {
return MakeBoolResult(true)
} else if lhs.Type == ResultTypeEmpty && (rhs.Type == ResultTypeNumber || rhs.Type == ResultTypeString) {
return MakeBoolResult(!equalsToEmpty(rhs))
} else if (lhs.Type == ResultTypeNumber || lhs.Type == ResultTypeString) && rhs.Type == ResultTypeEmpty {
return MakeBoolResult(!equalsToEmpty(lhs))
}
case BinOpTypeLEQ:
if lhs.Type == rhs.Type {
@ -149,6 +202,17 @@ func (b BinaryExpr) Eval(ctx Context, ev Evaluator) Result {
if lhs.Type == ResultTypeString {
return MakeBoolResult(lhs.ValueString <= rhs.ValueString)
}
if lhs.Type == ResultTypeEmpty {
return MakeBoolResult(true)
}
} else if lhs.Type == ResultTypeString && rhs.Type == ResultTypeNumber {
return MakeBoolResult(false)
} else if lhs.Type == ResultTypeNumber && rhs.Type == ResultTypeString {
return MakeBoolResult(true)
} else if lhs.Type == ResultTypeEmpty && (rhs.Type == ResultTypeNumber || rhs.Type == ResultTypeString) {
return MakeBoolResult(equalsToEmpty(rhs))
} else if (lhs.Type == ResultTypeNumber || lhs.Type == ResultTypeString) && rhs.Type == ResultTypeEmpty {
return MakeBoolResult(equalsToEmpty(lhs))
}
case BinOpTypeGEQ:
if lhs.Type == rhs.Type {
@ -158,6 +222,17 @@ func (b BinaryExpr) Eval(ctx Context, ev Evaluator) Result {
if lhs.Type == ResultTypeString {
return MakeBoolResult(lhs.ValueString >= rhs.ValueString)
}
if lhs.Type == ResultTypeEmpty {
return MakeBoolResult(true)
}
} else if lhs.Type == ResultTypeString && rhs.Type == ResultTypeNumber {
return MakeBoolResult(true)
} else if lhs.Type == ResultTypeNumber && rhs.Type == ResultTypeString {
return MakeBoolResult(false)
} else if lhs.Type == ResultTypeEmpty && (rhs.Type == ResultTypeNumber || rhs.Type == ResultTypeString) {
return MakeBoolResult(equalsToEmpty(rhs))
} else if (lhs.Type == ResultTypeNumber || lhs.Type == ResultTypeString) && rhs.Type == ResultTypeEmpty {
return MakeBoolResult(equalsToEmpty(lhs))
}
case BinOpTypeConcat:
return MakeStringResult(lhs.Value() + rhs.Value())
@ -166,6 +241,7 @@ func (b BinaryExpr) Eval(ctx Context, ev Evaluator) Result {
return MakeErrorResult("unsupported binary op")
}
// Reference returns an invalid reference for BinaryExpr.
func (b BinaryExpr) Reference(ctx Context, ev Evaluator) Reference {
return ReferenceInvalid
}

View File

@ -13,10 +13,12 @@ import (
"github.com/unidoc/unioffice"
)
// Bool is a boolean expression.
type Bool struct {
b bool
}
// NewBool constructs a new boolean expression.
func NewBool(v string) Expression {
b, err := strconv.ParseBool(v)
if err != nil {
@ -25,10 +27,12 @@ func NewBool(v string) Expression {
return Bool{b}
}
// Eval evaluates and returns a boolean.
func (b Bool) Eval(ctx Context, ev Evaluator) Result {
return MakeBoolResult(b.b)
}
// Reference returns an invalid reference for Bool.
func (b Bool) Reference(ctx Context, ev Evaluator) Reference {
return ReferenceInvalid
}

View File

@ -22,6 +22,7 @@ func (c CellRef) Eval(ctx Context, ev Evaluator) Result {
return ctx.Cell(c.s, ev)
}
// Reference returns a string reference value to a cell.
func (c CellRef) Reference(ctx Context, ev Evaluator) Reference {
return Reference{Type: ReferenceTypeCell, Value: c.s}
}

View File

@ -7,14 +7,17 @@
package formula
// ConstArrayExpr is a constant array expression.
type ConstArrayExpr struct {
data [][]Expression
}
// NewConstArrayExpr constructs a new constant array expression with a given data.
func NewConstArrayExpr(data [][]Expression) Expression {
return &ConstArrayExpr{data}
}
// Eval evaluates and returns the result of a constant array expression.
func (c ConstArrayExpr) Eval(ctx Context, ev Evaluator) Result {
res := [][]Result{}
for _, row := range c.data {
@ -27,6 +30,7 @@ func (c ConstArrayExpr) Eval(ctx Context, ev Evaluator) Result {
return MakeArrayResult(res)
}
// Reference returns an invalid reference for ConstArrayExpr.
func (c ConstArrayExpr) Reference(ctx Context, ev Evaluator) Reference {
return ReferenceInvalid
}

View File

@ -46,6 +46,12 @@ type Context interface {
// IsDBCS returns if workbook default language is among DBCS.
IsDBCS() bool
// LastColumn returns the name of last column which contains data in range of context sheet's given rows.
LastColumn(rowFrom, rowTo int) string
// LastRow returns the name of last row which contains data in range of context sheet's given columns.
LastRow(colFrom string) int
// SetLocked returns sets cell's protected attribute.
SetLocked(cellRef string, locked bool)

View File

@ -0,0 +1,54 @@
// 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 "fmt"
// defEval is the default formula evaluator which implements the Evaluator interface.
type defEval struct {
evCache
lastEvalIsRef bool
}
// Eval evaluates and returns the result of a formula.
func (d *defEval) Eval(ctx Context, formula string) Result {
expr := ParseString(formula)
if expr != nil {
d.checkLastEvalIsRef(ctx, expr)
result := expr.Eval(ctx, d)
if result.Type == ResultTypeError {
d.lastEvalIsRef = false
}
return result
}
return MakeErrorResult(fmt.Sprintf("unable to parse formula %s", formula))
}
// LastEvalIsRef returns if last evaluation with the evaluator was a reference.
func (d *defEval) LastEvalIsRef() bool {
return d.lastEvalIsRef
}
// checkLastEvalIsRef adds information which is needed for some functions but is lost after evaluation. E.g. which arguments are actually references.
func (d *defEval) checkLastEvalIsRef(ctx Context, expr Expression) {
switch expr.(type) {
case FunctionCall:
switch expr.(FunctionCall).name {
case "ISREF":
for _, arg := range expr.(FunctionCall).args {
switch arg.(type) {
case CellRef, Range, HorizontalRange, VerticalRange, NamedRangeRef, PrefixExpr, PrefixRangeExpr, PrefixHorizontalRange, PrefixVerticalRange:
evResult := arg.Eval(ctx, d)
d.lastEvalIsRef = !(evResult.Type == ResultTypeError && evResult.ValueString == "#NAME?")
default:
d.lastEvalIsRef = false
}
}
}
}
}

View File

@ -7,17 +7,21 @@
package formula
// EmptyExpr is an empty expression.
type EmptyExpr struct {
}
// NewEmptyExpr constructs a new empty expression.
func NewEmptyExpr() Expression {
return EmptyExpr{}
}
// Eval evaluates and returns the result of an empty expression.
func (e EmptyExpr) Eval(ctx Context, ev Evaluator) Result {
return MakeEmptyResult()
}
// Reference returns an invalid reference for EmptyExpr.
func (e EmptyExpr) Reference(ctx Context, ev Evaluator) Reference {
return ReferenceInvalid
}

View File

@ -7,18 +7,22 @@
package formula
// Error is an error expression.
type Error struct {
s string
}
// NewError constructs a new error expression from a string.
func NewError(v string) Expression {
return Error{v}
}
// Eval evaluates and returns the result of an error expression.
func (e Error) Eval(ctx Context, ev Evaluator) Result {
return MakeErrorResult(e.s)
}
// Reference returns an invalid reference for Error.
func (e Error) Reference(ctx Context, ev Evaluator) Reference {
return ReferenceInvalid
}

View File

@ -7,75 +7,20 @@
package formula
import (
"fmt"
"regexp"
"strconv"
"strings"
)
// Evaluator is the interface for a formula evaluator. This is needed so we can
// pass it to the spreadsheet to let it evaluate formula cells before returning
// the results.
// NOTE: in order to implement Evaluator without cache embed noCache in it.
type Evaluator interface {
Eval(ctx Context, formula string) Result
SetCache(key string, value Result)
GetFromCache(key string) (Result, bool)
LastEvalIsRef() bool
}
// NewEvaluator constructs a new defEval object which is the default formula evaluator.
func NewEvaluator() Evaluator {
return &defEval{}
}
type defEval struct {
isRef bool
}
func (d *defEval) Eval(ctx Context, formula string) Result {
expr := ParseString(formula)
if expr != nil {
d.addInfo(ctx, expr)
return expr.Eval(ctx, d)
}
return MakeErrorResult(fmt.Sprintf("unable to parse formula %s", formula))
}
// addInfo adds information which is needed for some functions but is lost after evaluation. E.g. which zeroes and ones were actually booleans before evaluation and which arguments are actually references.
func (d *defEval) addInfo(ctx Context, expr Expression) {
switch expr.(type) {
case FunctionCall:
switch expr.(FunctionCall).name {
case "ISREF":
for _, arg := range expr.(FunctionCall).args {
switch arg.(type) {
case CellRef:
d.isRef = validateRef(arg.(CellRef))
return
case Range:
switch arg.(Range).from.(type) {
case CellRef:
d.isRef = validateRef(arg.(Range).from.(CellRef))
return
}
switch arg.(Range).to.(type) {
case CellRef:
d.isRef = validateRef(arg.(Range).to.(CellRef))
return
}
}
}
}
}
}
var refRegexp *regexp.Regexp = regexp.MustCompile(`^([a-z]+)([0-9]+)$`)
func validateRef(cr CellRef) bool {
if submatch := refRegexp.FindStringSubmatch(strings.ToLower(cr.s)); len(submatch) > 2 {
col := submatch[1]
row, err := strconv.Atoi(submatch[2])
if err != nil { // for the case if the row number is bigger then int capacity
return false
}
return row <= 1048576 && col <= "zz"
}
return false
ev := &defEval{}
ev.evCache = newEvCache()
return ev
}

View File

@ -0,0 +1,36 @@
// 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 "sync"
// evCache is a struct with collection of caching methods intended for add cache support to evaluators.
type evCache struct {
cache map[string]Result
lock *sync.Mutex
}
func newEvCache() evCache {
ev := evCache{}
ev.cache = make(map[string]Result)
ev.lock = &sync.Mutex{}
return ev
}
func (ec *evCache) SetCache(key string, value Result) {
ec.lock.Lock()
ec.cache[key] = value
ec.lock.Unlock()
}
func (ec *evCache) GetFromCache(key string) (Result, bool) {
ec.lock.Lock()
result, found := ec.cache[key]
ec.lock.Unlock()
return result, found
}

View File

@ -771,7 +771,7 @@ func YearFrac(args []Result) Result {
basis := 0
if argsNum == 3 && args[2].Type != ResultTypeEmpty {
if args[2].Type != ResultTypeNumber {
return MakeErrorResult("YEARFRAC requires two or three number arguments")
return MakeErrorResult("YEARFRAC requires basis argument to be a number")
}
basis = int(args[2].ValueNumber)
if !checkBasis(basis) {

View File

@ -293,10 +293,11 @@ func Offset(ctx Context, ev Evaluator, args []Result) Result {
return MakeErrorResult(fmt.Sprintf("Invalid range in OFFSET(): %s", ref.Type))
}
col, rowIdx, err := ParseCellReference(origin)
if err != nil {
return MakeErrorResult(fmt.Sprintf("parse origin error OFFSET(): %s", err))
parsedRef, parseErr := reference.ParseCellReference(origin)
if parseErr != nil {
return MakeErrorResult(fmt.Sprintf("parse origin error OFFSET(): %s", parseErr.Error()))
}
col, rowIdx, sheetName := parsedRef.Column, parsedRef.RowIdx, parsedRef.SheetName
rOff := args[1].AsNumber()
if rOff.Type != ResultTypeNumber {
@ -348,7 +349,11 @@ func Offset(ctx Context, ev Evaluator, args []Result) Result {
beg := fmt.Sprintf("%s%d", reference.IndexToColumn(origCol), origRow)
end := fmt.Sprintf("%s%d", reference.IndexToColumn(endCol), endRow)
return resultFromCellRange(ctx, ev, beg, end)
if sheetName == "" {
return resultFromCellRange(ctx, ev, beg, end)
} else {
return resultFromCellRange(ctx.Sheet(sheetName), ev, beg, end)
}
}
// VLookup implements the VLOOKUP function that returns a matching value from a
@ -392,7 +397,7 @@ lfor:
continue
}
rval := row[0]
switch compareResults(rval, lookupValue, false) {
switch compareResults(rval, lookupValue, false, exactMatch) {
case cmpResultLess:
// less than
matchIdx = i
@ -425,7 +430,7 @@ const (
cmpResultInvalid cmpResult = 2
)
func compareResults(lhs, rhs Result, caseSensitive bool) cmpResult {
func compareResults(lhs, rhs Result, caseSensitive, exactMatch bool) cmpResult {
lhs = lhs.AsNumber()
rhs = rhs.AsNumber()
// differing types
@ -446,11 +451,21 @@ func compareResults(lhs, rhs Result, caseSensitive bool) cmpResult {
// both strings
if lhs.Type == ResultTypeString {
ls := lhs.ValueString
rs := rhs.ValueString
if !caseSensitive {
return cmpResult(strings.Compare(strings.ToLower(lhs.ValueString),
strings.ToLower(rhs.ValueString)))
ls = strings.ToLower(ls)
rs = strings.ToLower(rs)
}
return cmpResult(strings.Compare(lhs.ValueString, rhs.ValueString))
if exactMatch {
match := wildcard.Match(rs, ls)
if match {
return cmpResultEqual
} else {
return cmpResultGreater
}
}
return cmpResult(strings.Compare(ls, rs))
}
// empty cells are equal
@ -467,7 +482,7 @@ func compareResults(lhs, rhs Result, caseSensitive bool) cmpResult {
return cmpResultGreater
}
for i := range lhs.ValueList {
cmp := compareResults(lhs.ValueList[i], rhs.ValueList[i], caseSensitive)
cmp := compareResults(lhs.ValueList[i], rhs.ValueList[i], caseSensitive, exactMatch)
if cmp != cmpResultEqual {
return cmp
}
@ -493,7 +508,7 @@ func compareResults(lhs, rhs Result, caseSensitive bool) cmpResult {
return cmpResultGreater
}
for c := range lrow {
cmp := compareResults(lrow[c], rrow[c], caseSensitive)
cmp := compareResults(lrow[c], rrow[c], caseSensitive, exactMatch)
if cmp != cmpResultEqual {
return cmp
}
@ -523,7 +538,7 @@ func Lookup(args []Result) Result {
idx := -1
for i, v := range col {
if compareResults(lookupValue, v, false) == cmpResultEqual {
if compareResults(lookupValue, v, false, false) == cmpResultEqual {
idx = i
}
}
@ -592,7 +607,7 @@ func HLookup(args []Result) Result {
row := arr.ValueArray[0]
lfor:
for i, val := range row {
switch compareResults(val, lookupValue, false) {
switch compareResults(val, lookupValue, false, exactMatch) {
case cmpResultLess:
// less than
matchIdx = i

View File

@ -88,6 +88,9 @@ func Cell(ctx Context, ev Evaluator, args []Result) Result {
return MakeErrorResult("Incorrect reference: " + refStr)
}
address := "$"+cr.Column+"$"+strconv.Itoa(int(cr.RowIdx))
if cr.SheetName != "" {
address = cr.SheetName + "!" + address
}
return MakeStringResult(address)
case "col":
cr, err := reference.ParseCellReference(refStr)
@ -189,7 +192,11 @@ func Cell(ctx Context, ev Evaluator, args []Result) Result {
if err != nil {
return MakeErrorResult("Incorrect reference: " + refStr)
}
return MakeNumberResult(ctx.GetWidth(int(cr.ColumnIdx)))
if cr.SheetName == "" {
return MakeNumberResult(ctx.GetWidth(int(cr.ColumnIdx)))
} else {
return MakeNumberResult(ctx.Sheet(cr.SheetName).GetWidth(int(cr.ColumnIdx)))
}
}
return MakeErrorResult("Incorrect first argument of CELL: "+typ.ValueString)
}
@ -295,7 +302,7 @@ func IsLogical(ctx Context, ev Evaluator, args []Result) Result {
return MakeErrorResult("ISLOGICAL requires the first argument to be of type reference")
}
return MakeBoolResult(ctx.IsBool(ref.Value))
return MakeBoolResult(ctx.Cell(ref.Value, ev).IsBoolean)
}
// IsNA is an implementation of the Excel ISNA() function.
@ -342,7 +349,7 @@ func IsRef(ctx Context, ev Evaluator, args []Result) Result {
if len(args) != 1 {
MakeErrorResult("ISREF() accepts a single argument")
}
return MakeBoolResult(ev.(*defEval).isRef)
return MakeBoolResult(ev.LastEvalIsRef())
}
// ISTEXT is an implementation of the Excel ISTEXT() function.

View File

@ -19,7 +19,6 @@ func init() {
RegisterFunction("OR", Or)
RegisterFunction("TRUE", True)
RegisterFunction("_xlfn.XOR", Xor)
}
// And is an implementation of the Excel AND() function.
@ -91,36 +90,63 @@ func If(args []Result) Result {
// false case
if len(args) == 3 {
return args[2]
} else {
return MakeBoolResult(false)
}
case ResultTypeList:
return MakeListResult(ifList(args))
return ifList(args)
case ResultTypeArray:
return ifArray(args)
default:
return MakeErrorResult("IF initial argument must be numeric or array")
}
return MakeBoolResult(false)
}
func fillArray(arg Result, rows, cols int) [][]Result {
array := [][]Result{}
result := [][]Result{}
switch arg.Type {
case ResultTypeArray:
return arg.ValueArray
case ResultTypeNumber, ResultTypeString:
for r := 0; r < rows; r++ {
list := []Result{}
for c := 0; c < cols; c++ {
list = append(list, arg)
for ir, row := range arg.ValueArray {
if ir < rows {
result = append(result, fillList(MakeListResult(row), cols))
} else {
result = append(result, fillList(MakeErrorResultType(ErrorTypeNA, ""), cols))
}
array = append(array, list)
}
return array
case ResultTypeList:
return [][]Result{arg.ValueList}
list := fillList(arg, cols)
for r := 0; r < rows; r++ {
result = append(result, list)
}
case ResultTypeNumber, ResultTypeString, ResultTypeError, ResultTypeEmpty:
for r := 0; r < rows; r++ {
list := fillList(arg, cols)
result = append(result, list)
}
}
return [][]Result{}
return result
}
func fillList(arg Result, cols int) []Result {
list := []Result{}
switch arg.Type {
case ResultTypeList:
valueList := arg.ValueList
maxCols := len(valueList)
for ic := 0; ic < cols; ic++ {
if ic < maxCols {
list = append(list, valueList[ic])
} else {
list = append(list, MakeErrorResultType(ErrorTypeNA, ""))
}
}
case ResultTypeNumber, ResultTypeString, ResultTypeError, ResultTypeEmpty:
for ic := 0; ic < cols; ic++ {
list = append(list, arg)
}
}
return list
}
func ifArray(args []Result) Result {
@ -129,7 +155,7 @@ func ifArray(args []Result) Result {
if len(args) == 1 {
result := [][]Result{}
for _, v := range condArray {
result = append(result, ifList([]Result{MakeListResult(v)}))
result = append(result, ifList([]Result{MakeListResult(v)}).ValueList)
}
return MakeArrayResult(result)
} else if len(args) == 2 {
@ -143,12 +169,9 @@ func ifArray(args []Result) Result {
if i < tl {
truesList = truesArray[i]
} else {
truesList = []Result{}
for j := 0; j < cols; j++ {
truesList = append(truesList, MakeErrorResultType(ErrorTypeValue, ""))
}
truesList = fillList(MakeErrorResultType(ErrorTypeNA, ""), cols)
}
result = append(result, ifList([]Result{MakeListResult(v), MakeListResult(truesList)}))
result = append(result, ifList([]Result{MakeListResult(v), MakeListResult(truesList)}).ValueList)
}
return MakeArrayResult(result)
} else if len(args) == 3 {
@ -164,85 +187,141 @@ func ifArray(args []Result) Result {
if i < tl {
truesList = truesArray[i]
} else {
truesList = []Result{}
for j := 0; j < cols; j++ {
truesList = append(truesList, MakeErrorResultType(ErrorTypeValue, ""))
}
truesList = fillList(MakeErrorResultType(ErrorTypeNA, ""), cols)
}
if i < fl {
falsesList = falsesArray[i]
} else {
falsesList = []Result{}
for j := 0; j < cols; j++ {
falsesList = append(falsesList, MakeErrorResultType(ErrorTypeValue, ""))
}
falsesList = fillList(MakeErrorResultType(ErrorTypeNA, ""), cols)
}
result = append(result, ifList([]Result{MakeListResult(v), MakeListResult(truesList), MakeListResult(falsesList)}))
result = append(result, ifList([]Result{MakeListResult(v), MakeListResult(truesList), MakeListResult(falsesList)}).ValueList)
}
return MakeArrayResult(result)
}
return MakeErrorResultType(ErrorTypeValue, "")
}
func ifList(args []Result) []Result {
func ifList(args []Result) Result {
condList := args[0].ValueList
cols := len(condList)
switch len(args) {
// single argument returns list of contitions
if len(args) == 1 {
case 1:
result := []Result{}
for _, v := range condList {
result = append(result, MakeBoolResult(v.ValueNumber != 0))
}
return result
}
return MakeListResult(result)
// two arguments case
if len(args) == 2 {
trues := args[1].ValueList
tl := len(trues)
result := []Result{}
for i, v := range condList {
var newValue Result
if v.ValueNumber == 0 {
newValue = MakeBoolResult(false)
} else {
if i < tl {
newValue = trues[i]
case 2:
trues := args[1]
switch trues.Type {
case ResultTypeNumber, ResultTypeString, ResultTypeEmpty:
result := []Result{}
for _, v := range condList {
var newValue Result
if v.ValueNumber == 0 {
newValue = MakeBoolResult(false)
} else {
newValue = MakeErrorResultType(ErrorTypeValue, "")
newValue = trues
}
result = append(result, newValue)
}
result = append(result, newValue)
return MakeListResult(result)
case ResultTypeList:
truesList := fillList(trues, cols)
result := []Result{}
for i, v := range condList {
var newValue Result
if v.ValueNumber == 0 {
newValue = MakeBoolResult(false)
} else {
newValue = truesList[i]
}
result = append(result, newValue)
}
return MakeListResult(result)
case ResultTypeArray:
truesArray := fillArray(trues, len(trues.ValueArray), cols)
result := [][]Result{}
for _, row := range truesArray {
rowResult := []Result{}
for ic, v := range condList {
var newValue Result
if v.ValueNumber == 0 {
newValue = MakeBoolResult(false)
} else {
newValue = row[ic]
}
rowResult = append(rowResult, newValue)
}
result = append(result, rowResult)
}
return MakeArrayResult(result)
}
return result
}
// false case
if len(args) == 3 {
trues := args[1].ValueList
falses := args[2].ValueList
tl := len(trues)
tf := len(falses)
result := []Result{}
for i, v := range condList {
var newValue Result
if v.ValueNumber != 0 {
if i < tl {
newValue = trues[i]
case 3:
trues := args[1]
falses := args[2]
truesSingleValue := checkSingleValue(trues)
falsesSingleValue := checkSingleValue(falses)
if truesSingleValue && falsesSingleValue {
result := []Result{}
for _, v := range condList {
var newValue Result
if v.ValueNumber == 0 {
newValue = falses
} else {
newValue = MakeErrorResultType(ErrorTypeValue, "")
}
} else {
if i < tf {
newValue = falses[i]
} else {
newValue = MakeErrorResultType(ErrorTypeValue, "")
newValue = trues
}
result = append(result, newValue)
}
result = append(result, newValue)
return MakeListResult(result)
}
return result
if trues.Type != ResultTypeArray && falses.Type != ResultTypeArray {
truesList := fillList(trues, cols)
falsesList := fillList(falses, cols)
result := []Result{}
for i, v := range condList {
var newValue Result
if v.ValueNumber == 0 {
newValue = falsesList[i]
} else {
newValue = truesList[i]
}
result = append(result, newValue)
}
return MakeListResult(result)
}
maxRows := len(trues.ValueArray)
if len(falses.ValueArray) > maxRows {
maxRows = len(falses.ValueArray)
}
truesArray := fillArray(trues, maxRows, cols)
falsesArray := fillArray(falses, maxRows, cols)
result := [][]Result{}
for ir := 0; ir < maxRows; ir++ {
rowResult := []Result{}
for ic, v := range condList {
var newValue Result
if v.ValueNumber == 0 {
newValue = falsesArray[ir][ic]
} else {
newValue = truesArray[ir][ic]
}
rowResult = append(rowResult, newValue)
}
result = append(result, rowResult)
}
return MakeArrayResult(result)
}
return []Result{}
return MakeErrorResult("")
}
func checkSingleValue(result Result) bool {
t := result.Type
return t != ResultTypeArray && t != ResultTypeList
}
// IfError is an implementation of the Excel IFERROR() function. It takes two arguments.

View File

@ -174,15 +174,21 @@ func parseSearchResults(fname string, args []Result) (*parsedSearchObject, Resul
return nil, MakeErrorResult(fname + " requires two or three arguments")
}
findTextResult := args[0]
if findTextResult.Type != ResultTypeString {
if findTextResult.Type == ResultTypeError {
return nil, findTextResult
}
if findTextResult.Type != ResultTypeString && findTextResult.Type != ResultTypeNumber {
return nil, MakeErrorResult("The first argument should be a string")
}
textResult := args[1]
if textResult.Type != ResultTypeString {
if textResult.Type == ResultTypeError {
return nil, textResult
}
if textResult.Type != ResultTypeString && textResult.Type != ResultTypeNumber {
return nil, MakeErrorResult("The second argument should be a string")
}
text := textResult.ValueString
findText := findTextResult.ValueString
text := textResult.Value()
findText := findTextResult.Value()
position := 1
if argsNum == 3 && args[2].Type != ResultTypeEmpty {
positionResult := args[2]
@ -388,10 +394,14 @@ func Mid(args []Result) Result {
if len(args) != 3 {
return MakeErrorResult("MID requires three arguments")
}
if args[0].Type != ResultTypeString {
textArg := args[0]
if textArg.Type == ResultTypeError {
return textArg
}
if textArg.Type != ResultTypeString && textArg.Type != ResultTypeNumber && textArg.Type != ResultTypeEmpty {
return MakeErrorResult("MID requires text to be a string")
}
text := args[0].ValueString
text := args[0].Value()
if args[1].Type != ResultTypeNumber {
return MakeErrorResult("MID requires start_num to be a number")
}

View File

@ -7,15 +7,18 @@
package formula
// FunctionCall is a function call expression.
type FunctionCall struct {
name string
args []Expression
}
// NewFunction constructs a new function call expression.
func NewFunction(name string, args []Expression) Expression {
return FunctionCall{name, args}
}
// Eval evaluates and returns the result of a function call.
func (f FunctionCall) Eval(ctx Context, ev Evaluator) Result {
fn := LookupFunction(f.name)
if fn != nil {
@ -39,6 +42,7 @@ func (f FunctionCall) Eval(ctx Context, ev Evaluator) Result {
return MakeErrorResult("unknown function " + f.name)
}
// Reference returns an invalid reference for FunctionCall.
func (f FunctionCall) Reference(ctx Context, ev Evaluator) Reference {
return ReferenceInvalid
}

View File

@ -920,12 +920,14 @@ func TestIsLogical(t *testing.T) {
sheet.Cell("A2").SetBool(false)
sheet.Cell("A3").SetNumber(0)
sheet.Cell("A4").SetString("FALSE")
sheet.Cell("A5").SetFormulaRaw("=1=2")
td := []testStruct{
{`=ISLOGICAL(A1)`, `1 ResultTypeNumber`},
{`=ISLOGICAL(A2)`, `1 ResultTypeNumber`},
{`=ISLOGICAL(A3)`, `0 ResultTypeNumber`},
{`=ISLOGICAL(A4)`, `0 ResultTypeNumber`},
{`=ISLOGICAL(A5)`, `1 ResultTypeNumber`},
}
ctx := sheet.FormulaContext()
@ -960,7 +962,11 @@ func TestIsRef(t *testing.T) {
sheet.Cell("A3").SetFormulaRaw("=ISREF(A1048577)")
sheet.Cell("A4").SetFormulaRaw("=ISREF(ZZA0)")
sheet.Cell("A5").SetFormulaRaw("=ISREF(ZZ0)")
sheet.Cell("A6").SetString("A1")
sheet.Cell("A6").SetFormulaRaw("=ISREF(AA:ZZ)")
sheet.Cell("A7").SetFormulaRaw("=ISREF(1:4)")
sheet.Cell("A8").SetFormulaRaw("=4/0")
sheet.Cell("A9").SetFormulaRaw("=ISREF(A7)")
sheet.Cell("A10").SetString("A1")
td := []testStruct{
{`A1`, `1 ResultTypeNumber`},
@ -968,7 +974,11 @@ func TestIsRef(t *testing.T) {
{`A3`, `0 ResultTypeNumber`},
{`A4`, `0 ResultTypeNumber`},
{`A5`, `1 ResultTypeNumber`},
{`A6`, `A1 ResultTypeString`},
{`A6`, `1 ResultTypeNumber`},
{`A7`, `1 ResultTypeNumber`},
{`A8`, `#DIV/0! ResultTypeError`},
{`A9`, `1 ResultTypeNumber`},
{`A10`, `A1 ResultTypeString`},
}
ctx := sheet.FormulaContext()

View File

@ -1,3 +1,4 @@
// Code generated by goyacc -l -o grammar.go grammar.y. DO NOT EDIT.
// Copyright 2017 FoxyUtils ehf. All rights reserved.
//
// Use of this source code is governed by the terms of the Affero GNU General
@ -18,43 +19,45 @@ type yySymType struct {
}
const tokenHorizontalRange = 57346
const tokenReservedName = 57347
const tokenDDECall = 57348
const tokenLexError = 57349
const tokenNamedRange = 57350
const tokenBool = 57351
const tokenNumber = 57352
const tokenString = 57353
const tokenError = 57354
const tokenErrorRef = 57355
const tokenSheet = 57356
const tokenCell = 57357
const tokenFunctionBuiltin = 57358
const tokenLBrace = 57359
const tokenRBrace = 57360
const tokenLParen = 57361
const tokenRParen = 57362
const tokenPlus = 57363
const tokenMinus = 57364
const tokenMult = 57365
const tokenDiv = 57366
const tokenExp = 57367
const tokenEQ = 57368
const tokenLT = 57369
const tokenGT = 57370
const tokenLEQ = 57371
const tokenGEQ = 57372
const tokenNE = 57373
const tokenColon = 57374
const tokenComma = 57375
const tokenAmpersand = 57376
const tokenSemi = 57377
const tokenVerticalRange = 57347
const tokenReservedName = 57348
const tokenDDECall = 57349
const tokenLexError = 57350
const tokenNamedRange = 57351
const tokenBool = 57352
const tokenNumber = 57353
const tokenString = 57354
const tokenError = 57355
const tokenErrorRef = 57356
const tokenSheet = 57357
const tokenCell = 57358
const tokenFunctionBuiltin = 57359
const tokenLBrace = 57360
const tokenRBrace = 57361
const tokenLParen = 57362
const tokenRParen = 57363
const tokenPlus = 57364
const tokenMinus = 57365
const tokenMult = 57366
const tokenDiv = 57367
const tokenExp = 57368
const tokenEQ = 57369
const tokenLT = 57370
const tokenGT = 57371
const tokenLEQ = 57372
const tokenGEQ = 57373
const tokenNE = 57374
const tokenColon = 57375
const tokenComma = 57376
const tokenAmpersand = 57377
const tokenSemi = 57378
var yyToknames = [...]string{
"$end",
"error",
"$unk",
"tokenHorizontalRange",
"tokenVerticalRange",
"tokenReservedName",
"tokenDDECall",
"tokenLexError",
@ -101,82 +104,88 @@ var yyExca = [...]int{
const yyPrivate = 57344
const yyLast = 179
const yyLast = 187
var yyAct = [...]int{
43, 3, 42, 30, 18, 38, 68, 44, 45, 28,
29, 30, 37, 46, 47, 30, 50, 41, 24, 13,
37, 19, 66, 52, 48, 23, 21, 53, 54, 55,
56, 57, 58, 59, 60, 61, 62, 63, 64, 67,
51, 65, 76, 11, 9, 1, 20, 10, 2, 8,
73, 71, 70, 26, 27, 28, 29, 30, 35, 31,
32, 33, 34, 36, 72, 0, 37, 0, 0, 75,
74, 0, 0, 77, 69, 26, 27, 28, 29, 30,
35, 31, 32, 33, 34, 36, 0, 0, 37, 26,
27, 28, 29, 30, 35, 31, 32, 33, 34, 36,
0, 0, 37, 26, 27, 28, 29, 30, 0, 0,
0, 24, 14, 15, 16, 17, 37, 25, 23, 22,
39, 0, 12, 0, 6, 7, 0, 0, 0, 40,
24, 14, 15, 16, 17, 0, 25, 23, 22, 5,
0, 12, 0, 6, 7, 0, 0, 0, 4, 24,
14, 15, 16, 17, 0, 25, 23, 22, 39, 0,
12, 49, 6, 7, 24, 14, 15, 16, 17, 0,
25, 23, 22, 39, 0, 12, 0, 6, 7,
45, 3, 44, 32, 18, 40, 72, 46, 47, 30,
31, 32, 39, 48, 28, 29, 30, 31, 32, 75,
39, 49, 32, 56, 50, 70, 23, 39, 76, 57,
58, 59, 60, 61, 62, 63, 64, 65, 66, 67,
68, 77, 71, 69, 54, 43, 13, 19, 21, 55,
82, 11, 78, 9, 74, 28, 29, 30, 31, 32,
37, 33, 34, 35, 36, 38, 1, 20, 39, 10,
2, 8, 0, 80, 79, 0, 0, 0, 83, 0,
81, 73, 28, 29, 30, 31, 32, 37, 33, 34,
35, 36, 38, 0, 0, 39, 28, 29, 30, 31,
32, 37, 33, 34, 35, 36, 38, 26, 27, 39,
51, 52, 25, 14, 15, 16, 17, 0, 24, 23,
22, 41, 23, 12, 0, 6, 7, 26, 27, 0,
42, 0, 25, 14, 15, 16, 17, 0, 24, 23,
22, 5, 0, 12, 0, 6, 7, 26, 27, 0,
4, 0, 25, 14, 15, 16, 17, 0, 24, 23,
22, 41, 0, 12, 53, 6, 7, 26, 27, 0,
0, 0, 25, 14, 15, 16, 17, 0, 24, 23,
22, 41, 0, 12, 0, 6, 7,
}
var yyPact = [...]int{
122, -1000, -1000, 68, 156, 103, 156, 156, -1000, -1000,
-1000, -1000, 156, -1000, -1000, -1000, -1000, -1000, -18, 10,
-1000, -1000, 141, -1000, -1000, -1000, 156, 156, 156, 156,
156, 156, 156, 156, 156, 156, 156, 156, 68, 156,
156, 4, -27, 68, -14, -14, 54, 10, -1000, -1000,
31, -1000, 68, -14, -14, -22, -22, -1000, 82, 82,
82, 82, 82, 82, -10, 32, -1000, 156, 156, -1000,
-1000, -1000, 156, -1000, -27, 68, -1000, 68,
123, -1000, -1000, 74, 163, 103, 163, 163, -1000, -1000,
-1000, -1000, 163, -1000, -1000, -1000, -1000, -1000, -12, 106,
-1000, -1000, 143, -1000, -1000, -1000, -1000, -1000, 163, 163,
163, 163, 163, 163, 163, 163, 163, 163, 163, 163,
74, 163, 163, 6, -28, 74, -15, -15, 60, 10,
-14, -1000, -1000, -1000, 7, -1000, 74, -15, -15, -23,
-23, -1000, -8, -8, -8, -8, -8, -8, -4, 33,
-1000, 163, 163, -1000, -1000, 10, -1000, 163, -1000, -28,
74, -1000, -1000, 74,
}
var yyPgo = [...]int{
0, 0, 49, 48, 47, 4, 46, 45, 44, 43,
42, 40, 26, 21, 19, 17, 16, 2,
0, 0, 71, 70, 69, 4, 67, 66, 53, 51,
50, 49, 48, 47, 46, 45, 44, 2,
}
var yyR1 = [...]int{
0, 7, 3, 3, 3, 8, 8, 8, 8, 1,
1, 1, 2, 2, 2, 2, 2, 14, 15, 15,
17, 17, 4, 4, 4, 13, 5, 5, 6, 12,
12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
12, 9, 9, 9, 16, 16, 11, 10, 10,
17, 17, 4, 4, 4, 13, 5, 6, 6, 6,
6, 6, 6, 6, 12, 12, 12, 12, 12, 12,
12, 12, 12, 12, 12, 12, 9, 9, 9, 16,
16, 11, 10, 10,
}
var yyR2 = [...]int{
0, 1, 1, 2, 4, 1, 1, 1, 1, 2,
2, 1, 1, 1, 1, 3, 1, 3, 1, 3,
1, 3, 1, 2, 1, 1, 1, 1, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 1, 2, 3, 1, 3, 1, 1, 0,
1, 3, 1, 2, 1, 1, 1, 3, 4, 1,
1, 1, 2, 2, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 1, 2, 3, 1,
3, 1, 1, 0,
}
var yyChk = [...]int{
-1000, -7, -3, -1, 26, 17, 21, 22, -2, -8,
-4, -9, 19, -14, 9, 10, 11, 12, -5, -13,
-6, -12, 16, 15, 8, 14, 21, 22, 23, 24,
25, 27, 28, 29, 30, 26, 31, 34, -1, 17,
26, -15, -17, -1, -1, -1, -1, 32, -5, 20,
-16, -11, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, 18, 35, 33, 20,
-5, 20, 33, 18, -17, -1, -10, -1,
-1000, -7, -3, -1, 27, 18, 22, 23, -2, -8,
-4, -9, 20, -14, 10, 11, 12, 13, -5, -13,
-6, -12, 17, 16, 15, 9, 4, 5, 22, 23,
24, 25, 26, 28, 29, 30, 31, 27, 32, 35,
-1, 18, 27, -15, -17, -1, -1, -1, -1, 33,
-5, 4, 5, 21, -16, -11, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
19, 36, 34, 21, -5, 33, 21, 34, 19, -17,
-1, -5, -10, -1,
}
var yyDef = [...]int{
0, -2, 1, 2, 0, 0, 0, 0, 11, 12,
13, 14, 0, 16, 5, 6, 7, 8, 22, 0,
24, 41, 0, 26, 27, 25, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 3, 0,
0, 0, 18, 20, 9, 10, 0, 0, 23, 42,
0, 44, 46, 29, 30, 31, 32, 33, 34, 35,
36, 37, 38, 39, 40, 0, 17, 0, 0, 15,
28, 43, 48, 4, 19, 21, 45, 47,
24, 46, 0, 26, 25, 29, 30, 31, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
3, 0, 0, 0, 18, 20, 9, 10, 0, 0,
23, 32, 33, 47, 0, 49, 51, 34, 35, 36,
37, 38, 39, 40, 41, 42, 43, 44, 45, 0,
17, 0, 0, 15, 27, 0, 48, 53, 4, 19,
21, 28, 50, 52,
}
var yyTok1 = [...]int{
@ -187,7 +196,7 @@ var yyTok2 = [...]int{
2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
32, 33, 34, 35,
32, 33, 34, 35, 36,
}
var yyTok3 = [...]int{
0,
@ -618,96 +627,121 @@ yydefault:
yyVAL.expr = NewCellRef(yyDollar[1].node.val)
}
case 27:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.expr = NewNamedRangeRef(yyDollar[1].node.val)
}
case 28:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.expr = NewRange(yyDollar[1].expr, yyDollar[3].expr)
}
case 29:
yyDollar = yyS[yypt-3 : yypt+1]
case 28:
yyDollar = yyS[yypt-4 : yypt+1]
{
yyVAL.expr = NewBinaryExpr(yyDollar[1].expr, BinOpTypePlus, yyDollar[3].expr)
yyVAL.expr = NewPrefixRangeExpr(yyDollar[1].expr, yyDollar[2].expr, yyDollar[4].expr)
}
case 29:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.expr = NewNamedRangeRef(yyDollar[1].node.val)
}
case 30:
yyDollar = yyS[yypt-3 : yypt+1]
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.expr = NewBinaryExpr(yyDollar[1].expr, BinOpTypeMinus, yyDollar[3].expr)
yyVAL.expr = NewHorizontalRange(yyDollar[1].node.val)
}
case 31:
yyDollar = yyS[yypt-3 : yypt+1]
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.expr = NewBinaryExpr(yyDollar[1].expr, BinOpTypeMult, yyDollar[3].expr)
yyVAL.expr = NewVerticalRange(yyDollar[1].node.val)
}
case 32:
yyDollar = yyS[yypt-3 : yypt+1]
yyDollar = yyS[yypt-2 : yypt+1]
{
yyVAL.expr = NewBinaryExpr(yyDollar[1].expr, BinOpTypeDiv, yyDollar[3].expr)
yyVAL.expr = NewPrefixHorizontalRange(yyDollar[1].expr, yyDollar[2].node.val)
}
case 33:
yyDollar = yyS[yypt-3 : yypt+1]
yyDollar = yyS[yypt-2 : yypt+1]
{
yyVAL.expr = NewBinaryExpr(yyDollar[1].expr, BinOpTypeExp, yyDollar[3].expr)
yyVAL.expr = NewPrefixVerticalRange(yyDollar[1].expr, yyDollar[2].node.val)
}
case 34:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.expr = NewBinaryExpr(yyDollar[1].expr, BinOpTypeLT, yyDollar[3].expr)
yyVAL.expr = NewBinaryExpr(yyDollar[1].expr, BinOpTypePlus, yyDollar[3].expr)
}
case 35:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.expr = NewBinaryExpr(yyDollar[1].expr, BinOpTypeGT, yyDollar[3].expr)
yyVAL.expr = NewBinaryExpr(yyDollar[1].expr, BinOpTypeMinus, yyDollar[3].expr)
}
case 36:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.expr = NewBinaryExpr(yyDollar[1].expr, BinOpTypeLEQ, yyDollar[3].expr)
yyVAL.expr = NewBinaryExpr(yyDollar[1].expr, BinOpTypeMult, yyDollar[3].expr)
}
case 37:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.expr = NewBinaryExpr(yyDollar[1].expr, BinOpTypeGEQ, yyDollar[3].expr)
yyVAL.expr = NewBinaryExpr(yyDollar[1].expr, BinOpTypeDiv, yyDollar[3].expr)
}
case 38:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.expr = NewBinaryExpr(yyDollar[1].expr, BinOpTypeEQ, yyDollar[3].expr)
yyVAL.expr = NewBinaryExpr(yyDollar[1].expr, BinOpTypeExp, yyDollar[3].expr)
}
case 39:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.expr = NewBinaryExpr(yyDollar[1].expr, BinOpTypeNE, yyDollar[3].expr)
yyVAL.expr = NewBinaryExpr(yyDollar[1].expr, BinOpTypeLT, yyDollar[3].expr)
}
case 40:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.expr = NewBinaryExpr(yyDollar[1].expr, BinOpTypeConcat, yyDollar[3].expr)
yyVAL.expr = NewBinaryExpr(yyDollar[1].expr, BinOpTypeGT, yyDollar[3].expr)
}
case 41:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.expr = NewBinaryExpr(yyDollar[1].expr, BinOpTypeLEQ, yyDollar[3].expr)
}
case 42:
yyDollar = yyS[yypt-2 : yypt+1]
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.expr = NewFunction(yyDollar[1].node.val, nil)
yyVAL.expr = NewBinaryExpr(yyDollar[1].expr, BinOpTypeGEQ, yyDollar[3].expr)
}
case 43:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.expr = NewFunction(yyDollar[1].node.val, yyDollar[2].args)
yyVAL.expr = NewBinaryExpr(yyDollar[1].expr, BinOpTypeEQ, yyDollar[3].expr)
}
case 44:
yyDollar = yyS[yypt-1 : yypt+1]
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.args = append(yyVAL.args, yyDollar[1].expr)
yyVAL.expr = NewBinaryExpr(yyDollar[1].expr, BinOpTypeNE, yyDollar[3].expr)
}
case 45:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.args = append(yyDollar[1].args, yyDollar[3].expr)
yyVAL.expr = NewBinaryExpr(yyDollar[1].expr, BinOpTypeConcat, yyDollar[3].expr)
}
case 47:
yyDollar = yyS[yypt-2 : yypt+1]
{
yyVAL.expr = NewFunction(yyDollar[1].node.val, nil)
}
case 48:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.expr = NewFunction(yyDollar[1].node.val, yyDollar[2].args)
}
case 49:
yyDollar = yyS[yypt-1 : yypt+1]
{
yyVAL.args = append(yyVAL.args, yyDollar[1].expr)
}
case 50:
yyDollar = yyS[yypt-3 : yypt+1]
{
yyVAL.args = append(yyDollar[1].args, yyDollar[3].expr)
}
case 53:
yyDollar = yyS[yypt-0 : yypt+1]
{
yyVAL.expr = NewEmptyExpr()

View File

@ -23,7 +23,7 @@ package formula
%type <rows> constArrayRows
%type <args> arguments constArrayCols
%token <node> tokenHorizontalRange tokenReservedName tokenDDECall tokenLexError tokenNamedRange
%token <node> tokenHorizontalRange tokenVerticalRange tokenReservedName tokenDDECall tokenLexError tokenNamedRange
%token <node> tokenBool tokenNumber tokenString tokenError tokenErrorRef tokenSheet tokenCell
%token <node> tokenFunctionBuiltin
@ -78,18 +78,23 @@ constArrayCols:
reference:
referenceItem
| prefix referenceItem { $$ = NewPrefixExpr($1,$2)}
| prefix referenceItem { $$ = NewPrefixExpr($1,$2)}
| refFunctionCall;
prefix: tokenSheet { $$ = NewSheetPrefixExpr($1.val) };
referenceItem:
tokenCell { $$ = NewCellRef($1.val)}
| tokenNamedRange { $$ = NewNamedRangeRef($1.val)}
;
refFunctionCall:
referenceItem tokenColon referenceItem { $$ = NewRange($1,$3) };
referenceItem tokenColon referenceItem { $$ = NewRange($1,$3) }
| prefix referenceItem tokenColon referenceItem { $$ = NewPrefixRangeExpr($1,$2,$4)}
| tokenNamedRange { $$ = NewNamedRangeRef($1.val)}
| tokenHorizontalRange { $$ = NewHorizontalRange($1.val) }
| tokenVerticalRange { $$ = NewVerticalRange($1.val) }
| prefix tokenHorizontalRange { $$ = NewPrefixHorizontalRange($1,$2.val) }
| prefix tokenVerticalRange { $$ = NewPrefixVerticalRange($1,$2.val) };
binOp:

View File

@ -0,0 +1,58 @@
// 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 (
"fmt"
"strconv"
"strings"
)
// HorizontalRange is a range expression that when evaluated returns a list of Results from references like 1:4 (all cells from rows 1 to 4).
type HorizontalRange struct {
rowFrom, rowTo int
}
// NewHorizontalRange constructs a new full rows range.
func NewHorizontalRange(v string) Expression {
sl := strings.Split(v, ":")
if len(sl) != 2 {
return nil
}
from, _ := strconv.Atoi(sl[0])
to, _ := strconv.Atoi(sl[1])
return HorizontalRange{from, to}
}
// Eval evaluates a horizontal range returning a list of results or an error.
func (r HorizontalRange) Eval(ctx Context, ev Evaluator) Result {
key := r.horizontalRangeReference()
if cached, found := ev.GetFromCache(key); found {
return cached
}
from, to := cellRefsFromHorizontalRange(ctx, r.rowFrom, r.rowTo)
res := resultFromCellRange(ctx, ev, from, to)
ev.SetCache(key, res)
return res
}
func (r HorizontalRange) horizontalRangeReference() string {
return fmt.Sprintf("%d:%d", r.rowFrom, r.rowTo)
}
// Reference returns a string reference value to a horizontal range.
func (r HorizontalRange) Reference(ctx Context, ev Evaluator) Reference {
return Reference{Type: ReferenceTypeHorizontalRange, Value: r.horizontalRangeReference()}
}
func cellRefsFromHorizontalRange(ctx Context, rowFrom, rowTo int) (string, string) {
from := "A" + strconv.Itoa(rowFrom)
lastColumn := ctx.LastColumn(rowFrom, rowTo)
to := lastColumn + strconv.Itoa(rowTo)
return from, to
}

View File

@ -82,3 +82,13 @@ func (i *ivr) IsBool(cellRef string) bool {
func (i *ivr) IsDBCS() bool {
return false
}
// LastColumn returns empty string for the invalid reference context.
func (i *ivr) LastColumn(rowFrom, rowTo int) string {
return ""
}
// LastRow returns 0 for the invalid reference context.
func (i *ivr) LastRow(colFrom string) int {
return 0
}

File diff suppressed because it is too large Load Diff

View File

@ -32,19 +32,20 @@ import (
fnName = 'TODO';
file = 'TODO';
horizontalRange = '$'? [0-9]+ ':' '$'? [0-9]+;
verticalRange = '$'? [A-Z]+ ':' '$'? [A-Z]+;
# there is a function list at https://msdn.microsoft.com/en-us/library/dd906358(v=office.12).aspx
builtinFunction = [A-Z] [A-Z0-9.]* '(';
excelFn = '_xlfn.' [A-Z_] [A-Z0-9.]* '(';
sheetChar = ^['%\[\]\\:/?();{}#"=<>&+\-*/^%,_!];
sheetChar = ^['%\[\]\\:/?();{}#"=<>&+\-*/^%,!];
enclosedSheetChar = ^['*\[\]\\:\/?];
number = [0-9]+ '.'? [0-9]* ('e' [0-9]+)?;
sheet = sheetChar+ '!';
quotedSheet = (sheetChar+ | squote (enclosedSheetChar | dquote)+ squote) '!';
namedRange = [A-Z_\\][A-Za-z0-9\\_.]+;
namedRange = [A-Za-z_\\][A-Za-z0-9\\_.]+;
reservedName = '_xlnm.' [A-Z_]+;
@ -59,6 +60,7 @@ import (
errorLiteral => {l.emit(tokenError, data[ts:te])};
errorRef => {l.emit(tokenErrorRef, data[ts:te])};
horizontalRange => {l.emit(tokenHorizontalRange, data[ts:te])};
verticalRange => {l.emit(tokenVerticalRange, data[ts:te])};
sheet => {l.emit(tokenSheet, data[ts:te-1])}; # chop '!'
quotedSheet => {l.emit(tokenSheet, data[ts+1:te-2])}; # chop leading quote and trailing quote & !
reservedName => {l.emit(tokenReservedName, data[ts:te])};

View File

@ -12,7 +12,7 @@ import (
"strings"
)
// NamedRangeRef is a reference to a named range
// NamedRangeRef is a reference to a named range.
type NamedRangeRef struct {
s string
}
@ -25,20 +25,31 @@ func NewNamedRangeRef(v string) Expression {
// Eval evaluates and returns the result of the NamedRangeRef reference.
func (n NamedRangeRef) Eval(ctx Context, ev Evaluator) Result {
ref := ctx.NamedRange(n.s)
switch ref.Type {
case ReferenceTypeCell:
return ev.Eval(ctx, ref.Value)
case ReferenceTypeRange:
// should look like "A2:C5"
sp := strings.Split(ref.Value, ":")
if len(sp) == 2 {
return resultFromCellRange(ctx, ev, sp[0], sp[1])
}
return MakeErrorResult(fmt.Sprintf("unsuppported named range value %s", ref.Value))
refValue := ref.Value
if cached, found := ev.GetFromCache(refValue); found {
return cached
}
return MakeErrorResult(fmt.Sprintf("unsuppported reference type %s", ref.Type))
sl := strings.Split(refValue, "!")
if len(sl) != 2 {
return MakeErrorResult(fmt.Sprintf("unsupported named range value %s", refValue))
}
sheetCtx := ctx.Sheet(sl[0])
sp := strings.Split(sl[1], ":")
switch len(sp) {
case 1:
result := ev.Eval(sheetCtx, sp[0])
ev.SetCache(refValue, result)
return result
case 2:
// should look like "A2:C5"
result := resultFromCellRange(sheetCtx, ev, sp[0], sp[1])
ev.SetCache(refValue, result)
return result
}
return MakeErrorResult(fmt.Sprintf("unsupported reference type %s", ref.Type))
}
// Reference returns a string reference value to a named range.
func (n NamedRangeRef) Reference(ctx Context, ev Evaluator) Reference {
return Reference{Type: ReferenceTypeNamedRange, Value: n.s}
}

View File

@ -7,14 +7,17 @@
package formula
// Negate is a negate expression like -A1.
type Negate struct {
e Expression
}
// NewNegate constructs a new negate expression.
func NewNegate(e Expression) Expression {
return Negate{e}
}
// Eval evaluates and returns the result of a Negate expression.
func (n Negate) Eval(ctx Context, ev Evaluator) Result {
r := n.e.Eval(ctx, ev)
if r.Type == ResultTypeNumber {
@ -23,6 +26,7 @@ func (n Negate) Eval(ctx Context, ev Evaluator) Result {
return MakeErrorResult("NEGATE expected number argument")
}
// Reference returns an invalid reference for Negate.
func (n Negate) Reference(ctx Context, ev Evaluator) Reference {
return ReferenceInvalid
}

View File

@ -0,0 +1,17 @@
// 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
// noCache is a struct with collection of caching methods stubs intended for evaluators without cache.
type noCache struct {}
func (nc *noCache) SetCache(key string, value Result) {}
func (nc *noCache) GetFromCache(key string) (Result, bool) {
return empty, false
}

View File

@ -13,10 +13,12 @@ import (
"github.com/unidoc/unioffice"
)
// Number is a nubmer expression.
type Number struct {
v float64
}
// NewNumber constructs a new number expression.
func NewNumber(v string) Expression {
f, err := strconv.ParseFloat(v, 64)
if err != nil {
@ -25,10 +27,12 @@ func NewNumber(v string) Expression {
return Number{f}
}
// Eval evaluates and returns a number.
func (n Number) Eval(ctx Context, ev Evaluator) Result {
return MakeNumberResult(n.v)
}
// Reference returns an invalid reference for Number.
func (n Number) Reference(ctx Context, ev Evaluator) Reference {
return ReferenceInvalid
}

View File

@ -9,15 +9,18 @@ package formula
import "fmt"
// PrefixExpr is an expression containing reference to another sheet like Sheet1!A1 (the value of the cell A1 from sheet 'Sheet1').
type PrefixExpr struct {
pfx Expression
exp Expression
}
// NewPrefixExpr constructs an expression with prefix.
func NewPrefixExpr(pfx, exp Expression) Expression {
return &PrefixExpr{pfx, exp}
}
// Eval evaluates and returns an expression with prefix.
func (p PrefixExpr) Eval(ctx Context, ev Evaluator) Result {
ref := p.pfx.Reference(ctx, ev)
switch ref.Type {
@ -29,6 +32,12 @@ func (p PrefixExpr) Eval(ctx Context, ev Evaluator) Result {
}
}
// Reference returns a string reference value to an expression with prefix.
func (p PrefixExpr) Reference(ctx Context, ev Evaluator) Reference {
pfx := p.pfx.Reference(ctx, ev)
exp := p.exp.Reference(ctx, ev)
if pfx.Type == ReferenceTypeSheet && exp.Type == ReferenceTypeCell {
return Reference{Type: ReferenceTypeCell, Value: pfx.Value + "!" + exp.Value}
}
return ReferenceInvalid
}

View File

@ -0,0 +1,60 @@
// 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 (
"fmt"
"strconv"
"strings"
)
// 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').
type PrefixHorizontalRange struct {
pfx Expression
rowFrom, rowTo int
}
// NewPrefixHorizontalRange constructs a new full rows range with prefix.
func NewPrefixHorizontalRange(pfx Expression, v string) Expression {
sl := strings.Split(v, ":")
if len(sl) != 2 {
return nil
}
from, _ := strconv.Atoi(sl[0])
to, _ := strconv.Atoi(sl[1])
return PrefixHorizontalRange{pfx, from, to}
}
// Eval evaluates a horizontal range with prefix returning a list of results or an error.
func (r PrefixHorizontalRange) Eval(ctx Context, ev Evaluator) Result {
pfx := r.pfx.Reference(ctx, ev)
switch pfx.Type {
case ReferenceTypeSheet:
ref := r.horizontalRangeReference(pfx.Value)
if cached, found := ev.GetFromCache(ref); found {
return cached
}
c := ctx.Sheet(pfx.Value)
from, to := cellRefsFromHorizontalRange(c, r.rowFrom, r.rowTo)
result := resultFromCellRange(c, ev, from, to)
ev.SetCache(ref, result)
return result
default:
return MakeErrorResult(fmt.Sprintf("no support for reference type %s", pfx.Type))
}
}
func (r PrefixHorizontalRange) horizontalRangeReference(sheetName string) string {
return fmt.Sprintf("%s!%d:%d", sheetName, r.rowFrom, r.rowTo)
}
// Reference returns a string reference value to a horizontal range with prefix.
func (r PrefixHorizontalRange) Reference(ctx Context, ev Evaluator) Reference {
pfx := r.pfx.Reference(ctx, ev)
return Reference{Type: ReferenceTypeHorizontalRange, Value: r.horizontalRangeReference(pfx.Value)}
}

View File

@ -0,0 +1,58 @@
// 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 "fmt"
// 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 {
pfx, from, to Expression
}
// NewPrefixRangeExpr constructs a new range with prefix.
func NewPrefixRangeExpr(pfx, from, to Expression) Expression {
return PrefixRangeExpr{pfx, from, to}
}
// Eval evaluates a range with prefix returning a list of results or an error.
func (p PrefixRangeExpr) Eval(ctx Context, ev Evaluator) Result {
pfx := p.pfx.Reference(ctx, ev)
from := p.from.Reference(ctx, ev)
to := p.to.Reference(ctx, ev)
switch pfx.Type {
case ReferenceTypeSheet:
ref := prefixRangeReference(pfx, from, to)
if from.Type == ReferenceTypeCell && to.Type == ReferenceTypeCell {
if cached, found := ev.GetFromCache(ref); found {
return cached
} else {
result := resultFromCellRange(ctx.Sheet(pfx.Value), ev, from.Value, to.Value)
ev.SetCache(ref, result)
return result
}
}
return MakeErrorResult("invalid range " + ref)
default:
return MakeErrorResult(fmt.Sprintf("no support for reference type %s", pfx.Type))
}
}
func prefixRangeReference(pfx, from, to Reference) string {
return fmt.Sprintf("%s!%s:%s", pfx.Value, from.Value, to.Value)
}
// Reference returns a string reference value to a range with prefix.
func (p PrefixRangeExpr) Reference(ctx Context, ev Evaluator) Reference {
pfx := p.pfx.Reference(ctx, ev)
from := p.from.Reference(ctx, ev)
to := p.to.Reference(ctx, ev)
if pfx.Type == ReferenceTypeSheet && from.Type == ReferenceTypeCell && to.Type == ReferenceTypeCell {
return MakeRangeReference(prefixRangeReference(pfx, from, to))
}
return ReferenceInvalid
}

View File

@ -0,0 +1,57 @@
// 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 (
"fmt"
"strings"
)
// 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').
type PrefixVerticalRange struct {
pfx Expression
colFrom, colTo string
}
// NewPrefixVerticalRange constructs a new full columns range with prefix.
func NewPrefixVerticalRange(pfx Expression, v string) Expression {
sl := strings.Split(v, ":")
if len(sl) != 2 {
return nil
}
return PrefixVerticalRange{pfx, sl[0], sl[1]}
}
// Eval evaluates a vertical range with prefix returning a list of results or an error.
func (r PrefixVerticalRange) Eval(ctx Context, ev Evaluator) Result {
pfx := r.pfx.Reference(ctx, ev)
switch pfx.Type {
case ReferenceTypeSheet:
ref := r.verticalRangeReference(pfx.Value)
if cached, found := ev.GetFromCache(ref); found {
return cached
}
c := ctx.Sheet(pfx.Value)
from, to := cellRefsFromVerticalRange(c, r.colFrom, r.colTo)
result := resultFromCellRange(c, ev, from, to)
ev.SetCache(ref, result)
return result
default:
return MakeErrorResult(fmt.Sprintf("no support for reference type %s", pfx.Type))
}
}
func (r PrefixVerticalRange) verticalRangeReference(sheetName string) string {
return fmt.Sprintf("%s!%s:%s", sheetName, r.colFrom, r.colTo)
}
// Reference returns a string reference value to a vertical range with prefix.
func (r PrefixVerticalRange) Reference(ctx Context, ev Evaluator) Reference {
pfx := r.pfx.Reference(ctx, ev)
return Reference{Type: ReferenceTypeVerticalRange, Value: r.verticalRangeReference(pfx.Value)}
}

View File

@ -9,8 +9,6 @@ package formula
import (
"fmt"
"strconv"
"strings"
"github.com/unidoc/unioffice/spreadsheet/reference"
)
@ -25,65 +23,54 @@ func NewRange(from, to Expression) Expression {
return Range{from, to}
}
// Eval evaluates the range returning a list of results or an error.
// Eval evaluates a range returning a list of results or an error.
func (r Range) Eval(ctx Context, ev Evaluator) Result {
from := r.from.Reference(ctx, ev)
to := r.to.Reference(ctx, ev)
ref := rangeReference(from, to)
if from.Type == ReferenceTypeCell && to.Type == ReferenceTypeCell {
return resultFromCellRange(ctx, ev, from.Value, to.Value)
if cached, found := ev.GetFromCache(ref); found {
return cached
} else {
result := resultFromCellRange(ctx, ev, from.Value, to.Value)
ev.SetCache(ref, result)
return result
}
}
return MakeErrorResult("invalid range " + from.Value + " to " + to.Value)
return MakeErrorResult("invalid range " + ref)
}
func rangeReference(from, to Reference) string {
return fmt.Sprintf("%s:%s", from.Value, to.Value)
}
// Reference returns a string reference value to a range.
func (r Range) Reference(ctx Context, ev Evaluator) Reference {
from := r.from.Reference(ctx, ev)
to := r.to.Reference(ctx, ev)
if from.Type == ReferenceTypeCell && to.Type == ReferenceTypeCell {
return MakeRangeReference(from.Value + ":" + to.Value)
return MakeRangeReference(rangeReference(from, to))
}
return ReferenceInvalid
}
// TODO: move these somewhere to remove duplication
func ParseCellReference(s string) (col string, row uint32, err error) {
s = strings.Replace(s, "$", "", -1)
split := -1
lfor:
for i := 0; i < len(s); i++ {
switch {
case s[i] >= '0' && s[i] <= '9':
split = i
break lfor
}
}
switch split {
case 0:
return col, row, fmt.Errorf("no letter prefix in %s", s)
case -1:
return col, row, fmt.Errorf("no digits in %s", s)
}
col = s[0:split]
r64, err := strconv.ParseUint(s[split:], 10, 32)
row = uint32(r64)
return col, row, err
}
func resultFromCellRange(ctx Context, ev Evaluator, from, to string) Result {
fc, fr, fe := ParseCellReference(from)
tc, tr, te := ParseCellReference(to)
fromRef, fe := reference.ParseCellReference(from)
if fe != nil {
return MakeErrorResult("unable to parse range " + from)
return MakeErrorResult(fmt.Sprintf("unable to parse range %s: error %s", from, fe.Error()))
}
fc, fr := fromRef.ColumnIdx, fromRef.RowIdx
toRef, te := reference.ParseCellReference(to)
if te != nil {
return MakeErrorResult("unable to parse range " + to)
return MakeErrorResult(fmt.Sprintf("unable to parse range %s: error %s", to, te.Error()))
}
bc := reference.ColumnToIndex(fc)
ec := reference.ColumnToIndex(tc)
tc, tr := toRef.ColumnIdx, toRef.RowIdx
arr := [][]Result{}
for r := fr; r <= tr; r++ {
args := []Result{}
for c := bc; c <= ec; c++ {
for c := fc; c <= tc; c++ {
res := ctx.Cell(fmt.Sprintf("%s%d", reference.IndexToColumn(c), r), ev)
args = append(args, res)
}

View File

@ -14,6 +14,8 @@ type ReferenceType byte
const (
ReferenceTypeInvalid ReferenceType = iota
ReferenceTypeCell
ReferenceTypeHorizontalRange
ReferenceTypeVerticalRange
ReferenceTypeNamedRange
ReferenceTypeRange
ReferenceTypeSheet

View File

@ -7,18 +7,22 @@
package formula
// SheetPrefixExpr is a reference to a sheet like Sheet1! (reference to sheet 'Sheet1').
type SheetPrefixExpr struct {
sheet string
}
// NewSheetPrefixExpr constructs a new prefix expression.
func NewSheetPrefixExpr(s string) Expression {
return &SheetPrefixExpr{s}
}
// Eval evaluates and returns the result of a sheet expression.
func (s SheetPrefixExpr) Eval(ctx Context, ev Evaluator) Result {
return MakeErrorResult("sheet prefix should never be evaluated")
}
// Reference returns a string reference value to a sheet.
func (s SheetPrefixExpr) Reference(ctx Context, ev Evaluator) Reference {
return Reference{Type: ReferenceTypeSheet, Value: s.sheet}
}

View File

@ -9,20 +9,24 @@ package formula
import "strings"
// String is a string expression.
type String struct {
s string
}
// NewString constructs a new string expression.
func NewString(v string) Expression {
// Excel escapes quotes within a string by repeating them
v = strings.Replace(v, `""`, `"`, -1)
return String{v}
}
// Eval evaluates and returns a string.
func (s String) Eval(ctx Context, ev Evaluator) Result {
return MakeStringResult(s.s)
}
// Reference returns an invalid reference for String.
func (s String) Reference(ctx Context, ev Evaluator) Reference {
return ReferenceInvalid
}

View File

@ -0,0 +1,56 @@
// 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 (
"fmt"
"strconv"
"strings"
)
// 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).
type VerticalRange struct {
colFrom, colTo string
}
// NewVerticalRange constructs a new full columns range.
func NewVerticalRange(v string) Expression {
sl := strings.Split(v, ":")
if len(sl) != 2 {
return nil
}
return VerticalRange{sl[0], sl[1]}
}
// Eval evaluates a vertical range returning a list of results or an error.
func (r VerticalRange) Eval(ctx Context, ev Evaluator) Result {
key := r.verticalRangeReference()
if cached, found := ev.GetFromCache(key); found {
return cached
}
from, to := cellRefsFromVerticalRange(ctx, r.colFrom, r.colTo)
res := resultFromCellRange(ctx, ev, from, to)
ev.SetCache(key, res)
return res
}
func (r VerticalRange) verticalRangeReference() string {
return fmt.Sprintf("%s:%s", r.colFrom, r.colTo)
}
// Reference returns a string reference value to a vertical range.
func (r VerticalRange) Reference(ctx Context, ev Evaluator) Reference {
return Reference{Type: ReferenceTypeVerticalRange, Value: r.verticalRangeReference()}
}
func cellRefsFromVerticalRange(ctx Context, colFrom, colTo string) (string, string) {
from := colFrom + "1"
lastRow := ctx.LastRow(colFrom)
to := colTo + strconv.Itoa(lastRow)
return from, to
}

View File

@ -40,8 +40,8 @@ func New() *Workbook {
wb.ContentTypes.AddOverride(unioffice.AbsoluteFilename(unioffice.DocTypeSpreadsheet, unioffice.StylesType, 0), unioffice.SMLStyleSheetContentType)
wb.SharedStrings = NewSharedStrings()
wb.ContentTypes.AddOverride(unioffice.AbsoluteFilename(unioffice.DocTypeSpreadsheet, unioffice.SharedStingsType, 0), unioffice.SharedStringsContentType)
wb.wbRels.AddRelationship(unioffice.RelativeFilename(unioffice.DocTypeSpreadsheet, unioffice.OfficeDocumentType, unioffice.SharedStingsType, 0), unioffice.SharedStingsType)
wb.ContentTypes.AddOverride(unioffice.AbsoluteFilename(unioffice.DocTypeSpreadsheet, unioffice.SharedStringsType, 0), unioffice.SharedStringsContentType)
wb.wbRels.AddRelationship(unioffice.RelativeFilename(unioffice.DocTypeSpreadsheet, unioffice.OfficeDocumentType, unioffice.SharedStringsType, 0), unioffice.SharedStringsType)
return wb
}

View File

@ -22,6 +22,7 @@ type CellReference struct {
Column string
AbsoluteColumn bool
AbsoluteRow bool
SheetName string
}
func (c CellReference) String() string {
@ -44,8 +45,13 @@ func ParseCellReference(s string) (CellReference, error) {
if len(s) < 2 {
return CellReference{}, errors.New("cell reference must have at least two characters")
}
r := CellReference{}
r := CellReference{}
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

View File

@ -22,7 +22,7 @@ func ColumnToIndex(col string) uint32 {
return res - 1
}
// IndexToColumn maps a column number to a coumn name (e.g. 0 = A, 1 = B, 26 = AA)
// IndexToColumn maps a column number to a column name (e.g. 0 = A, 1 = B, 26 = AA)
func IndexToColumn(col uint32) string {
var a [64 + 1]byte
i := len(a)

View File

@ -15,11 +15,21 @@ import (
// ParseRangeReference splits a range reference of the form "A1:B5" into its
// components.
func ParseRangeReference(s string) (from, to CellReference, 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 CellReference{}, CellReference{}, errors.New("invalid range format")
}
if sheetName != "" {
sp[0] = sheetName + "!" + sp[0]
sp[1] = sheetName + "!" + sp[1]
}
fromRef, err := ParseCellReference(sp[0])
if err != nil {
return CellReference{}, CellReference{}, err

View File

@ -648,7 +648,6 @@ func (s *Sheet) RecalculateFormulas() {
if c.X().F.TAttr == sml.ST_CellFormulaTypeShared && len(formStr) == 0 {
continue
}
res := ev.Eval(ctx, formStr).AsString()
if res.Type == formula.ResultTypeError {
unioffice.Log("error evaulating formula %s: %s", formStr, res.ErrorMessage)
@ -663,8 +662,12 @@ func (s *Sheet) RecalculateFormulas() {
// the formula is of type array, so if the result is also an
// array we need to expand the array out into cells
if c.X().F.TAttr == sml.ST_CellFormulaTypeArray && res.Type == formula.ResultTypeArray {
s.setArray(c.Reference(), res)
if c.X().F.TAttr == sml.ST_CellFormulaTypeArray {
if res.Type == formula.ResultTypeArray {
s.setArray(c.Reference(), res)
} else if res.Type == formula.ResultTypeList {
s.setList(c.Reference(), res)
}
} else if c.X().F.TAttr == sml.ST_CellFormulaTypeShared && c.X().F.RefAttr != nil {
from, to, err := reference.ParseRangeReference(*c.X().F.RefAttr)
if err != nil {
@ -720,6 +723,32 @@ func (s *Sheet) setArray(origin string, arr formula.Result) error {
for ic, val := range row {
cell := sr.Cell(reference.IndexToColumn(cref.ColumnIdx + uint32(ic)))
if val.Type != formula.ResultTypeEmpty {
if val.IsBoolean {
cell.SetBool(val.ValueNumber != 0)
} else {
cell.SetCachedFormulaResult(val.String())
}
}
}
}
return nil
}
// setList expands an array into cached values starting at the origin which
// should be a cell reference of the type "A1". This is used when evaluating
// list type formulas.
func (s *Sheet) setList(origin string, list formula.Result) error {
cref, err := reference.ParseCellReference(origin)
if err != nil {
return err
}
sr := s.Row(cref.RowIdx)
for ic, val := range list.ValueList {
cell := sr.Cell(reference.IndexToColumn(cref.ColumnIdx + uint32(ic)))
if val.Type != formula.ResultTypeEmpty {
if val.IsBoolean {
cell.SetBool(val.ValueNumber != 0)
} else {
cell.SetCachedFormulaResult(val.String())
}
}

View File

@ -102,6 +102,33 @@ func (wb *Workbook) AddSheet() Sheet {
return Sheet{wb, rs, ws}
}
var sstAbsTargetAttr = unioffice.AbsoluteFilename(unioffice.DocTypeSpreadsheet, unioffice.SharedStringsType, 0)
var sstRelTargetAttr = unioffice.RelativeFilename(unioffice.DocTypeSpreadsheet, unioffice.OfficeDocumentType, unioffice.SharedStringsType, 0)
// ensureSharedStringsRelationships checks if relationships and content types related to shared strings are already exist and add them in the case of absence.
func (wb *Workbook) ensureSharedStringsRelationships() {
foundSharedStringContentType := false
for _, o := range wb.ContentTypes.X().Override {
if o.ContentTypeAttr == unioffice.SharedStringsContentType {
foundSharedStringContentType = true
break
}
}
if !foundSharedStringContentType {
wb.ContentTypes.AddOverride(sstAbsTargetAttr, unioffice.SharedStringsContentType)
}
foundSharedStringRel := false
for _, rel := range wb.wbRels.Relationships() {
if rel.X().TargetAttr == sstRelTargetAttr {
foundSharedStringRel = true
break
}
}
if !foundSharedStringRel {
wb.wbRels.AddRelationship(sstRelTargetAttr, unioffice.SharedStringsType)
}
}
// RemoveSheet removes the sheet with the given index from the workbook.
func (wb *Workbook) RemoveSheet(ind int) error {
if wb.SheetCount() <= ind {
@ -314,7 +341,7 @@ func (wb *Workbook) Save(w io.Writer) error {
zippkg.MarshalXML(z, fn, sheet)
zippkg.MarshalXML(z, zippkg.RelationsPathFor(fn), wb.xwsRels[i].X())
}
if err := zippkg.MarshalXMLByType(z, dt, unioffice.SharedStingsType, wb.SharedStrings.X()); err != nil {
if err := zippkg.MarshalXMLByType(z, dt, unioffice.SharedStringsType, wb.SharedStrings.X()); err != nil {
return err
}
@ -470,7 +497,7 @@ func (wb *Workbook) onNewRelationship(decMap *zippkg.DecodeMap, target, typ stri
decMap.AddTarget(target, thm, typ, 0)
rel.TargetAttr = unioffice.RelativeFilename(dt, src.Typ, typ, len(wb.themes))
case unioffice.SharedStingsType:
case unioffice.SharedStringsType:
wb.SharedStrings = NewSharedStrings()
decMap.AddTarget(target, wb.SharedStrings.X(), typ, 0)
rel.TargetAttr = unioffice.RelativeFilename(dt, src.Typ, typ, 0)

View File

@ -78,8 +78,8 @@ func Decode(f *zip.File, dest interface{}) error {
// SML
case unioffice.WorksheetTypeStrict:
ds.Relationship[i].TypeAttr = unioffice.WorksheetType
case unioffice.SharedStingsTypeStrict:
ds.Relationship[i].TypeAttr = unioffice.SharedStingsType
case unioffice.SharedStringsTypeStrict:
ds.Relationship[i].TypeAttr = unioffice.SharedStringsType
case unioffice.TableTypeStrict:
ds.Relationship[i].TypeAttr = unioffice.TableType