Comparisons and criteria look at 15 significant digits. MID, CHOOSE and INDEX out of range are #NUM!, SEQUENCE(0) too, and ROWS and COLUMNS give an error they're handed. UPPER maps case fully, TEXT keeps TRUE, IFERROR with one argument is blank, conditions read "TRUE" and "", COUNT skips errors and COUNTA counts them, COUNTIF matches text numbers, INDEX with a 0 passes the whole row or column through, odd roots of negatives are real, SUMPRODUCT counts TRUE, and LET's unused errors don't matter. The oracle skips the three Excel error codes that now differ, and the decimal demo compares a residue with 0, which binary still misses.
- commit
- 441dcbc9dff48f7dbccb3c676de100049c367bf8
- parent
- 5f4d97f9438ac4614d5d1048be03af7d96c66498
- author
- Dami <dami.etoile@gmail.com>
- kind
- fix risk score 2.6 of 3, Jev's estimate
@@ -1,5 +1,5 @@
11 # Decimal arithmetic: binary floating point makes $4.35 come to 4342-# whole cents and $0.10 + $0.20 miss 0.3. File > Settings > Decimal2+# whole cents and $0.10 + $0.20 - 0.3 miss 0. File > Settings > Decimal33 # arithmetic, here from the palette, makes this file's money math exact.44 Output out/decimal.gif55 Source lib/setup.tape@@ -14,7 +14,7 @@ Type "Coffee" Tab Type "$4.35" Enter
1414 Type "Tip" Tab Type "$0.10" Enter1515 Type "Fee" Tab Type "$0.20" Enter1616 Type "Cents" Tab Type "=INT(B1*100)" Enter17-Type "Is 0.30?" Tab Type "=B2+B3=0.3" Enter17+Type "Is 0.30?" Tab Type "=B2+B3-0.3=0" Enter1818 Type "Residue" Tab Type "=0.1+0.2-0.3" Enter1919 Sleep 1.5s2020 Ctrl+K@@ -36,11 +36,26 @@ caret on the problem and says what's wrong on the context line, e.g.
3636 tighter than `^` as in Sheets and Excel, so `=-2^2` is 4. 1-2-3's `#AND#`,3737 `#OR#` and `#NOT#` still work; `AND()`, `OR()` and `NOT()` are the Sheets way.3838 39+Numbers are binary floating point, as in Sheets and Excel, and follow40+Sheets where that shows:41+42+- Comparisons, and criteria such as `COUNTIF`'s, look at the 1543+ significant digits a cell shows, so `=0.1+0.2=0.3` is TRUE. A44+ difference itself keeps every digit: `=0.1+0.2-0.3` is 5.551E-1745+ ([decimal arithmetic](decimal.md) makes it 0).46+- A number written in a formula keeps 15 significant digits:47+ `=123456789012345678` is 123456789012345000.48+- A negative number has odd roots: `=(-8)^(1/3)` and `=POWER(-8, 1/3)`49+ are -2; other fractional powers of it are `#NUM!`.50+3951 ## Values and errors4052 4153 Text in arithmetic is `#VALUE!` unless it reads as a number (`="3"*2` is 6);42-blanks count as 0 or empty text. Aggregates such as `SUM` over a range skip43-text. Errors use Sheets' codes:54+blanks count as 0 or empty text. A condition (`IF`, `IFS`, `AND`, `OR`,55+`NOT`) also reads the text `TRUE` or `FALSE`, and empty text as FALSE, so56+`=IF("TRUE", 1, 2)` is 1. Aggregates such as `SUM` over a range skip57+text; `COUNT` skips errors too and `COUNTA` counts them, where the others58+give the first error in the range. Errors use Sheets' codes:4459 4560 | Error | Meaning |4661 |---|---|@@ -49,7 +64,7 @@ text. Errors use Sheets' codes:
4964 | `#NAME?` | an unknown name |5065 | `#REF!` | a reference to deleted cells, or a circular reference: every cell of the cycle, even one whose formula catches errors (`IFERROR`) or skips them (`COUNTIF`), as in Sheets |5166 | `#N/A` | not available, e.g. a lookup found nothing |52-| `#NUM!` | a number out of range, e.g. `SQRT(-1)` |67+| `#NUM!` | a number out of range, e.g. `SQRT(-1)`, or a position outside what `CHOOSE`, `INDEX` or `MID` reads from, as in Sheets (Excel says `#VALUE!` or `#REF!`) |5368 5469 Error cells are red with a curly underline, and the context line explains the5570 active cell's error and where it came from, e.g.@@ -29,6 +29,8 @@ right and below, as in Sheets:
2929 The arguments of functions that take ranges are computed the same way,3030 so `=SUM(LEN(A2:A9))` counts every character and3131 `=SUMPRODUCT((B2:B99="north")*C2:C99)` sums a column by a condition.32+ This follows Excel 365 on purpose: Sheets computes `=SUM(C2:C4*2)` only33+ inside `ARRAYFORMULA`, and is `#VALUE!` without it.3234 - Elsewhere an array reads as its first value, as Sheets does:3335 `=LEN(SEQUENCE(3)*100)` is 3. `IF`, `IFERROR`, `IFNA`, `IFS`, `SWITCH`,3436 `CHOOSE` and `INDEX` pass an array through, so@@ -6,7 +6,9 @@ sidebar_position: 5
66 # Decimal arithmetic77 88 Like Sheets and Excel, 012 computes in binary floating point, so9-`=0.1+0.2=0.3` is FALSE and `=INT($4.35*100)` is 434. File > Settings >9+`=0.1+0.2-0.3` is 5.551E-17 rather than 0 and `=INT($4.35*100)` is 434.10+Comparisons look only at the 15 significant digits a cell shows, as in11+Sheets, so `=0.1+0.2=0.3` is TRUE, but `=0.1+0.2-0.3=0` is FALSE. File > Settings >1012 Decimal arithmetic (or search the palette for "decimal") switches the whole1113 file, every sheet of it, to decimal math for money. The status line then says1214 `decimal`, the menu shows a check mark, and the setting is saved with the@@ -20,15 +20,15 @@ Aliases: `AVG` for `AVERAGE`.
2020 | `ABS(value)` | Absolute value |2121 | `AND(logical1, [logical2, ...])` | TRUE if all arguments are true |2222 | `AVERAGE(value1, [value2, ...])` | Average of numbers, ignoring text |23-| `COUNT(value1, [value2, ...])` | Count of numeric values |24-| `COUNTA(value1, [value2, ...])` | Count of non-empty values |23+| `COUNT(value1, [value2, ...])` | Count of numeric values, skipping errors |24+| `COUNTA(value1, [value2, ...])` | Count of non-empty values, errors included |2525 | `FALSE()` | The logical value FALSE |2626 | `IF(condition, value_if_true, [value_if_false])` | Choose a value by a condition |27-| `IFERROR(value, [value_if_error])` | A fallback when a value is an error |27+| `IFERROR(value, [value_if_error])` | A fallback when a value is an error; blank without one |2828 | `INT(value)` | Round down to the nearest integer |2929 | `MAX(value1, [value2, ...])` | Largest number |3030 | `MIN(value1, [value2, ...])` | Smallest number |31-| `MOD(dividend, divisor)` | Remainder, with the sign of the divisor |31+| `MOD(dividend, divisor)` | Remainder, with the sign of the divisor: MOD(0.3, 0.1) is 0.1 as in Excel, where Sheets gives -5.55E-17 |3232 | `NA()` | The #N/A error |3333 | `NOT(logical)` | The opposite of a logical value |3434 | `OR(logical1, [logical2, ...])` | TRUE if any argument is true |@@ -48,7 +48,7 @@ Aliases: `AVG` for `AVERAGE`.
4848 | `LN(value)` | Natural logarithm |4949 | `LOG(value, [base])` | Logarithm, base 10 by default |5050 | `LOG10(value)` | Base-10 logarithm |51-| `POWER(base, exponent)` | A number raised to a power |51+| `POWER(base, exponent)` | A number raised to a power, odd roots of negatives included |5252 | `PRODUCT(factor1, [factor2, ...])` | Product of numbers |5353 | `QUOTIENT(dividend, divisor)` | Integer part of a division |5454 | `RAND()` | A random number from 0 up to 1, new on every change (recalculates on every change) |@@ -58,7 +58,7 @@ Aliases: `AVG` for `AVERAGE`.
5858 | `SIGN(value)` | 1, 0 or -1 by the sign of a number |5959 | `SUMIF(range, criterion, [sum_range])` | Sum of the cells that meet a condition |6060 | `SUMIFS(sum_range, criteria_range1, criterion1, [criteria_range2, criterion2, ...])` | Sum of the cells that meet every condition |61-| `SUMPRODUCT(array1, [array2, ...])` | Sum of the products of matching entries |61+| `SUMPRODUCT(array1, [array2, ...])` | Sum of the products of matching entries, TRUE counting as 1 |6262 | `TRUNC(value, [places])` | Drop decimals past a number of places |6363 6464 ## Statistics@@ -105,7 +105,7 @@ Aliases: `AVG` for `AVERAGE`.
105105 | `EXACT(string1, string2)` | TRUE if two texts are identical, case included |106106 | `FIND(search_for, text_to_search, [starting_at])` | Position of text, case-sensitive |107107 | `LEFT(string, [number_of_characters])` | The first characters of text |108-| `LEN(text)` | Number of characters in text |108+| `LEN(text)` | Number of characters in text: an emoji is one, where Sheets counts two |109109 | `LOWER(text)` | Text in lower case |110110 | `MID(string, starting_at, extract_length)` | Characters from the middle of text |111111 | `PROPER(text)` | Text with each word capitalized |@@ -117,7 +117,7 @@ Aliases: `AVG` for `AVERAGE`.
117117 | `TEXT(number, format)` | A number as text in a format, e.g. "$#,##0.00" or "yyyy-mm-dd" |118118 | `TEXTJOIN(delimiter, ignore_empty, text1, [text2, ...])` | Join text with a delimiter |119119 | `TRIM(text)` | Text without leading, trailing and repeated spaces |120-| `UPPER(text)` | Text in upper case |120+| `UPPER(text)` | Text in upper case: ß is SS |121121 | `VALUE(text)` | Text as a number; dates and times too |122122 123123 ## Split and regular expressions@@ -136,7 +136,7 @@ Aliases: `AVG` for `AVERAGE`.
136136 | `CHOOSE(index, choice1, [choice2, ...])` | The choice at a position |137137 | `COLUMNS(range)` | Number of columns in a range |138138 | `HLOOKUP(search_key, range, index, [is_sorted])` | Find a key in the first row and return a value from its column |139-| `INDEX(reference, [row], [column])` | The value at a row and column of a range |139+| `INDEX(reference, [row], [column])` | The value at a row and column of a range; row or column 0 for a whole column or row |140140 | `MATCH(search_key, range, [search_type])` | Position of a key in a row or column |141141 | `ROWS(range)` | Number of rows in a range |142142 | `VLOOKUP(search_key, range, index, [is_sorted])` | Find a key in the first column and return a value from its row |@@ -164,7 +164,7 @@ Aliases: `AVG` for `AVERAGE`.
164164 | `BYCOL(array_or_range, LAMBDA)` | Each column of an array passed to a LAMBDA, one value per column |165165 | `BYROW(array_or_range, LAMBDA)` | Each row of an array passed to a LAMBDA, one value per row |166166 | `LAMBDA([name, ...], formula_expression)` | A function of names, called with values: LAMBDA(x, x*2)(3) |167-| `LET(name1, value_expression1, [name2, value_expression2, ...], formula_expression)` | Name values for use in a formula |167+| `LET(name1, value_expression1, [name2, value_expression2, ...], formula_expression)` | Name values for use in a formula; an error counts only where its name is used |168168 | `MAKEARRAY(rows, columns, LAMBDA)` | An array of a size, each entry a LAMBDA of its row and column |169169 | `MAP(array1, [array2, ...], LAMBDA)` | Each entry of arrays passed to a LAMBDA |170170 | `REDUCE(initial_value, array_or_range, LAMBDA)` | An array folded into one value by a LAMBDA of the total so far and each entry |@@ -3,14 +3,14 @@ package e2e
33 import "testing"44 55 // money types a few amounts where binary floating point is off by a6-// hair: whole cents of $4.35 come to 434, $0.10 plus $0.20 isn't 0.3,6+// hair: whole cents of $4.35 come to 434, $0.10 plus $0.20 less 0.3 isn't 0,77 // and the difference shows in General format.88 func money(s *session) {99 s.keys("Coffee", "<tab>", "$4.35", "<enter>")1010 s.keys("Tip", "<tab>", "$0.10", "<enter>")1111 s.keys("Fee", "<tab>", "$0.20", "<enter>")1212 s.keys("Cents", "<tab>", "=INT(B1*100)", "<enter>")13- s.keys("Is 0.30?", "<tab>", "=B2+B3=0.3", "<enter>")13+ s.keys("Is 0.30?", "<tab>", "=B2+B3-0.3=0", "<enter>")1414 s.keys("Residue", "<tab>", "=0.1+0.2-0.3", "<enter>")1515 s.waitForBar("A7", "")1616 }@@ -1,6 +1,7 @@
11 package functions22 33 import (4+ "cmp"45 "strings"56 "unicode/utf8"67 @@ -345,18 +346,26 @@ func (c criterion) test(v Value) bool {
345346 }346347 return cmpResult(c.op, strings.Compare(strings.ToLower(v.Str), strings.ToLower(c.text)))347348 }348- // Numbers and booleans only match their own kind.349+ // Numbers and booleans only match their own kind, except that a350+ // number equals text that reads as it, as in Sheets: 3 matches "3".349351 if v.Kind != c.kind {352+ if v.Kind == value.Text && c.kind == value.Number && (c.op == "=" || c.op == "<>") {353+ if n, ok := value.ParseNumber(v.Str); ok {354+ return cmpResult(c.op, numCmp(n, c.num))355+ }356+ }350357 return c.op == "<>"351358 }352- d := 0353- switch {354- case v.Num < c.num:355- d = -1356- case v.Num > c.num:357- d = 1359+ return cmpResult(c.op, numCmp(v.Num, c.num))360+}361+362+// numCmp orders two numbers as the comparison operators do, equal when363+// they agree to the 15 digits a cell shows.364+func numCmp(a, b float64) int {365+ if value.SameShown(a, b) {366+ return 0358367 }359- return cmpResult(c.op, d)368+ return cmp.Compare(a, b)360369 }361370 362371 func cmpResult(op string, d int) bool {@@ -296,7 +296,10 @@ func sequence(args []Node, get lookup) Value {
296296 if err != nil {297297 return *err298298 }299- if rows < 1 || cols < 1 || tooBig(rows, cols) {299+ switch {300+ case rows < 1 || cols < 1:301+ return value.ErrNum // as Sheets; Excel says #CALC!302+ case tooBig(rows, cols):300303 return value.ErrValue301304 }302305 out := NewArray(rows, cols)@@ -1,8 +1,6 @@
11 package functions22 33 import (4- "math"5-64 "github.com/FelineStateMachine/012/internal/formula"75 "github.com/FelineStateMachine/012/internal/value"86 )@@ -278,7 +276,7 @@ func binaryOp(op string, l, r Value, dec bool) Value {
278276 case "&":279277 return Value{Kind: value.Text, Str: text(l) + text(r)}280278 case "=", "<>", "<", ">", "<=", ">=":281- return boolean(cmpResult(op, compare(l, r)))279+ return boolean(cmpResult(op, value.CompareShown(l, r)))282280 }283281 a, err := toNum(l)284282 if err != nil {@@ -306,7 +304,7 @@ func binaryOp(op string, l, r Value, dec bool) Value {
306304 }307305 return num(a / b)308306 case "^":309- return num(math.Pow(a, b))307+ return powerOf(a, b)310308 case "#AND#":311309 return boolean(a != 0 && b != 0)312310 case "#OR#":@@ -119,10 +119,10 @@ func init() {
119119 }120120 return num(s.sum / float64(s.nums))121121 }), format: inherit},122- &FuncDef{Name: "COUNT", Args: "value1, [value2, ...]", Desc: "Count of numeric values", Min: 1, Max: -1,123- eval: aggregate(func(s Agg) Value { return num(float64(s.nums)) })},124- &FuncDef{Name: "COUNTA", Args: "value1, [value2, ...]", Desc: "Count of non-empty values", Min: 1, Max: -1,125- eval: aggregate(func(s Agg) Value { return num(float64(s.count)) })},122+ &FuncDef{Name: "COUNT", Args: "value1, [value2, ...]", Desc: "Count of numeric values, skipping errors", Min: 1, Max: -1,123+ eval: counter(func(s Agg) Value { return num(float64(s.nums)) })},124+ &FuncDef{Name: "COUNTA", Args: "value1, [value2, ...]", Desc: "Count of non-empty values, errors included", Min: 1, Max: -1,125+ eval: counter(func(s Agg) Value { return num(float64(s.count + s.errs)) })},126126 &FuncDef{Name: "MIN", Args: "value1, [value2, ...]", Desc: "Smallest number", Min: 1, Max: -1,127127 eval: aggregate(func(s Agg) Value {128128 if s.nums == 0 {@@ -142,7 +142,7 @@ func init() {
142142 &FuncDef{Name: "SQRT", Args: "value", Desc: "Square root", Min: 1, Max: 1, eval: math1(math.Sqrt)},143143 &FuncDef{Name: "ROUND", Args: "value, [places]", Desc: "Round to a number of decimal places, halves away from zero", Min: 1, Max: 2,144144 eval: rounder(numfmt.HalfUp), format: inheritFrom(0)},145- &FuncDef{Name: "MOD", Args: "dividend, divisor", Desc: "Remainder, with the sign of the divisor", Min: 2, Max: 2,145+ &FuncDef{Name: "MOD", Args: "dividend, divisor", Desc: "Remainder, with the sign of the divisor: MOD(0.3, 0.1) is 0.1 as in Excel, where Sheets gives -5.55E-17", Min: 2, Max: 2,146146 eval: numeric(func(x []float64) Value {147147 if x[1] == 0 {148148 return value.ErrDiv0@@ -155,15 +155,11 @@ func init() {
155155 &FuncDef{Name: "NA", Desc: "The #N/A error", Max: 0, eval: constant(value.ErrNA)},156156 &FuncDef{Name: "IF", Args: "condition, value_if_true, [value_if_false]", Desc: "Choose a value by a condition", Min: 2, Max: 3, arrays: liftPass,157157 eval: func(args []Node, get lookup) Value {158- c := eval1(args[0], get)159- if c.Kind == value.Error {160- return c161- }162- f, err := toNum(c)158+ t, err := truth(eval1(args[0], get))163159 if err != nil {164160 return *err165161 }166- if f != 0 {162+ if t {167163 return eval(args[1], get)168164 }169165 if len(args) < 3 {@@ -171,14 +167,14 @@ func init() {
171167 }172168 return eval(args[2], get)173169 }, format: func(args []Node, infer func(Node) Format) Format { return inherit(args[1:], infer) }},174- &FuncDef{Name: "IFERROR", Args: "value, [value_if_error]", Desc: "A fallback when a value is an error", Min: 1, Max: 2, arrays: liftPass,170+ &FuncDef{Name: "IFERROR", Args: "value, [value_if_error]", Desc: "A fallback when a value is an error; blank without one", Min: 1, Max: 2, arrays: liftPass,175171 eval: func(args []Node, get lookup) Value {176172 v := eval(args[0], get)177173 if v.Kind != value.Error {178174 return v179175 }180176 if len(args) < 2 {181- return Value{Kind: value.Text}177+ return Value{} // blank, as in Sheets182178 }183179 return eval(args[1], get)184180 }},@@ -187,7 +183,13 @@ func init() {
187183 &FuncDef{Name: "OR", Args: "logical1, [logical2, ...]", Desc: "TRUE if any argument is true", Min: 1, Max: -1,188184 eval: logical(func(t, _ int) bool { return t > 0 })},189185 &FuncDef{Name: "NOT", Args: "logical", Desc: "The opposite of a logical value", Min: 1, Max: 1,190- eval: numeric(func(x []float64) Value { return boolean(x[0] == 0) })},186+ eval: func(args []Node, get lookup) Value {187+ t, err := truth(eval(args[0], get))188+ if err != nil {189+ return *err190+ }191+ return boolean(!t)192+ }},191193 )192194 }193195 @@ -198,9 +200,13 @@ func init() {
198200 type Agg struct {199201 sum float64200202 prod float64201- count int // non-empty values203+ count int // non-empty values other than errors202204 nums int203205 min, max float64206+ // counting is set for COUNT and COUNTA, which count errors (errs)207+ // where the others stop at the first.208+ counting bool209+ errs int204210 }205211 206212 // each calls fn for every value in args: every cell of a range that@@ -264,10 +270,15 @@ func eachEntry(a *Array, fn func(v Value, direct bool) *Value) *Value {
264270 }265271 266272 // Add counts v into the aggregate, with SUM's rules: blanks are skipped,267-// errors returned, and text counts only when given directly.273+// errors returned (counted, when counting), and text counts only when274+// given directly.268275 func (s *Agg) Add(v Value, direct bool) *Value {269276 switch v.Kind {270277 case value.Error:278+ if s.counting {279+ s.errs++280+ return nil281+ }271282 return errOf(v)272283 case value.Empty:273284 return nil@@ -298,8 +309,21 @@ func NewAgg() Agg { return Agg{prod: 1, min: math.Inf(1), max: math.Inf(-1)} }
298309 // read each cell once; other ranges the engine adds up itself299310 // (Book.Fold), as it reads them.300311 func aggregate(done func(Agg) Value) func([]Node, lookup) Value {312+ return aggregateFrom(NewAgg(), done)313+}314+315+// counter builds COUNT and COUNTA, which read past errors, as Sheets316+// does: COUNT skips them and COUNTA counts them. Their ranges are read317+// directly, as the shared aggregates stop at an error.318+func counter(done func(Agg) Value) func([]Node, lookup) Value {319+ s := NewAgg()320+ s.counting = true321+ return aggregateFrom(s, done)322+}323+324+func aggregateFrom(start Agg, done func(Agg) Value) func([]Node, lookup) Value {301325 return func(args []Node, get lookup) Value {302- s := NewAgg()326+ s := start303327 for _, arg := range args {304328 if rn, ok := get.refOf(arg).(formula.Range); ok {305329 var e *Value@@ -319,7 +343,7 @@ func aggregate(done func(Agg) Value) func([]Node, lookup) Value {
319343 // rangeAgg adds the range rn to s: from the running aggregates when s is320344 // empty and the range is shared, and otherwise as the engine reads it.321345 func rangeAgg(rn formula.Range, s Agg, get lookup) (Agg, *Value) {322- if s.count == 0 {346+ if s.count == 0 && !s.counting {323347 if a, e, ok := get.book.RangeAgg(rn.Sheet, rn.Rect); ok {324348 return a, e325349 }@@ -337,12 +361,12 @@ func logical(test func(trues, n int) bool) func([]Node, lookup) Value {
337361 case v.Kind == value.Empty, v.Kind == value.Text && !direct:338362 return nil339363 }340- f, err := toNum(v)364+ t, err := truth(v)341365 if err != nil {342366 return err343367 }344368 n++345- if f != 0 {369+ if t {346370 trues++347371 }348372 return nil@@ -13,7 +13,7 @@ import (
1313 1414 func init() {1515 define(16- &FuncDef{Name: "LET", Args: "name1, value_expression1, [name2, value_expression2, ...], formula_expression", Desc: "Name values for use in a formula", Min: 3, Max: -1, step: 2,16+ &FuncDef{Name: "LET", Args: "name1, value_expression1, [name2, value_expression2, ...], formula_expression", Desc: "Name values for use in a formula; an error counts only where its name is used", Min: 3, Max: -1, step: 2,1717 eval: let, arrays: takesArrays, binds: formula.BindLet, format: func(args []Node, infer func(Node) Format) Format { return infer(args[len(args)-1]) }},1818 &FuncDef{Name: "LAMBDA", Args: "[name, ...], formula_expression", Desc: "A function of names, called with values: LAMBDA(x, x*2)(3)", Min: 1, Max: -1,1919 eval: makeLambda, arrays: takesArrays, binds: formula.BindLambda},@@ -37,11 +37,9 @@ func let(args []Node, get lookup) Value {
3737 defer func() { get.scope = get.scope[:outer] }()3838 last := len(args) - 13939 for i := 0; i < last; i += 2 {40- b := get.bind(args[i].(formula.Local).Name, args[i+1])41- if b.ref == nil && b.val.Kind == value.Error {42- return b.val43- }44- get.scope = append(get.scope, b)40+ // An error is the result only where a name bound to it is used:41+ // LET(x, 1/0, 5) is 5, as in Sheets.42+ get.scope = append(get.scope, get.bind(args[i].(formula.Local).Name, args[i+1]))4543 }4644 return eval(args[last], get)4745 }@@ -88,7 +88,8 @@ func liftCall(f *FuncDef, args []Node, get lookup) Value {
8888 lift, want := get.lift, get.wantArr8989 get.lift, get.wantArr = 0, f.arrays == liftPass9090 defer func() { get.lift, get.wantArr = lift, want }()91- v := get.first(f.call(stand, get))91+ whole := f.call(stand, get)92+ v := get.first(whole)9293 var s shape9394 var arrs []*Array9495 for i, r := range fr.read {@@ -99,6 +100,9 @@ func liftCall(f *FuncDef, args []Node, get lookup) Value {
99100 }100101 }101102 if len(arrs) == 0 {103+ if f.arrays == liftPass {104+ return whole // mapped over nothing: an array it returns stays whole105+ }102106 return v103107 }104108 s.data(arrs...)@@ -1,6 +1,10 @@
11 package functions22 3-import "github.com/FelineStateMachine/012/internal/value"3+import (4+ "strings"5+6+ "github.com/FelineStateMachine/012/internal/value"7+)48 59 func init() {610 define(@@ -33,11 +37,11 @@ func init() {
3337 3438 func ifs(args []Node, get lookup) Value {3539 for i := 0; i+1 < len(args); i += 2 {36- c, err := numArg(args[i], get)40+ c, err := truth(eval1(args[i], get))3741 if err != nil {3842 return *err3943 }40- if c != 0 {44+ if c {4145 return eval(args[i+1], get)4246 }4347 }@@ -97,3 +101,22 @@ func sameKind(a, b Value) bool {
97101 }98102 return ka == kb99103 }104+105+// truth reads v as a condition, as Sheets does: a number is true unless106+// 0, a blank or "" is false, and text reads as TRUE, FALSE or a number,107+// so IF("TRUE", 1, 2) is 1; other text is #VALUE!.108+func truth(v Value) (bool, *Value) {109+ switch v.Kind {110+ case value.Error:111+ return false, errOf(v)112+ case value.Text:113+ switch {114+ case v.Str == "", strings.EqualFold(v.Str, "FALSE"):115+ return false, nil116+ case strings.EqualFold(v.Str, "TRUE"):117+ return true, nil118+ }119+ }120+ f, err := toNum(v)121+ return f != 0, err122+}@@ -14,16 +14,16 @@ func init() {
1414 eval: func(args []Node, get lookup) Value { return tableLookup(args, get, false) }},1515 &FuncDef{Name: "MATCH", Args: "search_key, range, [search_type]", Desc: "Position of a key in a row or column", Min: 2, Max: 3,1616 eval: match},17- &FuncDef{Name: "INDEX", Args: "reference, [row], [column]", Desc: "The value at a row and column of a range", Min: 1, Max: 3,17+ &FuncDef{Name: "INDEX", Args: "reference, [row], [column]", Desc: "The value at a row and column of a range; row or column 0 for a whole column or row", Min: 1, Max: 3, arrays: liftPass,1818 eval: index, format: inheritFrom(0)},1919 &FuncDef{Name: "XLOOKUP", Args: "search_key, lookup_range, result_range, [missing_value], [match_mode], [search_mode]", Desc: "Find a key and return the matching entry of another range", Min: 3, Max: 6,2020 eval: xlookup, format: inheritFrom(2)},2121 &FuncDef{Name: "CHOOSE", Args: "index, choice1, [choice2, ...]", Desc: "The choice at a position", Min: 2, Max: -1, arrays: liftPass,2222 eval: choose},2323 &FuncDef{Name: "ROWS", Args: "range", Desc: "Number of rows in a range", Min: 1, Max: 1,24- eval: func(args []Node, get lookup) Value { return num(float64(matrixArg(args[0], get).rows)) }},24+ eval: func(args []Node, get lookup) Value { return measure(args[0], get, true) }},2525 &FuncDef{Name: "COLUMNS", Args: "range", Desc: "Number of columns in a range", Min: 1, Max: 1,26- eval: func(args []Node, get lookup) Value { return num(float64(matrixArg(args[0], get).cols)) }},26+ eval: func(args []Node, get lookup) Value { return measure(args[0], get, false) }},2727 )2828 }2929 @@ -83,7 +83,7 @@ func index(args []Node, get lookup) Value {
8383 }8484 switch {8585 case row > m.rows || col > m.cols:86- return value.ErrRef86+ return value.ErrNum // as Sheets; Excel says #REF!8787 case row == 0 || col == 0: // a whole column, row or both: an array8888 return get.arrayValue(m.slice(row, col))8989 }@@ -113,13 +113,26 @@ func (m matrix) slice(row, col int) *Array {
113113 return a114114 }115115 116+// measure is ROWS, or COLUMNS when rows is false. An error in place of117+// a range or array is the answer: ROWS(SEQUENCE(0)) is #NUM!.118+func measure(n Node, get lookup, rows bool) Value {119+ m := matrixArg(n, get)120+ switch {121+ case !m.ref && m.rows == 1 && m.cols == 1 && m.blank.Kind == value.Error:122+ return m.blank123+ case rows:124+ return num(float64(m.rows))125+ }126+ return num(float64(m.cols))127+}128+116129 func choose(args []Node, get lookup) Value {117130 i, err := intArg(args, 0, 0, get)118131 if err != nil {119132 return *err120133 }121134 if i < 1 || i >= len(args) {122- return value.ErrValue135+ return value.ErrNum // as Sheets; Excel says #VALUE!123136 }124137 return eval(args[i], get)125138 }@@ -281,8 +281,8 @@ func sumProductTerms(args []Node, get lookup, term func(fs []float64)) *Value {
281281 switch v := m.cell(pc.r, pc.c); v.Kind {282282 case value.Error:283283 return &v284- case value.Number:285- fs[i] = v.Num284+ case value.Number, value.Bool:285+ fs[i] = v.Num // TRUE is 1, as in Sheets286286 default:287287 fs[i] = 0288288 }@@ -14,7 +14,7 @@ func init() {
1414 eval: sumIf, format: inheritFrom(2)},1515 &FuncDef{Name: "SUMIFS", Args: "sum_range, criteria_range1, criterion1, [criteria_range2, criterion2, ...]", Desc: "Sum of the cells that meet every condition", Min: 3, Max: -1, step: 2,1616 eval: sumIfs, format: inheritFrom(0)},17- &FuncDef{Name: "SUMPRODUCT", Args: "array1, [array2, ...]", Desc: "Sum of the products of matching entries", Min: 1, Max: -1,17+ &FuncDef{Name: "SUMPRODUCT", Args: "array1, [array2, ...]", Desc: "Sum of the products of matching entries, TRUE counting as 1", Min: 1, Max: -1,1818 eval: sumProduct},1919 &FuncDef{Name: "PRODUCT", Args: "factor1, [factor2, ...]", Desc: "Product of numbers", Min: 1, Max: -1,2020 eval: aggregate(func(s Agg) Value {@@ -23,7 +23,7 @@ func init() {
2323 }2424 return num(s.prod)2525 })},26- &FuncDef{Name: "POWER", Args: "base, exponent", Desc: "A number raised to a power", Min: 2, Max: 2,26+ &FuncDef{Name: "POWER", Args: "base, exponent", Desc: "A number raised to a power, odd roots of negatives included", Min: 2, Max: 2,2727 eval: numeric(func(x []float64) Value { return powerOf(x[0], x[1]) })},2828 &FuncDef{Name: "ROUNDUP", Args: "value, [places]", Desc: "Round away from zero", Min: 1, Max: 2,2929 eval: rounder(numfmt.Up), format: inheritFrom(0)},@@ -102,7 +102,21 @@ func powerOf(b, e float64) Value {
102102 if b == 0 && e < 0 {103103 return value.ErrDiv0104104 }105- return num(math.Pow(b, e))105+ return num(power(b, e))106+}107+108+// power is b raised to e, as POWER and ^ compute it. A negative base109+// has a real odd root, as in Sheets: (-8)^(1/3) is -2, where math.Pow110+// gives NaN (#NUM!). Other fractional powers of a negative stay NaN.111+func power(b, e float64) float64 {112+ if b >= 0 || e == math.Trunc(e) {113+ return math.Pow(b, e)114+ }115+ n := math.Round(1 / e) // e is 1/n, or near it: 1/3 isn't exact116+ if math.Abs(1/e-n) > 1e-9*math.Abs(n) || math.Mod(n, 2) == 0 {117+ return math.NaN()118+ }119+ return -math.Pow(-b, e)106120 }107121 108122 // toMultiple builds CEILING and FLOOR with Sheets semantics: the factor@@ -3,6 +3,10 @@ package functions
33 import (44 "strings"55 "unicode"6+ "unicode/utf8"7+8+ "golang.org/x/text/cases"9+ "golang.org/x/text/language"610 711 "github.com/FelineStateMachine/012/internal/numfmt"812 "github.com/FelineStateMachine/012/internal/value"@@ -25,10 +29,10 @@ func init() {
2529 eval: textSlice(func(r []rune, n int) string { return string(r[len(r)-min(n, len(r)):]) })},2630 &FuncDef{Name: "MID", Args: "string, starting_at, extract_length", Desc: "Characters from the middle of text", Min: 3, Max: 3,2731 eval: mid},28- &FuncDef{Name: "LEN", Args: "text", Desc: "Number of characters in text", Min: 1, Max: 1,32+ &FuncDef{Name: "LEN", Args: "text", Desc: "Number of characters in text: an emoji is one, where Sheets counts two", Min: 1, Max: 1,2933 eval: textFn(func(s string) Value { return num(float64(runeLen(s))) })},30- &FuncDef{Name: "UPPER", Args: "text", Desc: "Text in upper case", Min: 1, Max: 1,31- eval: textFn(func(s string) Value { return str(strings.ToUpper(s)) })},34+ &FuncDef{Name: "UPPER", Args: "text", Desc: "Text in upper case: ß is SS", Min: 1, Max: 1,35+ eval: textFn(func(s string) Value { return str(upper(s)) })},3236 &FuncDef{Name: "LOWER", Args: "text", Desc: "Text in lower case", Min: 1, Max: 1,3337 eval: textFn(func(s string) Value { return str(strings.ToLower(s)) })},3438 &FuncDef{Name: "PROPER", Args: "text", Desc: "Text with each word capitalized", Min: 1, Max: 1,@@ -113,7 +117,10 @@ func mid(args []Node, get lookup) Value {
113117 if err != nil {114118 return *err115119 }116- if start < 1 || n < 0 {120+ switch {121+ case start < 1:122+ return value.ErrNum // as Sheets; Excel says #VALUE!123+ case n < 0:117124 return value.ErrValue118125 }119126 r := []rune(s)@@ -123,6 +130,18 @@ func mid(args []Node, get lookup) Value {
123130 return str(string(r[start-1 : min(start-1+n, len(r))]))124131 }125132 133+// upper is UPPER's full case mapping, which Sheets uses: "straße" is134+// "STRASSE", where mapping each letter alone would keep the ß. A Caser135+// keeps state, so each call makes its own, and ASCII needs none.136+func upper(s string) string {137+ for i := 0; i < len(s); i++ {138+ if s[i] >= utf8.RuneSelf {139+ return cases.Upper(language.Und).String(s)140+ }141+ }142+ return strings.ToUpper(s)143+}144+126145 func trimSpaces(s string) Value {127146 return str(strings.Join(strings.FieldsFunc(s, func(r rune) bool { return r == ' ' }), " "))128147 }@@ -162,6 +181,9 @@ func textFormat(args []Node, get lookup) Value {
162181 if err != nil {163182 return *err164183 }184+ if v.Kind == value.Bool {185+ return str(v.String()) // TRUE stays TRUE whatever the format, as in Sheets186+ }165187 if v.Kind == value.Text {166188 n, _, ok := value.ParseValue(v.Str)167189 if !ok {@@ -36,12 +36,13 @@ func TestDecimalArithmetic(t *testing.T) {
3636 binary, decim string3737 }{3838 {"sum of two", "=0.1+0.2", nil, "0.30000000000000004", "0.3"},39- {"comparison", "=0.1+0.2=0.3", nil, "FALSE", "TRUE"},39+ {"comparison", "=0.1+0.2=0.3", nil, "TRUE", "TRUE"}, // at the 15 digits shown40+ {"comparison with zero", "=0.1+0.2-0.3=0", nil, "FALSE", "TRUE"},4041 {"residue", "=1.1-1-0.1", nil, "8.326672684688674E-17", "0"},4142 {"product", "=0.07*3", nil, "0.21000000000000002", "0.21"},4243 {"cents to whole", "=INT(A1*100)", []string{"$4.35"}, "434", "435"},4344 {"currency sum", "=SUM(A1:A10)", tenCents, "0.9999999999999999", "1"},44- {"currency sum equals", "=SUM(A1:A10)=1", tenCents, "FALSE", "TRUE"},45+ {"currency sum equals", "=SUM(A1:A10)-1=0", tenCents, "FALSE", "TRUE"},4546 {"average", "=AVERAGE(0.1, 0.2)", nil, "0.15000000000000002", "0.15"},4647 {"division", "=0.3/0.1", nil, "2.9999999999999996", "3"},4748 {"postfix percent", "=1.1%", nil, "0.011000000000000001", "0.011"},@@ -77,7 +78,7 @@ func TestDecimalArithmetic(t *testing.T) {
7778 {"product of text", `=PRODUCT("a")`, nil, "#VALUE!", "#VALUE!"},7879 {"huge product falls back", "=PRODUCT(1E308, 10)", nil, "#NUM!", "#NUM!"},7980 {"sumif", `=SUMIF(A1:A10, ">0")`, tenCents, "0.9999999999999999", "1"},80- {"sumif equals", `=SUMIF(A1:A10, ">0")=1`, tenCents, "FALSE", "TRUE"},81+ {"sumif equals", `=SUMIF(A1:A10, ">0")-1=0`, tenCents, "FALSE", "TRUE"},8182 {"sumif with sum range", `=SUMIF(A1:A3, "<>x", A1:A3)`, []string{"0.1", "x", "0.2"}, "0.30000000000000004", "0.3"},8283 {"sumif error", `=SUMIF(A1:A2, ">0")`, []string{"1", "=1/0"}, "1", "1"},8384 {"sumifs", `=SUMIFS(A1:A10, A1:A10, "0.1")`, tenCents, "0.9999999999999999", "1"},@@ -110,7 +111,7 @@ func TestDecimalArithmetic(t *testing.T) {
110111 // Turning the setting on and off recalculates, is one undo step, and111112 // undo and redo recalculate too.112113 func TestDecimalUndo(t *testing.T) {113- s := decimalSheet(t, false, "=0.1+0.2=0.3")114+ s := decimalSheet(t, false, "=0.1+0.2-0.3=0")114115 s.ClearHistory()115116 check := func(when string, want bool) {116117 t.Helper()@@ -85,7 +85,7 @@ func TestArrayFunctions(t *testing.T) {
8585 {"=SEQUENCE(3)", []string{"1", "2", "3"}},8686 {"=SEQUENCE(2, 3)", []string{"1|2|3", "4|5|6"}},8787 {"=SEQUENCE(2, 2, 10, -5)", []string{"10|5", "0|-5"}},88- {"=SEQUENCE(0)", []string{"#VALUE!"}},88+ {"=SEQUENCE(0)", []string{"#NUM!"}}, // as Sheets8989 {"=FILTER(D1:D5, C1:C5>3)", []string{"date", "Apple"}},9090 {`=FILTER(C1:D5, B1:B5="north", C1:C5>1)`, []string{"3|Cherry", "5|Apple"}},9191 {"=FILTER(C1:E1, {TRUE,FALSE,TRUE})", []string{"1|1.5"}},@@ -25,8 +25,8 @@ func TestLookupFunctions(t *testing.T) {
2525 {`=MATCH(2, C1:D2, 0)`, ErrNA}, // not a single row or column2626 {"=INDEX(D1:E3, 2, 2)", num(2.5)},2727 {"=INDEX(D1:D3, 3)", txt("Cherry")},28- {"=INDEX(C1:E1, 2)", txt("Apple")}, // a single row counts across29- {"=INDEX(D1:E3, 4, 1)", ErrRef},28+ {"=INDEX(C1:E1, 2)", txt("Apple")}, // a single row counts across29+ {"=INDEX(D1:E3, 4, 1)", ErrNum}, // as Sheets; Excel says #REF!3030 {"=INDEX(D1:E3, 2)", txt("Banana")}, // a whole row, spilled3131 {`=XLOOKUP("Cherry", D1:D3, E1:E3)`, num(3.5)},3232 {`=XLOOKUP("kiwi", D1:D3, E1:E3)`, ErrNA},@@ -39,7 +39,8 @@ func TestLookupFunctions(t *testing.T) {
3939 {`=XLOOKUP(1, C1:C3, E1:E2)`, ErrValue},4040 {`=CHOOSE(2, "a", "b", "c")`, txt("b")},4141 {`=CHOOSE(2.9, "a", "b", "c")`, txt("b")},42- {`=CHOOSE(4, "a", "b", "c")`, ErrValue},42+ {`=CHOOSE(4, "a", "b", "c")`, ErrNum},43+ {`=CHOOSE(0, "a")`, ErrNum},4344 {`=CHOOSE(1, "a", 1/0)`, txt("a")}, // only the chosen value is evaluated4445 {"=ROWS(A1:C7)", num(7)},4546 {"=COLUMNS(A1:C7)", num(3)},@@ -24,7 +24,12 @@ func TestMathFunctions(t *testing.T) {
2424 {"=PRODUCT(2, \"3\")", num(6)},2525 {"=POWER(2, 10)", num(1024)},2626 {"=POWER(0, -1)", ErrDiv0},27- {"=POWER(-8, 1/3)", ErrNum},27+ {"=POWER(-8, 1/3)", num(-2)}, // an odd root, as Sheets28+ {"=(-8)^(1/3)", num(-2)},29+ {"=POWER(-32, 0.2)", num(-2)},30+ {"=POWER(-8, 0.5)", ErrNum},31+ {"=POWER(-8, 2/3)", ErrNum},32+ {"=POWER(-8, 2)", num(64)},2833 {"=ROUND(1.005, 2)", num(1.01)},2934 {"=ROUND(-2.5)", num(-3)},3035 {"=ROUND(1234.5, -2)", num(1200)},@@ -0,0 +1,67 @@
1+package sheet2+3+import "testing"4+5+// Where Sheets and Excel differ, 012 follows Sheets (the Sheets corpus in6+// internal/fileio checks these against Sheets itself).7+func TestSheetsSemantics(t *testing.T) {8+ s := fixture(t)9+ for a, in := range map[string]string{"J1": "'3", "J2": "=1/0", "J3": "FALSE", "J4": "=NA()"} {10+ if err := s.Set(at(a), in); err != nil {11+ t.Fatal(err)12+ }13+ }14+ checkFormulas(t, s, []fnCase{15+ // Text.16+ {`=UPPER("straße")`, txt("STRASSE")},17+ {`=UPPER("dž")`, txt("DŽ")},18+ {`=TEXT(TRUE, "0")`, txt("TRUE")},19+ {`=TEXT(FALSE, "0.00")`, txt("FALSE")},20+ {`=TEXT(0.75, "# ?/?")`, txt("3/4")},21+ {`=TEXT(2.5, "# ?/?")`, txt("2 1/2")},22+ // Numbers compare at the 15 digits a cell shows.23+ {"=0.1+0.2=0.3", boolean(true)},24+ {"=(1-0.9)=0.1", boolean(true)},25+ {"=SUM(0.1, 0.2, 0.3)=0.6", boolean(true)},26+ {"=0.1+0.2<0.3", boolean(false)},27+ {"=0.1+0.2>=0.3", boolean(true)},28+ {"=0.1+0.2-0.3=0", boolean(false)},29+ {"=1.00000000000001=1", boolean(false)},30+ {`=COUNTIF(E1:E3, 0.1+0.2+1.2)`, num(1)},31+ {"=123456789012345678", num(123456789012345000)},32+ {"=123456789012345678=123456789012345000", boolean(true)},33+ {"=ROUND(1.23456789012345, 14)=1.23456789012346", boolean(true)}, // the half added carries34+ {"=ROUND(2.675, 2)", num(2.68)},35+ {"=ROUND(-2.5)", num(-3)},36+ // Errors and conditions.37+ {"=IFERROR(NA())", Value{}},38+ {`=ISBLANK(IFERROR(NA()))`, boolean(true)},39+ {`=IF("TRUE", 1, 2)`, num(1)},40+ {`=IF("false", 1, 2)`, num(2)},41+ {`=IF("", 1, 2)`, num(2)},42+ {`=IF("abc", 1, 2)`, ErrValue},43+ {`=NOT("")`, boolean(true)},44+ {`=NOT("TRUE")`, boolean(false)},45+ {`=AND("TRUE", 1)`, boolean(true)},46+ {"=COUNT(J1:J4)", num(0)},47+ {"=COUNTA(J1:J4)", num(4)},48+ {"=COUNT(1/0, 2)", num(1)},49+ {"=SUM(J1:J4)", ErrDiv0},50+ {"=COUNTIF(J1:J4, 3)", num(1)},51+ {`=COUNTIF(J1:J4, "<>3")`, num(3)},52+ {`=COUNTIF(J1:J4, ">2")`, num(0)},53+ // Positions out of range, and whole rows and columns.54+ {`=CHOOSE(0, "a")`, ErrNum},55+ {"=INDEX(A1:A3, 5)", ErrNum},56+ {"=ROWS(INDEX(A1:C3, 0, 1))", num(3)},57+ {"=COLUMNS(INDEX(A1:C3, 2, 0))", num(3)},58+ {"=SUM(INDEX(C1:E3, 0, 1))", num(6)},59+ {"=ROWS(SEQUENCE(0))", ErrNum},60+ {"=ROWS(1/0)", ErrDiv0},61+ // Arrays and names.62+ {"=SUMPRODUCT(C1:C3>1)", num(2)},63+ {"=SUMPRODUCT(C1:C3>1, C1:C3)", num(5)},64+ {"=LET(x, 1/0, 5)", num(5)},65+ {"=LET(x, 1/0, x+1)", ErrDiv0},66+ })67+}@@ -16,7 +16,7 @@ func TestTextFunctions(t *testing.T) {
1616 {`=RIGHT("hello", 3)`, txt("llo")},1717 {`=MID("spreadsheet", 7, 5)`, txt("sheet")},1818 {`=MID("abc", 5, 2)`, txt("")},19- {`=MID("abc", 0, 2)`, ErrValue},19+ {`=MID("abc", 0, 2)`, ErrNum}, // as Sheets; Excel says #VALUE!2020 {`=LEN("héllo")`, num(5)},2121 {"=LEN(A1)", num(2)},2222 {`=UPPER("abc")`, txt("ABC")},@@ -383,7 +383,7 @@ func TestSingleSheetFileStaysCompatible(t *testing.T) {
383383 func TestDecimalIsWorkbookWide(t *testing.T) {384384 w := bookOf(t,385385 page{"Sheet1", map[string]string{"A1": "=0.1+0.2"}},386- page{"Data", map[string]string{"A1": "=Sheet1!A1=0.3"}},386+ page{"Data", map[string]string{"A1": "=Sheet1!A1-0.3=0"}},387387 )388388 w.SetDecimal(true)389389 expect(t, w, map[string]string{"Sheet1!A1": "0.3", "Data!A1": "TRUE"})@@ -11,7 +11,7 @@ import (
1111 // and Ctrl+Z turns it back off.1212 func TestDecimalSetting(t *testing.T) {1313 m := newModel()14- press(t, m, "=0.1+0.2=0.3", "<enter>")14+ press(t, m, "=0.1+0.2-0.3=0", "<enter>")1515 value := func() string { return m.sheet.Value(addr("A1")).String() }1616 if value() != "FALSE" {1717 t.Fatalf("binary: A1 = %s", value())@@ -9,6 +9,7 @@ package value
99 import (1010 "cmp"1111 "math"12+ "strconv"1213 "strings"1314 1415 "github.com/FelineStateMachine/012/internal/numfmt"@@ -169,3 +170,29 @@ func Compare(l, r Value) int {
169170 }170171 return 0171172 }173+174+// CompareShown is Compare as the comparison operators and criteria see175+// numbers: at the 15 significant digits a cell shows, as Sheets and176+// Excel do, so =0.1+0.2=0.3 is TRUE though the sum is177+// 0.30000000000000004. Sorting and lookups use Compare, which is exact178+// and so orders values consistently.179+func CompareShown(l, r Value) int {180+ d := Compare(l, r)181+ if d != 0 && l.Kind == Number && r.Kind == Number && SameShown(l.Num, r.Num) {182+ return 0183+ }184+ return d185+}186+187+// SameShown reports whether a and b agree to 15 significant digits.188+func SameShown(a, b float64) bool {189+ switch {190+ case a == b:191+ return true192+ case a == 0 || b == 0 || (a < 0) != (b < 0):193+ return false194+ case math.Abs(a-b) > 1e-14*math.Max(math.Abs(a), math.Abs(b)):195+ return false // differ before the 15th digit: the common case, at once196+ }197+ return strconv.FormatFloat(a, 'e', 14, 64) == strconv.FormatFloat(b, 'e', 14, 64)198+}@@ -111,7 +111,10 @@ var formulas = []string{
111111 // Excel disagree, or where excelize departs from Excel. Each says why.112112 var skipped = map[string]string{113113 // Sheets and Excel disagree; 012 follows Sheets.114- `=ROUND(-2.5)`: "Sheets makes ROUND's places optional; Excel requires them",114+ `=ROUND(-2.5)`: "Sheets makes ROUND's places optional; Excel requires them",115+ `=MID("abc", 0, 2)`: "Sheets gives #NUM! for a start before the text; Excel gives #VALUE!",116+ `=INDEX(D1:E3, 4, 1)`: "Sheets gives #NUM! for a row past the range; Excel gives #REF!",117+ `=CHOOSE(4, "a", "b", "c")`: "Sheets gives #NUM! for an index past the choices; Excel gives #VALUE!",115118 116119 // excelize departs from Excel (and Sheets) here.117120 `=SUM(C1:C2, E6)`: "excelize counts TRUE in a referenced cell as 1; Excel and Sheets ignore it",