formula: add more math formulas

- ABS
- ACOS
- ACOSH
- ARABIC
- ASIN
- ASINH
- ATAN
- ATANH
- ATAN2
- BASE
- CEILING.MATH
- CEILING.PRECISE
- COMBIN
- COMBINA
- COS
- COSH
- DECIMAL
- DEGREES
- PI
This commit is contained in:
Todd 2017-09-15 16:13:41 -05:00
parent 6778397c6b
commit 382c157c11
13 changed files with 1216 additions and 584 deletions

View File

@ -4,9 +4,8 @@ package main
import (
"fmt"
"baliance.com/gooxml/spreadsheet/formula"
"baliance.com/gooxml/spreadsheet"
"baliance.com/gooxml/spreadsheet/formula"
)
func main() {

View File

@ -0,0 +1,23 @@
// 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 sales@baliance.com.
package formula
type EmptyExpr struct {
}
func NewEmptyExpr() Expression {
return EmptyExpr{}
}
func (e EmptyExpr) Eval(ctx Context, ev Evaluator) Result {
return MakeEmptyResult()
}
func (e EmptyExpr) Reference(ctx Context, ev Evaluator) Reference {
return ReferenceInvalid
}

View File

@ -0,0 +1,404 @@
// 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 sales@baliance.com.
package formula
import (
"fmt"
"math"
"strconv"
"strings"
)
func init() {
RegisterFunction("ABS", makeMathWrapper("ASIN", math.Abs))
RegisterFunction("ACOS", makeMathWrapper("ASIN", math.Acos))
RegisterFunction("ACOSH", makeMathWrapper("ASIN", math.Acosh))
// TODO: RegisterFunction("ACOT", Acot) /// Excel 2013+
// TODO: RegisterFunction("ACOTH", Acoth) /// Excel 2013+
// TODO: RegisterFunction("_xlfn.AGGREGATE", Aggregate) // lots of dependencies
RegisterFunction("_xlfn.ARABIC", Arabic)
RegisterFunction("ASIN", makeMathWrapper("ASIN", math.Asin))
RegisterFunction("ASINH", makeMathWrapper("ASINH", math.Asinh))
RegisterFunction("ATAN", makeMathWrapper("ATAN", math.Atan))
RegisterFunction("ATANH", makeMathWrapper("ATANH", math.Atanh))
RegisterFunction("ATAN2", Atan2)
RegisterFunction("_xlfn.BASE", Base)
// RegisterFunction("CEILING", ) // TODO: figure out how this acts, Libre doesn't use it
RegisterFunction("_xlfn.CEILING.MATH", CeilingMath)
RegisterFunction("_xlfn.CEILING.PRECISE", CeilingPrecise)
RegisterFunction("COMBIN", Combin)
RegisterFunction("_xlfn.COMBINA", Combina)
RegisterFunction("COS", makeMathWrapper("COS", math.Cos))
RegisterFunction("COSH", makeMathWrapper("COSH", math.Cosh))
//RegisterFunction("COT",
//RegisterFunction("COTH"
//RegisterFunction("CSC"
//RegisterFunction("CSCH"
RegisterFunction("_xlfn.DECIMAL", Decimal)
RegisterFunction("DEGREES", Degrees)
RegisterFunction("PI", Pi)
}
// makeMathWrapper is used to wrap single argument math functions from the Go
// standard library and present them as a spreadsheet function.
func makeMathWrapper(name string, fn func(x float64) float64) Function {
return func(args []Result) Result {
if len(args) != 1 {
return MakeErrorResult(name + " requires one argument")
}
arg := args[0].AsNumber()
switch arg.Type {
case ResultTypeNumber:
v := fn(arg.ValueNumber)
if v != v {
return MakeErrorResult(name + " returned NaN")
}
return MakeNumberResult(v)
case ResultTypeList, ResultTypeString:
return MakeErrorResult(name + " requires a numeric argument")
case ResultTypeError:
return arg
default:
return MakeErrorResult(fmt.Sprintf("unhandled %s() argument type %s", name, arg.Type))
}
}
}
// Atan2 implements the Excel ATAN2 function. It accepts two numeric arguments,
// and the arguments are (x,y), reversed from normal to match Excel's behaviour.
func Atan2(args []Result) Result {
if len(args) != 2 {
return MakeErrorResult("ATAN2 requires two arguments")
}
arg1 := args[0].AsNumber()
arg2 := args[1].AsNumber()
if arg1.Type == ResultTypeNumber && arg2.Type == ResultTypeNumber {
// args are swapped here
v := math.Atan2(arg2.ValueNumber, arg1.ValueNumber)
if v != v {
return MakeErrorResult("ATAN2 returned NaN")
}
return MakeNumberResult(v)
}
for _, t := range []ResultType{arg1.Type, arg2.Type} {
switch t {
case ResultTypeList, ResultTypeString:
return MakeErrorResult("ATAN2 requires a numeric argument")
case ResultTypeError:
return arg1
default:
return MakeErrorResult(fmt.Sprintf("unhandled ATAN2() argument type %s", t))
}
}
return MakeErrorResult("unhandled error for ATAN2()")
}
// Arabic implements the Excel ARABIC function which parses roman numerals. It
// accepts one numeric argument.
func Arabic(args []Result) Result {
if len(args) != 1 {
return MakeErrorResult("ARABIC requires one argument")
}
arg := args[0]
switch arg.Type {
case ResultTypeNumber, ResultTypeList, ResultTypeEmpty:
return MakeErrorResult("ARABIC requires a string argument argument")
case ResultTypeString:
res := 0.0
last := 0.0
for _, c := range arg.ValueString {
digit := 0.0
switch c {
case 'I':
digit = 1
case 'V':
digit = 5
case 'X':
digit = 10
case 'L':
digit = 50
case 'C':
digit = 100
case 'D':
digit = 500
case 'M':
digit = 1000
}
res += digit
switch {
// repeated digits
case last == digit &&
(last == 5 || last == 50 || last == 500):
return MakeErrorResult("invalid ARABIC format")
// simpler form
case 2*last == digit:
return MakeErrorResult("invalid ARABIC format")
}
if last < digit {
res -= 2 * last
}
last = digit
}
return MakeNumberResult(res)
case ResultTypeError:
return arg
default:
return MakeErrorResult(fmt.Sprintf("unhandled ACOSH() argument type %s", arg.Type))
}
}
// CeilingMath implements _xlfn.CEILING.MATH which rounds numbers to the nearest
// multiple of the second argument, toward or away from zero as specified by the
// third argument.
func CeilingMath(args []Result) Result {
if len(args) == 0 {
return MakeErrorResult("CEILING.MATH() requires at least one argument")
}
if len(args) > 3 {
return MakeErrorResult("CEILING.MATH() allows at most three arguments")
}
// number to round
number := args[0].AsNumber()
if number.Type != ResultTypeNumber {
return MakeErrorResult("first arugment to CEILING.MATH() must be a number")
}
// significance
significance := float64(1)
if number.ValueNumber < 0 {
significance = -1
}
if len(args) > 1 {
sigArg := args[1].AsNumber()
if sigArg.Type != ResultTypeNumber {
return MakeErrorResult("second arugment to CEILING.MATH() must be a number")
}
significance = sigArg.ValueNumber
}
// round direction
direction := float64(1)
if len(args) > 2 {
dirArg := args[2].AsNumber()
if dirArg.Type != ResultTypeNumber {
return MakeErrorResult("third arugment to CEILING.MATH() must be a number")
}
direction = dirArg.ValueNumber
}
if len(args) == 1 {
return MakeNumberResult(math.Ceil(number.ValueNumber))
}
v := number.ValueNumber
v, res := math.Modf(v / significance)
if res != 0 {
if number.ValueNumber > 0 {
v++
} else if direction < 0 {
v--
}
}
return MakeNumberResult(v * significance)
}
func CeilingPrecise(args []Result) Result {
if len(args) == 0 {
return MakeErrorResult("CEILING.PRECISE() requires at least one argument")
}
if len(args) > 2 {
return MakeErrorResult("CEILING.PRECISE() allows at most two arguments")
}
// number to round
number := args[0].AsNumber()
if number.Type != ResultTypeNumber {
return MakeErrorResult("first arugment to CEILING.PRECISE() must be a number")
}
// significance
significance := float64(1)
if number.ValueNumber < 0 {
significance = -1
}
if len(args) > 1 {
sigArg := args[1].AsNumber()
if sigArg.Type != ResultTypeNumber {
return MakeErrorResult("second arugment to CEILING.MATH() must be a number")
}
// don't care about sign of significance
significance = math.Abs(sigArg.ValueNumber)
}
if len(args) == 1 {
return MakeNumberResult(math.Ceil(number.ValueNumber))
}
v := number.ValueNumber
v, res := math.Modf(v / significance)
if res != 0 {
if number.ValueNumber > 0 {
v++
}
}
return MakeNumberResult(v * significance)
}
// Base is an implementation of the Excel BASE function that returns a string
// form of an integer in a specified base and of a minimum length with padded
// zeros.
func Base(args []Result) Result {
if len(args) < 2 {
return MakeErrorResult("BASE() requires at least two arguments")
}
if len(args) > 3 {
return MakeErrorResult("BASE() allows at most three arguments")
}
// number to convert
number := args[0].AsNumber()
if number.Type != ResultTypeNumber {
return MakeErrorResult("first arugment to BASE() must be a number")
}
radixArg := args[1].AsNumber()
if radixArg.Type != ResultTypeNumber {
return MakeErrorResult("second arugment to BASE() must be a number")
}
radix := int(radixArg.ValueNumber)
if radix < 0 || radix > 36 {
return MakeErrorResult("radix must be in the range [0,36]")
}
// min length of result
minLength := 0
if len(args) > 2 {
lenArg := args[2].AsNumber()
if lenArg.Type != ResultTypeNumber {
return MakeErrorResult("third arugment to BASE() must be a number")
}
minLength = int(lenArg.ValueNumber)
}
s := strconv.FormatInt(int64(number.ValueNumber), radix)
if len(s) < minLength {
s = strings.Repeat("0", minLength-len(s)) + s
}
return MakeStringResult(s)
}
// Combin is an implementation of the Excel COMBINA function whic returns the
// number of combinations.
func Combin(args []Result) Result {
if len(args) != 2 {
return MakeErrorResult("COMBIN() requires two argument")
}
nArg := args[0].AsNumber()
kArg := args[1].AsNumber()
if nArg.Type != ResultTypeNumber || kArg.Type != ResultTypeNumber {
return MakeErrorResult("COMBIN() requires numeric arguments")
}
n := math.Trunc(nArg.ValueNumber)
k := math.Trunc(kArg.ValueNumber)
if k > n {
return MakeErrorResult("COMBIN() requires k <= n")
}
if k == n || k == 0 {
return MakeNumberResult(1)
}
res := float64(1)
for i := float64(1); i <= k; i++ {
res *= (n + 1 - i) / i
}
return MakeNumberResult(res)
}
// Combina is an implementation of the Excel COMBINA function whic returns the
// number of combinations with repetitions.
func Combina(args []Result) Result {
if len(args) != 2 {
return MakeErrorResult("COMBINA() requires two argument")
}
nArg := args[0].AsNumber()
kArg := args[1].AsNumber()
if nArg.Type != ResultTypeNumber || kArg.Type != ResultTypeNumber {
return MakeErrorResult("COMBINA() requires numeric arguments")
}
n := math.Trunc(nArg.ValueNumber)
k := math.Trunc(kArg.ValueNumber)
if n < k {
return MakeErrorResult("COMBINA() requires n > k")
}
if n == 0 {
return MakeNumberResult(0)
}
args[0] = MakeNumberResult(n + k - 1)
args[1] = MakeNumberResult(n - 1)
return Combin(args)
}
func fact(f float64) float64 {
res := float64(1)
for i := float64(2); i <= f; i++ {
res *= i
}
return res
}
// Decimal is an implementation of the Excel function DECIMAL() that parses a string
// in a given base and returns the numeric result.
func Decimal(args []Result) Result {
if len(args) != 2 {
return MakeErrorResult("DECIMAL() requires two arguments")
}
sArg := args[0].AsString()
if sArg.Type != ResultTypeString {
return MakeErrorResult("DECIMAL() requires string first argument")
}
baseArg := args[1].AsNumber()
if baseArg.Type != ResultTypeNumber {
return MakeErrorResult("DECIMAL() requires number second argument")
}
sv := sArg.ValueString
if len(sv) > 2 && (strings.HasPrefix(sv, "0x") || strings.HasPrefix(sv, "0X")) {
sv = sv[2:]
}
i, err := strconv.ParseInt(sv, int(baseArg.ValueNumber), 64)
if err != nil {
return MakeErrorResult("DECIMAL() error in conversion")
}
return MakeNumberResult(float64(i))
}
// Degrees is an implementation of the Excel function DEGREES() that converts
// radians to degrees.
func Degrees(args []Result) Result {
if len(args) != 1 {
return MakeErrorResult("DEGREES() requires one argument")
}
vArg := args[0].AsNumber()
if vArg.Type != ResultTypeNumber {
return MakeErrorResult("DEGREES() requires number argument")
}
return MakeNumberResult(180.0 / math.Pi * vArg.ValueNumber)
}
// Pi is an implementation of the Excel Pi() function that just returns the Pi
// constant.
func Pi(args []Result) Result {
if len(args) != 0 {
return MakeErrorResult("PI() accepts no arguments")
}
return MakeNumberResult(math.Pi)
}

View File

@ -34,6 +34,8 @@ func Sum(args []Result) Result {
// treated as zero by Excel
case ResultTypeError:
return a
case ResultTypeEmpty:
// skip
default:
return MakeErrorResult(fmt.Sprintf("unhandled SUM() argument type %s", a.Type))
}
@ -64,6 +66,8 @@ func Min(args []Result) Result {
if 0 < res.ValueNumber {
res.ValueNumber = 0
}
case ResultTypeEmpty:
// skip
case ResultTypeError:
return a
default:
@ -91,6 +95,8 @@ func Max(args []Result) Result {
if subMax.ValueNumber > res.ValueNumber {
res.ValueNumber = subMax.ValueNumber
}
case ResultTypeEmpty:
// skip
case ResultTypeString:
// treated as zero by Excel
if 0 > res.ValueNumber {

View File

@ -1,6 +1,6 @@
#!/bin/bash
echo "lexer"
ragel -G2 -Z lexer.rl
ragel -G0 -Z lexer.rl
goimports -w lexer.go
echo "parser"

View File

@ -98,13 +98,13 @@ const yyLast = 145
var yyAct = [...]int{
42, 43, 3, 39, 59, 35, 34, 26, 36, 37,
17, 24, 25, 26, 38, 26, 33, 60, 21, 41,
19, 11, 33, 9, 44, 45, 46, 47, 48, 49,
50, 51, 52, 53, 54, 55, 61, 56, 1, 22,
23, 24, 25, 26, 31, 27, 28, 29, 30, 32,
58, 18, 33, 10, 2, 8, 0, 0, 0, 0,
0, 62, 57, 22, 23, 24, 25, 26, 31, 27,
3, 24, 25, 26, 59, 34, 26, 36, 37, 17,
26, 39, 33, 38, 35, 33, 21, 60, 41, 19,
42, 43, 62, 44, 45, 46, 47, 48, 49, 50,
51, 52, 53, 54, 55, 61, 56, 11, 22, 23,
24, 25, 26, 31, 27, 28, 29, 30, 32, 58,
9, 33, 1, 18, 10, 2, 8, 0, 0, 0,
0, 63, 57, 22, 23, 24, 25, 26, 31, 27,
28, 29, 30, 32, 0, 0, 33, 22, 23, 24,
25, 26, 31, 27, 28, 29, 30, 32, 0, 0,
33, 22, 23, 24, 25, 26, 0, 0, 0, 13,
@ -116,42 +116,42 @@ var yyAct = [...]int{
}
var yyPact = [...]int{
92, -1000, -1000, 58, 124, -19, 124, 124, -1000, -1000,
-1000, -1000, 124, -1000, -1000, -1000, -1000, -27, -1000, -1000,
92, -1000, -1000, 58, 124, -10, 124, 124, -1000, -1000,
-1000, -1000, 124, -1000, -1000, -1000, -1000, -19, -1000, -1000,
110, -1000, 124, 124, 124, 124, 124, 124, 124, 124,
124, 124, 124, 124, 58, 124, -10, -10, 44, 5,
-1000, -14, -1000, 58, -10, -10, -16, -16, -1000, 72,
72, 72, 72, 72, 72, -8, 20, -1000, -1000, -1000,
124, -1000, -1000,
124, 124, 124, 124, 58, 124, -20, -20, 44, 3,
-1000, -14, -1000, 58, -20, -20, -17, -17, -1000, 72,
72, 72, 72, 72, 72, -13, 19, -1000, -1000, -1000,
124, -1000, -1000, 58,
}
var yyPgo = [...]int{
0, 1, 55, 54, 53, 10, 51, 38, 23, 21,
0, 20, 19,
0, 0, 56, 55, 54, 9, 53, 52, 50, 37,
22, 20, 19, 18,
}
var yyR1 = [...]int{
0, 7, 3, 3, 3, 8, 8, 8, 8, 1,
1, 1, 2, 2, 2, 2, 4, 4, 5, 6,
11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
11, 11, 9, 9, 9, 12, 12, 10,
12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
12, 12, 9, 9, 9, 13, 13, 11, 10, 10,
}
var yyR2 = [...]int{
0, 1, 1, 2, 4, 1, 1, 1, 1, 2,
2, 1, 1, 1, 1, 3, 1, 1, 1, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 1, 2, 3, 1, 3, 1,
3, 3, 1, 2, 3, 1, 3, 1, 1, 0,
}
var yyChk = [...]int{
-1000, -7, -3, -1, 24, 15, 19, 20, -2, -8,
-4, -9, 17, 7, 8, 9, 10, -5, -6, -11,
-4, -9, 17, 7, 8, 9, 10, -5, -6, -12,
14, 13, 19, 20, 21, 22, 23, 25, 26, 27,
28, 24, 29, 32, -1, 24, -1, -1, -1, 30,
18, -12, -10, -1, -1, -1, -1, -1, -1, -1,
18, -13, -11, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, 18, -5, 18,
31, 16, -10,
31, 16, -10, -1,
}
var yyDef = [...]int{
@ -161,7 +161,7 @@ var yyDef = [...]int{
0, 0, 0, 0, 3, 0, 9, 10, 0, 0,
33, 0, 35, 37, 20, 21, 22, 23, 24, 25,
26, 27, 28, 29, 30, 31, 0, 15, 19, 34,
0, 4, 36,
39, 4, 36, 38,
}
var yyTok1 = [...]int{
@ -652,6 +652,11 @@ yydefault:
{
yyVAL.args = append(yyDollar[1].args, yyDollar[3].expr)
}
case 39:
yyDollar = yyS[yypt-0 : yypt+1]
{
yyVAL.expr = NewEmptyExpr()
}
}
goto yystack /* stack new state and value */
}

View File

@ -17,7 +17,7 @@ package formula
}
%type <expr> formula formula1 initial reference referenceItem refFunctionCall
%type <expr> start constant functionCall argument
%type <expr> start constant functionCall argument argument1
%type <expr> binOp
%type <args> arguments
@ -96,10 +96,14 @@ functionCall:
| tokenFunctionBultin arguments tokenRParen { $$ = NewFunction($1.val,$2)} ;
arguments:
argument { $$ = append($$, $1) }
argument1{ $$ = append($$, $1) }
| arguments tokenComma argument { $$ = append($1,$3) }
;
argument: formula;
argument1: formula ;
argument:
formula
| /*empty*/ { $$ = NewEmptyExpr() } ;
%%

File diff suppressed because it is too large Load Diff

View File

@ -34,8 +34,8 @@ import (
horizontalRange = '$'? [0-9]+ ':' '$'? [0-9]+;
# there is a function list at https://msdn.microsoft.com/en-us/library/dd906358(v=office.12).aspx
builtinFunction = [A-Z]+ '(';
excelFn = '_xlfn.' [A-Z_]+ '(';
builtinFunction = [A-Z] [A-Z0-9]+ '(';
excelFn = '_xlfn.' [A-Z_] [A-Z0-9.]+ '(';
sheetChar = ^['%\[\]\\:/?();{}#"=<>&+\-*/^%,_];
enclosedSheetChar = ^['*\[\]\\:\/?];

View File

@ -20,7 +20,7 @@ type plex struct {
}
func (f *plex) Lex(lval *yySymType) int {
// yyDebug = 3
//yyDebug = 3
n := <-f.nodes
if n != nil {
lval.node = n
@ -40,5 +40,8 @@ func Parse(r io.Reader) Expression {
}
func ParseString(s string) Expression {
if s == "" {
return NewEmptyExpr()
}
return Parse(strings.NewReader(s))
}

View File

@ -7,7 +7,10 @@
package formula
import "fmt"
import (
"fmt"
"strconv"
)
// ResultType is the type of the result
//go:generate stringer -type=ResultType
@ -20,6 +23,7 @@ const (
ResultTypeString
ResultTypeList
ResultTypeError
ResultTypeEmpty
)
// Result is the result of a formula or cell evaluation .
@ -45,6 +49,29 @@ func (r Result) Value() string {
}
}
// AsNumber attempts to intepret a string cell value as a number. Upon success,
// it returns a new number result, upon failure it returns the original result.
// This is used as functions return strings that can then act like number (e.g.
// LEFT(1.2345,3) + LEFT(1.2345,3) = 2.4)
func (r Result) AsNumber() Result {
if r.Type == ResultTypeString {
f, err := strconv.ParseFloat(r.ValueString, 64)
if err == nil {
return MakeNumberResult(f)
}
}
return r
}
func (r Result) AsString() Result {
switch r.Type {
case ResultTypeNumber:
return MakeStringResult(r.Value())
default:
return r
}
}
// MakeNumberResult constructs a number result.
func MakeNumberResult(v float64) Result {
return Result{Type: ResultTypeNumber, ValueNumber: v}
@ -106,3 +133,8 @@ func MakeErrorResultType(t ErrorType, msg string) Result {
func MakeStringResult(s string) Result {
return Result{Type: ResultTypeString, ValueString: s}
}
// MakeEmptyResult is ued when parsing an empty argument.
func MakeEmptyResult() Result {
return Result{Type: ResultTypeEmpty}
}

View File

@ -4,9 +4,9 @@ package formula
import "fmt"
const _ResultType_name = "ResultTypeUnknownResultTypeNumberResultTypeStringResultTypeListResultTypeError"
const _ResultType_name = "ResultTypeUnknownResultTypeNumberResultTypeStringResultTypeListResultTypeErrorResultTypeEmpty"
var _ResultType_index = [...]uint8{0, 17, 33, 49, 63, 78}
var _ResultType_index = [...]uint8{0, 17, 33, 49, 63, 78, 93}
func (i ResultType) String() string {
if i >= ResultType(len(_ResultType_index)-1) {