Skip to content

Commit

Permalink
Spreadsheed Formula Functions (unidoc#345)
Browse files Browse the repository at this point in the history
* CELL function
* CELL moved to fninformation.go
* CHOOSE function
* CHOOSE function add one test
* COLUMN function
* COLUMNS function
* COUNTIF function
* COUNTIF, COUNTIFS, MINIFS, MAXIFS, SUMIF, SUMIFS, some style fixes
* SUMIF and SUMIFS moved to the right location
* VALUE function
* wildcard is added
* CELL format fix
  • Loading branch information
zgordan-vv authored and gunnsth committed Nov 5, 2019
1 parent a0ab7d5 commit 415c045
Show file tree
Hide file tree
Showing 13 changed files with 1,350 additions and 8 deletions.
83 changes: 83 additions & 0 deletions internal/wildcard/match.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* MinIO Cloud Storage, (C) 2015, 2016 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package wildcard

// MatchSimple - finds whether the text matches/satisfies the pattern string.
// supports only '*' wildcard in the pattern.
// considers a file system path as a flat name space.
func MatchSimple(pattern, name string) bool {
if pattern == "" {
return name == pattern
}
if pattern == "*" {
return true
}
rname := make([]rune, 0, len(name))
rpattern := make([]rune, 0, len(pattern))
for _, r := range name {
rname = append(rname, r)
}
for _, r := range pattern {
rpattern = append(rpattern, r)
}
simple := true // Does only wildcard '*' match.
return deepMatchRune(rname, rpattern, simple)
}

// Match - finds whether the text matches/satisfies the pattern string.
// supports '*' and '?' wildcards in the pattern string.
// unlike path.Match(), considers a path as a flat name space while matching the pattern.
// The difference is illustrated in the example here https://play.golang.org/p/Ega9qgD4Qz .
func Match(pattern, name string) (matched bool) {
if pattern == "" {
return name == pattern
}
if pattern == "*" {
return true
}
rname := make([]rune, 0, len(name))
rpattern := make([]rune, 0, len(pattern))
for _, r := range name {
rname = append(rname, r)
}
for _, r := range pattern {
rpattern = append(rpattern, r)
}
simple := false // Does extended wildcard '*' and '?' match.
return deepMatchRune(rname, rpattern, simple)
}

func deepMatchRune(str, pattern []rune, simple bool) bool {
for len(pattern) > 0 {
switch pattern[0] {
default:
if len(str) == 0 || str[0] != pattern[0] {
return false
}
case '?':
if len(str) == 0 && !simple {
return false
}
case '*':
return deepMatchRune(str, pattern[1:], simple) ||
(len(str) > 0 && deepMatchRune(str[1:], pattern, simple))
}
str = str[1:]
pattern = pattern[1:]
}
return len(str) == 0 && len(pattern) == 0
}
35 changes: 35 additions & 0 deletions spreadsheet/cell.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,30 @@ func (c Cell) getFormat() string {
return nf.GetFormat()
}

func (c Cell) getLabelPrefix() string {
if c.x.SAttr == nil {
return ""
}
sid := *c.x.SAttr
cs := c.w.StyleSheet.GetCellStyle(sid)
switch cs.xf.Alignment.HorizontalAttr {
case sml.ST_HorizontalAlignmentLeft: return "'"
case sml.ST_HorizontalAlignmentRight: return "\""
case sml.ST_HorizontalAlignmentCenter: return "^"
case sml.ST_HorizontalAlignmentFill: return "\\"
default: return ""
}
}

func (c Cell) getLocked() bool {
if c.x.SAttr == nil {
return false
}
sid := *c.x.SAttr
f := c.w.StyleSheet.GetCellStyle(sid)
return *f.xf.Protection.LockedAttr
}

// GetFormattedValue returns the formatted cell value as it would appear in
// Excel. This involves determining the format string to apply, parsing it, and
// then formatting the value according to the format string. This should only
Expand Down Expand Up @@ -553,6 +577,17 @@ func (c Cell) SetCachedFormulaResult(s string) {
c.x.V = &s
}

func (c Cell) setLocked(locked bool) {
sid := c.x.SAttr
if sid != nil {
f := c.w.StyleSheet.GetCellStyle(*sid)
if f.xf.Protection == nil {
f.xf.Protection = sml.NewCT_CellProtection()
}
f.xf.Protection.LockedAttr = &locked
}
}

func b2i(v bool) int {
if v {
return 1
Expand Down
36 changes: 36 additions & 0 deletions spreadsheet/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,39 @@ func (e *evalContext) SetOffset(col, row uint32) {
e.colOff = col
e.rowOff = row
}

// GetFilename returns the filename of the context's workbook.
func (e *evalContext) GetFilename() string {
return e.s.w.GetFilename()
}

// GetFormat returns a cell data format.
func (e *evalContext) GetFormat(cellRef string) string {
return e.s.Cell(cellRef).getFormat()
}

// GetLabelPrefix returns label prefix which depends on the cell's horizontal alignment.
func (e *evalContext) GetLabelPrefix(cellRef string) string {
return e.s.Cell(cellRef).getLabelPrefix()
}

// GetLocked returns if the cell is locked.
func (e *evalContext) GetLocked(cellRef string) bool {
return e.s.Cell(cellRef).getLocked()
}

// SetLocked sets cell locked or not.
func (e *evalContext) SetLocked(cellRef string, locked bool) {
e.s.Cell(cellRef).setLocked(locked)
}

// GetWidth returns a worksheet's column width.
func (e *evalContext) GetWidth(colIdx int) float64 {
colIdx++
for _, c := range e.s.X().Cols[0].Col {
if int(c.MinAttr) <= colIdx && colIdx <= int(c.MaxAttr) {
return float64(int(*c.WidthAttr))
}
}
return 0
}
20 changes: 19 additions & 1 deletion spreadsheet/formula/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,27 @@ type Context interface {
Cell(ref string, ev Evaluator) Result

// Sheet returns an evaluation context for a given sheet name. This is used
// when evaluating cells that pull data from other sheets (e.g. ='Sheet 2'!A1)
// when evaluating cells that pull data from other sheets (e.g. ='Sheet 2'!A1).
Sheet(name string) Context

// GetFilename returns the full filename of the context's Workbook.
GetFilename() string

// GetWidth returns a worksheet's column width.
GetWidth(colIdx int) float64

// GetFormat returns a cell's format.
GetFormat(cellRef string) string

// GetLabelPrefix returns cell's label prefix dependent on cell horizontal alignment.
GetLabelPrefix(cellRef string) string

// GetFormat returns if cell is protected.
GetLocked(cellRef string) bool

// GetFormat returns sets cell's protected attribute.
SetLocked(cellRef string, locked bool)

// NamedRange returns a named range.
NamedRange(name string) Reference

Expand Down
54 changes: 53 additions & 1 deletion spreadsheet/formula/fnindex.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import (
)

func init() {
RegisterFunction("CHOOSE", Choose)
RegisterFunction("COLUMN", Column)
RegisterFunction("COLUMNS", Columns)
RegisterFunction("INDEX", Index)
RegisterFunctionComplex("INDIRECT", Indirect)
RegisterFunctionComplex("OFFSET", Offset)
Expand All @@ -24,7 +27,55 @@ func init() {
RegisterFunction("TRANSPOSE", Transpose)
}

// Index implements the Excel INDEX function
// Choose implements the Excel CHOOSE function.
func Choose(args []Result) Result {
if len(args) < 2 {
return MakeErrorResult("CHOOSE requires two arguments")
}
index := args[0]
if index.Type != ResultTypeNumber {
return MakeErrorResult("CHOOSE requires first argument of type number")
}
i := int(index.ValueNumber)
if len(args) <= i {
return MakeErrorResult("Index should be less or equal to the number of values")
}
return args[i]
}

// Column implements the Excel COLUMN function.
func Column(args []Result) Result {
if len(args) < 1 {
return MakeErrorResult("COLUMN requires one argument")
}
ref := args[0].Ref
if ref.Type != ReferenceTypeCell {
return MakeErrorResult("COLUMN requires an argument to be of type reference")
}
cr, err := reference.ParseCellReference(ref.Value)
if err != nil {
return MakeErrorResult("Incorrect reference: " + ref.Value)
}
return MakeNumberResult(float64(cr.ColumnIdx+1))
}

// Columns implements the Excel COLUMNS function.
func Columns(args []Result) Result {
if len(args) < 1 {
return MakeErrorResult("COLUMNS requires one argument")
}
arrResult := args[0]
if arrResult.Type != ResultTypeArray && arrResult.Type != ResultTypeList {
return MakeErrorResult("COLUMNS requires first argument of type array")
}
arr := arrResult.ValueArray
if len(arr) == 0 {
return MakeErrorResult("COLUMNS requires array to contain at least 1 row")
}
return MakeNumberResult(float64(len(arr[0])))
}

// Index implements the Excel INDEX function.
func Index(args []Result) Result {
if len(args) < 3 {
return MakeErrorResult("INDEX requires three arguments")
Expand Down Expand Up @@ -81,6 +132,7 @@ func Indirect(ctx Context, ev Evaluator, args []Result) Result {
return ctx.Cell(sarg.ValueString, ev)
}

// Offset is an implementation of the Excel OFFSET function.
func Offset(ctx Context, ev Evaluator, args []Result) Result {
if len(args) != 5 {
return MakeErrorResult("OFFSET requires one or two arguments")
Expand Down
Loading

0 comments on commit 415c045

Please sign in to comment.