From b84bd1abc06457f6383013b8a600fc8c95eed2ed Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 27 Jan 2021 13:51:47 +0800 Subject: [PATCH] new formula fn: IF, LEN; not equal operator support and faster numeric precision process --- calc.go | 70 ++++++++++++++++++++++++++++++++++++++++++++++++---- calc_test.go | 24 +++++++++++++++--- lib.go | 27 ++++++++++++++++++-- rows.go | 10 +++----- rows_test.go | 11 ++++++--- 5 files changed, 121 insertions(+), 21 deletions(-) diff --git a/calc.go b/calc.go index d2bab1d..5b975f5 100644 --- a/calc.go +++ b/calc.go @@ -110,6 +110,7 @@ var tokenPriority = map[string]int{ "+": 3, "-": 3, "=": 2, + "<>": 2, "<": 2, "<=": 2, ">": 2, @@ -151,11 +152,9 @@ func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { return } result = token.TValue - if len(result) > 16 { - num, e := roundPrecision(result) - if e != nil { - return result, err - } + isNum, precision := isNumeric(result) + if isNum && precision > 15 { + num, _ := roundPrecision(result) result = strings.ToUpper(num) } return @@ -353,6 +352,12 @@ func calcEq(rOpd, lOpd string, opdStack *Stack) error { return nil } +// calcNEq evaluate not equal arithmetic operations. +func calcNEq(rOpd, lOpd string, opdStack *Stack) error { + opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(rOpd != lOpd)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + return nil +} + // calcL evaluate less than arithmetic operations. func calcL(rOpd, lOpd string, opdStack *Stack) error { lOpdVal, err := strconv.ParseFloat(lOpd, 64) @@ -498,6 +503,7 @@ func calculate(opdStack *Stack, opt efp.Token) error { "/": calcDiv, "+": calcAdd, "=": calcEq, + "<>": calcNEq, "<": calcL, "<=": calcLe, ">": calcG, @@ -3400,6 +3406,20 @@ func (fn *formulaFuncs) CLEAN(argsList *list.List) (result string, err error) { return } +// LEN returns the length of a supplied text string. The syntax of the +// function is: +// +// LEN(text) +// +func (fn *formulaFuncs) LEN(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("LEN requires 1 string argument") + return + } + result = strconv.Itoa(len(argsList.Front().Value.(formulaArg).String)) + return +} + // TRIM removes extra spaces (i.e. all spaces except for single spaces between // words or characters) from a supplied text string. The syntax of the // function is: @@ -3469,3 +3489,43 @@ func (fn *formulaFuncs) UPPER(argsList *list.List) (result string, err error) { result = strings.ToUpper(argsList.Front().Value.(formulaArg).String) return } + +// Conditional Functions + +// IF function tests a supplied condition and returns one result if the +// condition evaluates to TRUE, and another result if the condition evaluates +// to FALSE. The syntax of the function is: +// +// IF( logical_test, value_if_true, value_if_false ) +// +func (fn *formulaFuncs) IF(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { + err = errors.New("IF requires at least 1 argument") + return + } + if argsList.Len() > 3 { + err = errors.New("IF accepts at most 3 arguments") + return + } + token := argsList.Front().Value.(formulaArg) + var cond bool + switch token.Type { + case ArgString: + if cond, err = strconv.ParseBool(token.String); err != nil { + err = errors.New(formulaErrorVALUE) + return + } + if argsList.Len() == 1 { + result = strings.ToUpper(strconv.FormatBool(cond)) + return + } + if cond { + result = argsList.Front().Next().Value.(formulaArg).String + return + } + if argsList.Len() == 3 { + result = argsList.Back().Value.(formulaArg).String + } + } + return +} diff --git a/calc_test.go b/calc_test.go index ea60a50..c999540 100644 --- a/calc_test.go +++ b/calc_test.go @@ -323,13 +323,13 @@ func TestCalcCellValue(t *testing.T) { "=ROUND(991,-1)": "990", // ROUNDDOWN "=ROUNDDOWN(99.999,1)": "99.9", - "=ROUNDDOWN(99.999,2)": "99.99000000000001", + "=ROUNDDOWN(99.999,2)": "99.99000000000002", "=ROUNDDOWN(99.999,0)": "99", "=ROUNDDOWN(99.999,-1)": "90", - "=ROUNDDOWN(-99.999,2)": "-99.99000000000001", + "=ROUNDDOWN(-99.999,2)": "-99.99000000000002", "=ROUNDDOWN(-99.999,-1)": "-90", - // ROUNDUP - "=ROUNDUP(11.111,1)": "11.200000000000003", + // ROUNDUP` + "=ROUNDUP(11.111,1)": "11.200000000000001", "=ROUNDUP(11.111,2)": "11.120000000000003", "=ROUNDUP(11.111,0)": "12", "=ROUNDUP(11.111,-1)": "20", @@ -467,6 +467,9 @@ func TestCalcCellValue(t *testing.T) { // CLEAN "=CLEAN(\"\u0009clean text\")": "clean text", "=CLEAN(0)": "0", + // LEN + "=LEN(\"\")": "0", + "=LEN(D1)": "5", // TRIM "=TRIM(\" trim text \")": "trim text", "=TRIM(0)": "0", @@ -485,6 +488,12 @@ func TestCalcCellValue(t *testing.T) { "=UPPER(\"TEST\")": "TEST", "=UPPER(\"Test\")": "TEST", "=UPPER(\"TEST 123\")": "TEST 123", + // Conditional Functions + // IF + "=IF(1=1)": "TRUE", + "=IF(1<>1)": "FALSE", + "=IF(5<0, \"negative\", \"positive\")": "positive", + "=IF(-2<0, \"negative\", \"positive\")": "negative", } for formula, expected := range mathCalc { f := prepareData() @@ -805,6 +814,8 @@ func TestCalcCellValue(t *testing.T) { // CLEAN "=CLEAN()": "CLEAN requires 1 argument", "=CLEAN(1,2)": "CLEAN requires 1 argument", + // LEN + "=LEN()": "LEN requires 1 string argument", // TRIM "=TRIM()": "TRIM requires 1 argument", "=TRIM(1,2)": "TRIM requires 1 argument", @@ -817,6 +828,11 @@ func TestCalcCellValue(t *testing.T) { // PROPER "=PROPER()": "PROPER requires 1 argument", "=PROPER(1,2)": "PROPER requires 1 argument", + // Conditional Functions + // IF + "=IF()": "IF requires at least 1 argument", + "=IF(0,1,2,3)": "IF accepts at most 3 arguments", + "=IF(D1,1,2)": "#VALUE!", } for formula, expected := range mathCalcError { f := prepareData() diff --git a/lib.go b/lib.go index c89d69f..3f13512 100644 --- a/lib.go +++ b/lib.go @@ -403,8 +403,8 @@ func (f *File) addNameSpaces(path string, ns xml.Attr) { } } -// setIgnorableNameSpace provides a function to set XML namespace as ignorable by the given -// attribute. +// setIgnorableNameSpace provides a function to set XML namespace as ignorable +// by the given attribute. func (f *File) setIgnorableNameSpace(path string, index int, ns xml.Attr) { ignorableNS := []string{"c14", "cdr14", "a14", "pic14", "x14", "xdr14", "x14ac", "dsp", "mso14", "dgm14", "x15", "x12ac", "x15ac", "xr", "xr2", "xr3", "xr4", "xr5", "xr6", "xr7", "xr8", "xr9", "xr10", "xr11", "xr12", "xr13", "xr14", "xr15", "x15", "x16", "x16r2", "mo", "mx", "mv", "o", "v"} if inStrSlice(strings.Fields(f.xmlAttr[path][index].Value), ns.Name.Local) == -1 && inStrSlice(ignorableNS, ns.Name.Local) != -1 { @@ -418,6 +418,29 @@ func (f *File) addSheetNameSpace(sheet string, ns xml.Attr) { f.addNameSpaces(name, ns) } +// isNumeric determines whether an expression is a valid numeric type and get +// the precision for the numeric. +func isNumeric(s string) (bool, int) { + dot := false + p := 0 + for i, v := range s { + if v == '.' { + if dot { + return false, 0 + } + dot = true + } else if v < '0' || v > '9' { + if i == 0 && v == '-' { + continue + } + return false, 0 + } else if dot { + p++ + } + } + return true, p +} + // Stack defined an abstract data type that serves as a collection of elements. type Stack struct { list *list.List diff --git a/rows.go b/rows.go index 44c4b64..97cf2f5 100644 --- a/rows.go +++ b/rows.go @@ -20,7 +20,6 @@ import ( "log" "math" "strconv" - "strings" "github.com/mohae/deepcopy" ) @@ -346,12 +345,9 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { } return f.formattedValue(c.S, c.V), nil default: - splited := strings.Split(c.V, ".") - if len(splited) == 2 && len(splited[1]) > 15 { - val, err := roundPrecision(c.V) - if err != nil { - return "", err - } + isNum, precision := isNumeric(c.V) + if isNum && precision > 15 { + val, _ := roundPrecision(c.V) if val != c.V { return f.formattedValue(c.S, val), nil } diff --git a/rows_test.go b/rows_test.go index 02b00da..73931aa 100644 --- a/rows_test.go +++ b/rows_test.go @@ -835,9 +835,14 @@ func TestGetValueFromNumber(t *testing.T) { assert.Equal(t, "2.22", val) c = &xlsxC{T: "n", V: "2.220000ddsf0000000002-r"} - _, err = c.getValueFrom(f, d) - assert.NotNil(t, err) - assert.Equal(t, "strconv.ParseFloat: parsing \"2.220000ddsf0000000002-r\": invalid syntax", err.Error()) + val, err = c.getValueFrom(f, d) + assert.NoError(t, err) + assert.Equal(t, "2.220000ddsf0000000002-r", val) + + c = &xlsxC{T: "n", V: "2.2."} + val, err = c.getValueFrom(f, d) + assert.NoError(t, err) + assert.Equal(t, "2.2.", val) } func TestErrSheetNotExistError(t *testing.T) {