- commit
- 4cdc742e4327bb56d4790a14d681e9fb4c658257
- parent
- dff4f42f3bde750cee2c99819f0252b41a6aab5c
- parent
- 7d292fece5cc049d9f9a371d907ceb9ed8f4c0f8
- author
- Dami <dami.etoile@gmail.com>
@@ -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@@ -132,7 +132,12 @@ they agree (for two errors, compare C and F by eye). The exporter saves
132132 formulas Excel has nothing like (`SPLIT`, `REGEXMATCH`, `SORTN`,133133 `FLATTEN`, `ARRAYFORMULA` inside a formula) as values, so those rows134134 leave C empty and say so in the note: type the formula from B into C135-before downloading, or `-update-from` leaves them unconfirmed.135+before downloading, or `-update-from` leaves them unconfirmed. It also136+leaves a row unconfirmed where Sheets says `#NAME?` and 012 doesn't: the137+import didn't recognize a function, as Sheets doesn't read `FILTER` and138+`SORT` written with the `_xlfn._xlws.` prefix Excel gives them. A number139+Sheets downloads with a trailing point (`110.` for `=100*1.1`) is read140+without it.136141 137142 ## The XLSX differential test138143 @@ -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`), and one reached only through an `IF` branch not taken or a part of a range the formula doesn't read (`INDEX`, `VLOOKUP`), 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,20 +20,20 @@ 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 |3535 | `PI()` | The number pi |36-| `ROUND(value, [places])` | Round to a number of decimal places, halves away from zero |36+| `ROUND(value, [places])` | Round to a number of decimal places, halves away from zero, on the digits a cell shows: ROUND(1.5, 14) is 1.5 |3737 | `SQRT(value)` | Square root |3838 | `SUM(value1, [value2, ...])` | Sum of numbers |3939 | `TRUE()` | The logical value 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 }@@ -14,7 +14,7 @@
1414 <span style="color:var(--vt-palette-8)">│</span> AVERAGE<span style="color:var(--vt-palette-4);font-weight:bold">IF</span>(criteria_range, criterion, [average_range]) <span style="color:var(--vt-palette-0)">Average</span> <span style="color:var(--vt-palette-0)">of</span> <span style="color:var(--vt-palette-0)">the</span> <span style="color:var(--vt-palette-0)">cells</span> <span style="color:var(--vt-palette-0)">that</span> <span style="color:var(--vt-palette-0)">meet</span> <span style="color:var(--vt-palette-0)">a</span> <span style="color:var(--vt-palette-0)">…</span> <span style="color:var(--vt-palette-8)">│</span>1515 <span style="color:var(--vt-palette-8)">│</span> AVERAGE<span style="color:var(--vt-palette-4);font-weight:bold">IF</span>S(average_range, criteria_range1, criterion1, [c… <span style="color:var(--vt-palette-0)">Average</span> <span style="color:var(--vt-palette-0)">of</span> <span style="color:var(--vt-palette-0)">the</span> <span style="color:var(--vt-palette-0)">cells</span> <span style="color:var(--vt-palette-0)">that</span> <span style="color:var(--vt-palette-0)">meet</span> <span style="color:var(--vt-palette-0)">ev…</span> <span style="color:var(--vt-palette-8)">│</span>1616 <span style="color:var(--vt-palette-8)">│</span> JEV.CLASS<span style="color:var(--vt-palette-4);font-weight:bold">IF</span>Y(value, question, labels, [descriptions]) <span style="color:var(--vt-palette-0)">The</span> <span style="color:var(--vt-palette-0)">label</span> <span style="color:var(--vt-palette-0)">that</span> <span style="color:var(--vt-palette-0)">fits</span> <span style="color:var(--vt-palette-0)">best,</span> <span style="color:var(--vt-palette-0)">chosen</span> <span style="color:var(--vt-palette-0)">…</span> <span style="color:var(--vt-palette-8)">│</span>17-<span style="color:var(--vt-palette-8)">└──────────────────────────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-0)">80</span> <span style="color:var(--vt-palette-0)">of</span> <span style="color:var(--vt-palette-0)">142</span> <span style="color:var(--vt-palette-8)">─┘</span>17+<span style="color:var(--vt-palette-8)">└──────────────────────────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-0)">81</span> <span style="color:var(--vt-palette-0)">of</span> <span style="color:var(--vt-palette-0)">142</span> <span style="color:var(--vt-palette-8)">─┘</span>1818 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">14</span><span style="background:var(--vt-palette-7)"> </span>1919 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">15</span><span style="background:var(--vt-palette-7)"> </span>2020 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">16</span><span style="background:var(--vt-palette-7)"> </span>@@ -14,7 +14,7 @@
1414 <span style="color:var(--vt-palette-8)">│</span> AVERAGE<span style="color:var(--vt-palette-3);font-weight:bold">IF</span>(criteria_range, criterion, [average_range]) <span style="color:var(--vt-palette-8)">Average</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">the</span> <span style="color:var(--vt-palette-8)">cells</span> <span style="color:var(--vt-palette-8)">that</span> <span style="color:var(--vt-palette-8)">meet</span> <span style="color:var(--vt-palette-8)">a</span> <span style="color:var(--vt-palette-8)">…</span> <span style="color:var(--vt-palette-8)">│</span>1515 <span style="color:var(--vt-palette-8)">│</span> AVERAGE<span style="color:var(--vt-palette-3);font-weight:bold">IF</span>S(average_range, criteria_range1, criterion1, [c… <span style="color:var(--vt-palette-8)">Average</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">the</span> <span style="color:var(--vt-palette-8)">cells</span> <span style="color:var(--vt-palette-8)">that</span> <span style="color:var(--vt-palette-8)">meet</span> <span style="color:var(--vt-palette-8)">ev…</span> <span style="color:var(--vt-palette-8)">│</span>1616 <span style="color:var(--vt-palette-8)">│</span> JEV.CLASS<span style="color:var(--vt-palette-3);font-weight:bold">IF</span>Y(value, question, labels, [descriptions]) <span style="color:var(--vt-palette-8)">The</span> <span style="color:var(--vt-palette-8)">label</span> <span style="color:var(--vt-palette-8)">that</span> <span style="color:var(--vt-palette-8)">fits</span> <span style="color:var(--vt-palette-8)">best,</span> <span style="color:var(--vt-palette-8)">chosen</span> <span style="color:var(--vt-palette-8)">…</span> <span style="color:var(--vt-palette-8)">│</span>17-<span style="color:var(--vt-palette-8)">└──────────────────────────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">80</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">142</span> <span style="color:var(--vt-palette-8)">─┘</span>17+<span style="color:var(--vt-palette-8)">└──────────────────────────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">81</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">142</span> <span style="color:var(--vt-palette-8)">─┘</span>1818 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">14</span><span style="background:var(--vt-palette-8)"> </span>1919 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">15</span><span style="background:var(--vt-palette-8)"> </span>2020 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">16</span><span style="background:var(--vt-palette-8)"> </span>@@ -6,6 +6,7 @@ import (
66 "os"77 "slices"88 "strconv"9+ "strings"910 "testing"1011 1112 "github.com/FelineStateMachine/012/internal/sheet"@@ -59,8 +60,8 @@ func excelKeeps(formula string) bool {
5960 6061 // foldSheets records Sheets' results from name, a CSV download of the6162 // corpus sheet after importing the XLSX workbook into Sheets. Rows left62-// out of the download, and formulas saved as values that weren't typed63-// in, stay as they are.63+// out of the download, formulas saved as values that weren't typed in,64+// and functions the import didn't recognize stay as they are.6465 func foldSheets(t *testing.T, rows []*corpusLine, name string) {6566 t.Helper()6667 f, err := os.Open(name)@@ -78,13 +79,22 @@ func foldSheets(t *testing.T, rows []*corpusLine, name string) {
7879 sheets := map[string]string{}7980 for _, rec := range recs[1:] {8081 if len(rec) > colKind {81- sheets[rec[colFormula]] = encodeResult(rec[colText], rec[colKind])82+ text := rec[colText]83+ if rec[colKind] == "number" {84+ // Sheets writes a number a hair off a whole one, such as85+ // 100*1.1, as "110." when it rounds to 15 digits.86+ text = strings.TrimSuffix(text, ".")87+ }88+ sheets[rec[colFormula]] = encodeResult(text, rec[colKind])8289 }8390 }8491 skipped := 08592 for _, r := range rows {8693 got, ok := sheets[r.formula]87- if !ok || (got == "(blank)" && !excelKeeps(r.formula)) {94+ // #NAME? where 012 knows every name means the import didn't95+ // recognize a function (Sheets reads Excel's _xlfn._xlws.FILTER96+ // so): the formula wasn't computed.97+ if !ok || (got == "(blank)" && !excelKeeps(r.formula)) || (got == "#NAME?" && r.ours != got) {8898 skipped++8999 continue90100 }@@ -6,315 +6,315 @@
66 # Every formula computes one value; array results are measured with ROWS,77 # COLUMNS, INDEX or TEXTJOIN. Data! is the fixture sheet (sheetscorpus_test.go).88 ## Dates and times9-=DATE(2026, 9, 26) 46291 unconfirmed10-=DATE(2026, 14, 1) 46419 unconfirmed11-=DATE(2026, 1, 0) 46022 unconfirmed12-=DATE(26, 1, 1) 9498 unconfirmed13-=DATE(1899, 12, 30) 693961 unconfirmed14-=DATE(1900, 1, 1) 2 unconfirmed15-=DATE(1900, 2, 29) 61 unconfirmed16-=DATE(-1, 1, 1) #NUM! unconfirmed17-=DATE("2026", "9", "26") 46291 unconfirmed18-=TIME(14, 30, 0) 0.604166666666667 unconfirmed19-=TIME(25, 0, 0) 0.0416666666666667 unconfirmed20-=TIME(0, -1, 0) #NUM! unconfirmed21-=YEAR(0) 1899 unconfirmed22-=DAY(Data!A5) 30 unconfirmed23-=YEAR(Data!A4) #VALUE! unconfirmed24-=WEEKDAY(Data!F1) 7 unconfirmed25-=WEEKDAY(Data!F1, 2) 6 unconfirmed26-=WEEKDAY(Data!F1, 4) #NUM! unconfirmed27-=HOUR(0.99999999) 0 unconfirmed28-=HOUR(1.75) 18 unconfirmed29-=MINUTE("14:30:59") 30 unconfirmed30-=SECOND(59.6/86400) 0 unconfirmed31-=EDATE(DATE(2024, 2, 29), 12) 45716 unconfirmed32-=EDATE(Data!F1, 1.9) 46321 unconfirmed33-=EOMONTH(DATE(2026, 1, 31), 1) 46081 unconfirmed34-=DATEDIF(Data!F4, Data!F1, "MD") 28 unconfirmed35-=DATEDIF(Data!F4, Data!F1, "YD") 210 unconfirmed36-=DATEDIF(DATE(2024, 1, 31), DATE(2024, 3, 1), "MD") -1 unconfirmed37-=DATEDIF(Data!F2, Data!F5, "D") #NUM! unconfirmed38-=DAYS(Data!F1, Data!F2) 238 unconfirmed39-=NETWORKDAYS(Data!F5, Data!F2, Data!F5) 22 unconfirmed40-=NETWORKDAYS(Data!F1, Data!F1) 0 unconfirmed41-=DATEVALUE("9/26/2026") 46291 unconfirmed42-=DATEVALUE("Sep 26, 2026") 46291 unconfirmed43-=DATEVALUE("9/26/2026 18:00") 46291 unconfirmed44-=DATEVALUE("2/30/2026") #VALUE! unconfirmed45-=DATEVALUE(46291) #VALUE! unconfirmed46-=TIMEVALUE("2:30 PM") 0.604166666666667 unconfirmed47-=TIMEVALUE("9/26/2026 6:00") 0.25 unconfirmed48-=TIMEVALUE("noon") #VALUE! unconfirmed49-="2026-09-27"-Data!F1 1 unconfirmed50-=Data!F1+Data!F3 46291.6041666667 unconfirmed51-=Data!F1="2026-09-26" FALSE unconfirmed9+=DATE(2026, 9, 26) 46291 4629110+=DATE(2026, 14, 1) 46419 4641911+=DATE(2026, 1, 0) 46022 4602212+=DATE(26, 1, 1) 9498 949813+=DATE(1899, 12, 30) 693961 69396114+=DATE(1900, 1, 1) 2 215+=DATE(1900, 2, 29) 61 6116+=DATE(-1, 1, 1) #NUM! #NUM!17+=DATE("2026", "9", "26") 46291 4629118+=TIME(14, 30, 0) 0.604166666666667 0.60416666666666719+=TIME(25, 0, 0) 0.0416666666666667 0.041666666666666720+=TIME(0, -1, 0) #NUM! #NUM!21+=YEAR(0) 1899 189922+=DAY(Data!A5) 30 3023+=YEAR(Data!A4) #VALUE! #VALUE!24+=WEEKDAY(Data!F1) 7 725+=WEEKDAY(Data!F1, 2) 6 626+=WEEKDAY(Data!F1, 4) #NUM! #NUM!27+=HOUR(0.99999999) 0 028+=HOUR(1.75) 18 1829+=MINUTE("14:30:59") 30 3030+=SECOND(59.6/86400) 0 031+=EDATE(DATE(2024, 2, 29), 12) 45716 4571632+=EDATE(Data!F1, 1.9) 46321 4632133+=EOMONTH(DATE(2026, 1, 31), 1) 46081 4608134+=DATEDIF(Data!F4, Data!F1, "MD") 28 2835+=DATEDIF(Data!F4, Data!F1, "YD") 210 21036+=DATEDIF(DATE(2024, 1, 31), DATE(2024, 3, 1), "MD") -1 -137+=DATEDIF(Data!F2, Data!F5, "D") #NUM! #NUM!38+=DAYS(Data!F1, Data!F2) 238 23839+=NETWORKDAYS(Data!F5, Data!F2, Data!F5) 22 2240+=NETWORKDAYS(Data!F1, Data!F1) 0 041+=DATEVALUE("9/26/2026") 46291 4629142+=DATEVALUE("Sep 26, 2026") 46291 4629143+=DATEVALUE("9/26/2026 18:00") 46291 4629144+=DATEVALUE("2/30/2026") #VALUE! #VALUE!45+=DATEVALUE(46291) #VALUE! #VALUE!46+=TIMEVALUE("2:30 PM") 0.604166666666667 0.60416666666666747+=TIMEVALUE("9/26/2026 6:00") 0.25 0.2548+=TIMEVALUE("noon") #VALUE! #VALUE!49+="2026-09-27"-Data!F1 1 150+=Data!F1+Data!F3 46291.6041666667 46291.604166666751+=Data!F1="2026-09-26" FALSE FALSE5252 ## Text53-=CONCATENATE("a", 1, TRUE) "a1TRUE" unconfirmed54-=CONCATENATE(Data!A1:A3) "102030" unconfirmed55-=TEXTJOIN(", ", TRUE, Data!A3:A5) "30, apple" unconfirmed56-=TEXTJOIN(",", FALSE, Data!B5:B7) "north,,Ab*d" unconfirmed57-=LEFT("hi", -1) #VALUE! unconfirmed58-=LEFT(12345, 2) "12" unconfirmed59-=LEFT(TRUE, 1) "T" unconfirmed60-=MID("abc", 5, 2) "" unconfirmed61-=MID("abc", 0, 2) #VALUE! unconfirmed62-=LEN(Data!F1) 5 unconfirmed63-=LEN(1/4) 4 unconfirmed64-=LEN("héllo") 5 unconfirmed65-=LEN("😀") 1 unconfirmed66-=LEFT("日本語", 2) "日本" unconfirmed67-=UPPER("straße") "STRAßE" unconfirmed68-=UPPER(1.5) "1.5" unconfirmed69-=PROPER("hello wORLD it's") "Hello World It'S" unconfirmed70-=PROPER("o'neil mcdonald 3rd") "O'Neil Mcdonald 3Rd" unconfirmed71-=TRIM(" a b ") "a b" unconfirmed72-=SUBSTITUTE("aaa", "aa", "b") "ba" unconfirmed73-=SUBSTITUTE("abc", "", "x") "abc" unconfirmed74-=REPLACE("abc", 10, 1, "X") "abcX" unconfirmed75-=FIND("B", "abc") #VALUE! unconfirmed76-=FIND("", "abc") 1 unconfirmed77-=SEARCH("b?d", "abcbxd") 4 unconfirmed78-=SEARCH("~*", "a*b") 2 unconfirmed79-=REPT("ab", 2.9) "abab" unconfirmed80-=REPT("ab", -1) #VALUE! unconfirmed81-=EXACT(1, "1") TRUE unconfirmed82-=EXACT(TRUE, "TRUE") TRUE unconfirmed83-="a"&Data!A5 "a" unconfirmed84-=1&2 "12" unconfirmed85-=VALUE("1,234.5") 1234.5 unconfirmed86-=VALUE(" 42 ") 42 unconfirmed87-=VALUE("") 0 unconfirmed88-=VALUE("abc") #VALUE! unconfirmed89-=VALUE("9/26/2026") 46291 unconfirmed90-=VALUE(TRUE) #VALUE! unconfirmed53+=CONCATENATE("a", 1, TRUE) "a1TRUE" "a1TRUE"54+=CONCATENATE(Data!A1:A3) "102030" "102030"55+=TEXTJOIN(", ", TRUE, Data!A3:A5) "30, apple" "30, apple"56+=TEXTJOIN(",", FALSE, Data!B5:B7) "north,,Ab*d" "north,,Ab*d"57+=LEFT("hi", -1) #VALUE! #VALUE!58+=LEFT(12345, 2) "12" "12"59+=LEFT(TRUE, 1) "T" "T"60+=MID("abc", 5, 2) "" ""61+=MID("abc", 0, 2) #NUM! #NUM!62+=LEN(Data!F1) 5 9 on purpose: F1 is a date, stored in the XLSX as its serial with a m/d/yyyy format; Sheets' text functions read a date cell as it shows (9/26/2026), 012 reads its serial, as Excel does63+=LEN(1/4) 4 464+=LEN("héllo") 5 565+=LEN("😀") 1 2 on purpose: LEN counts characters (code points), where Sheets counts UTF-16 units66+=LEFT("日本語", 2) "日本" "日本"67+=UPPER("straße") "STRASSE" "STRASSE"68+=UPPER(1.5) "1.5" "1.5"69+=PROPER("hello wORLD it's") "Hello World It'S" "Hello World It'S"70+=PROPER("o'neil mcdonald 3rd") "O'Neil Mcdonald 3Rd" "O'Neil Mcdonald 3Rd"71+=TRIM(" a b ") "a b" "a b"72+=SUBSTITUTE("aaa", "aa", "b") "ba" "ba"73+=SUBSTITUTE("abc", "", "x") "abc" "abc"74+=REPLACE("abc", 10, 1, "X") "abcX" "abcX"75+=FIND("B", "abc") #VALUE! #VALUE!76+=FIND("", "abc") 1 177+=SEARCH("b?d", "abcbxd") 4 478+=SEARCH("~*", "a*b") 2 279+=REPT("ab", 2.9) "abab" "abab"80+=REPT("ab", -1) #VALUE! #VALUE!81+=EXACT(1, "1") TRUE TRUE82+=EXACT(TRUE, "TRUE") TRUE TRUE83+="a"&Data!A5 "a" "a"84+=1&2 "12" "12"85+=VALUE("1,234.5") 1234.5 1234.586+=VALUE(" 42 ") 42 4287+=VALUE("") 0 088+=VALUE("abc") #VALUE! #VALUE!89+=VALUE("9/26/2026") 46291 4629190+=VALUE(TRUE) #VALUE! #VALUE!9191 ## Regular expressions and SPLIT9292 =REGEXMATCH("ABC", "(?i)abc") TRUE unconfirmed9393 =REGEXMATCH(123, "1") #VALUE! unconfirmed9494 =REGEXEXTRACT("abc", "\d+") #N/A unconfirmed95-=TEXTJOIN("|", TRUE, REGEXEXTRACT("a1b2", "(\d)b(\d)")) "1" unconfirmed96-=REGEXREPLACE("john smith", "(\w+) (\w+)", "$2, $1") "smith, john" unconfirmed95+=TEXTJOIN("|", TRUE, REGEXEXTRACT("a1b2", "(\d)b(\d)")) "1|2" unconfirmed96+=REGEXREPLACE("john smith", "(\w+) (\w+)", "$2, $1") "smith, john" "smith, john"9797 =COLUMNS(SPLIT("a,b,,c", ",", TRUE, FALSE)) 4 unconfirmed9898 =SUM(SPLIT("1,2,3", ",")) 6 unconfirmed9999 =COLUMNS(SPLIT("a-b_c", "-_")) 3 unconfirmed100100 ## TEXT and number formats101-=TEXT(1234.567, "$#,##0.00") "$1,234.57" unconfirmed102-=TEXT(1234567.891, "#,##0,") "1,235" unconfirmed103-=TEXT(1234.5, "0.00E+00") "1.23E+03" unconfirmed104-=TEXT(0.0000000001, "0.00E+00") "1.00E-10" unconfirmed105-=TEXT(-1234.5, "#,##0.00;(#,##0.00)") "(1,234.50)" unconfirmed106-=TEXT(0, "0;-0;\z\e\r\o") "zero" unconfirmed107-=TEXT(3, "0.###") "3." unconfirmed108-=TEXT(2.5, "0") "3" unconfirmed109-=TEXT(-0.5, "0") "-1" unconfirmed110-=TEXT(1.005, "0.00") "1.01" unconfirmed111-=TEXT(0.75, "# ?/?") " /1" unconfirmed112-=TEXT("abc", "0") "abc" unconfirmed113-=TEXT("12", "0.00") "12.00" unconfirmed114-=TEXT(TRUE, "0") "1" unconfirmed115-=TEXT(Data!F1, "dddd") "Saturday" unconfirmed116-=TEXT(Data!F1, "mmm d, yyyy") "Sep 26, 2026" unconfirmed117-=TEXT(Data!F1, "dddd, mmmm d, yyyy") "Saturday, September 26, 2026" unconfirmed118-=TEXT(Data!F3, "h:mm AM/PM") "2:30 PM" unconfirmed119-=TEXT(0.75, "h AM/PM") "6 PM" unconfirmed120-=TEXT(Data!F1+Data!F3, "yyyy-mm-dd hh:mm") "2026-09-26 14:30" unconfirmed121-=TEXT(1.5, "[h]:mm") "36:00" unconfirmed122-=TEXT(90/1440, "[mm]:ss") "90:00" unconfirmed101+=TEXT(1234.567, "$#,##0.00") "$1,234.57" "$1,234.57"102+=TEXT(1234567.891, "#,##0,") "1,235" "1,235"103+=TEXT(1234.5, "0.00E+00") "1.23E+03" "1.23E+03"104+=TEXT(0.0000000001, "0.00E+00") "1.00E-10" "1.00E-10"105+=TEXT(-1234.5, "#,##0.00;(#,##0.00)") "(1,234.50)" "(1,234.50)"106+=TEXT(0, "0;-0;\z\e\r\o") "zero" "zero"107+=TEXT(3, "0.###") "3." "3."108+=TEXT(2.5, "0") "3" "3"109+=TEXT(-0.5, "0") "-1" "-1"110+=TEXT(1.005, "0.00") "1.01" "1.01"111+=TEXT(0.75, "# ?/?") "3/4" "3/4"112+=TEXT("abc", "0") "abc" "abc"113+=TEXT("12", "0.00") "12.00" "12.00"114+=TEXT(TRUE, "0") "TRUE" "TRUE"115+=TEXT(Data!F1, "dddd") "Saturday" "Saturday"116+=TEXT(Data!F1, "mmm d, yyyy") "Sep 26, 2026" "Sep 26, 2026"117+=TEXT(Data!F1, "dddd, mmmm d, yyyy") "Saturday, September 26, 2026" "Saturday, September 26, 2026"118+=TEXT(Data!F3, "h:mm AM/PM") "2:30 PM" "2:30 PM"119+=TEXT(0.75, "h AM/PM") "6 PM" "6 PM"120+=TEXT(Data!F1+Data!F3, "yyyy-mm-dd hh:mm") "2026-09-26 14:30" "2026-09-26 14:30"121+=TEXT(1.5, "[h]:mm") "36:00" "36:00"122+=TEXT(90/1440, "[mm]:ss") "90:00" "90:00"123123 ## Rounding and floating point124-=ROUND(1.005, 2) 1.01 unconfirmed125-=ROUND(2.675, 2) 2.68 unconfirmed126-=ROUND(0.285, 2) 0.29 unconfirmed127-=ROUND(-2.5) -3 unconfirmed128-=ROUND(1.5, 0.9) 2 unconfirmed129-=ROUND(1.23456789012345, 14) 1.23456789012345 unconfirmed130-=ROUNDUP(0.1+0.2, 1) 0.3 unconfirmed131-=ROUNDDOWN(1-0.9, 1) 0.1 unconfirmed132-=TRUNC(1234, -2) 1200 unconfirmed133-=INT(-1.5) -2 unconfirmed134-=INT(1-0.9+0.9) 1 unconfirmed135-=CEILING(0.3, 0.1) 0.3 unconfirmed136-=CEILING(-2.5, 1) -2 unconfirmed137-=CEILING(-2.5, -1) -3 unconfirmed138-=CEILING(2.5, -1) #NUM! unconfirmed139-=CEILING(2.5, 0) 0 unconfirmed140-=FLOOR(-2.5, 1) -3 unconfirmed141-=FLOOR(-2.5, -1) -2 unconfirmed142-=MOD(-7, 3) 2 unconfirmed143-=MOD(10, 0) #DIV/0! unconfirmed144-=MOD(0.3, 0.1) 0.1 unconfirmed145-=QUOTIENT(-7, 2) -3 unconfirmed146-=0.1+0.2 0.3 unconfirmed147-=0.1+0.2=0.3 FALSE unconfirmed148-=0.1+0.2-0.3 5.55111512312578E-17 unconfirmed149-=1-0.9 0.1 unconfirmed150-=(1-0.9)=0.1 FALSE unconfirmed151-=SUM(0.1, 0.2, 0.3)=0.6 FALSE unconfirmed152-=1/3 0.333333333333333 unconfirmed153-=100*1.1 110 unconfirmed154-=1E+21 1E+21 unconfirmed155-=123456789012345678 123456789012346000 unconfirmed156-=9007199254740993 9007199254740990 unconfirmed157-=0.000001 0.000001 unconfirmed158-=-0 0 unconfirmed159-=-2^2 4 unconfirmed160-=0^0 1 unconfirmed161-=POWER(0, 0) 1 unconfirmed162-=POWER(-8, 1/3) #NUM! unconfirmed163-=(-8)^(1/3) #NUM! unconfirmed164-=1E+308*10 #NUM! unconfirmed165-=LOG(10, 1) #DIV/0! unconfirmed124+=ROUND(1.005, 2) 1.01 1.01125+=ROUND(2.675, 2) 2.68 2.68126+=ROUND(0.285, 2) 0.29 0.29127+=ROUND(-2.5) -3 -3128+=ROUND(1.5, 0.9) 2 2129+=ROUND(1.23456789012345, 14) 1.23456789012345 1.23456789012346 on purpose: 012 rounds the shortest decimal half away from zero, so an exact input stays exact; Sheets' ...346 fits only a rule that also turns ROUND(1.5, 14) into 1.50000000000001130+=ROUNDUP(0.1+0.2, 1) 0.3 0.3131+=ROUNDDOWN(1-0.9, 1) 0.1 0.1132+=TRUNC(1234, -2) 1200 1200133+=INT(-1.5) -2 -2134+=INT(1-0.9+0.9) 1 1135+=CEILING(0.3, 0.1) 0.3 0.3136+=CEILING(-2.5, 1) -2 -2137+=CEILING(-2.5, -1) -3 -3138+=CEILING(2.5, -1) #NUM! #NUM!139+=CEILING(2.5, 0) 0 0140+=FLOOR(-2.5, 1) -3 -3141+=FLOOR(-2.5, -1) -2 -2142+=MOD(-7, 3) 2 2143+=MOD(10, 0) #DIV/0! #DIV/0!144+=MOD(0.3, 0.1) 0.1 -5.55111512312578E-17 on purpose: 012 computes n - d*INT(n/d) as Excel does, keeping the divisor's sign; Sheets' -5.55E-17 is a floating artefact145+=QUOTIENT(-7, 2) -3 -3146+=0.1+0.2 0.3 0.3147+=0.1+0.2=0.3 TRUE TRUE148+=0.1+0.2-0.3 5.55111512312578E-17 5.55111512312578E-17149+=1-0.9 0.1 0.1150+=(1-0.9)=0.1 TRUE TRUE151+=SUM(0.1, 0.2, 0.3)=0.6 TRUE TRUE152+=1/3 0.333333333333333 0.333333333333333153+=100*1.1 110 110154+=1E+21 1E+21 1E+21155+=123456789012345678 123456789012345000 123456789012345000156+=9007199254740993 9007199254740990 9007199254740990157+=0.000001 0.000001 0.000001158+=-0 0 0159+=-2^2 4 4160+=0^0 1 1161+=POWER(0, 0) 1 1162+=POWER(-8, 1/3) -2 -2163+=(-8)^(1/3) -2 -2164+=1E+308*10 #NUM! #NUM!165+=LOG(10, 1) #DIV/0! #DIV/0!166166 ## Errors and their propagation167-=0/0 #DIV/0! unconfirmed168-=Data!J2+1 #DIV/0! unconfirmed169-=SUM(Data!J2, 1) #DIV/0! unconfirmed170-=SUM(1, NA()) #N/A unconfirmed171-=NA()+1/0 #N/A unconfirmed172-=1/0+NA() #DIV/0! unconfirmed173-=IFERROR(1/0, "x") "x" unconfirmed174-=IFERROR(NA()) "" unconfirmed175-=IFERROR(Data!J2, Data!J4) #N/A unconfirmed176-=IFNA(1/0, 0) #DIV/0! unconfirmed177-=ISERR(NA()) FALSE unconfirmed178-=IF(1/0, 1, 2) #DIV/0! unconfirmed179-=IF("abc", 1, 2) #VALUE! unconfirmed180-=AND(TRUE, 1/0) #DIV/0! unconfirmed181-=OR(TRUE, NA()) #N/A unconfirmed182-=Data!A1+nosuchname #NAME? unconfirmed183-=CHOOSE(0, "a") #VALUE! unconfirmed184-=INDEX(Data!A1:A3, 5) #REF! unconfirmed185-=AVERAGEIF(Data!A1:A7, ">100") #DIV/0! unconfirmed186-=SWITCH(Data!B4, "north", 1) #N/A unconfirmed187-=PMT(0, 0, 1000) #NUM! unconfirmed188-=COUNT(Data!J1:J4) #DIV/0! unconfirmed189-=COUNTA(Data!J1:J4) #DIV/0! unconfirmed167+=0/0 #DIV/0! #DIV/0!168+=Data!J2+1 #DIV/0! #DIV/0!169+=SUM(Data!J2, 1) #DIV/0! #DIV/0!170+=SUM(1, NA()) #N/A #N/A171+=NA()+1/0 #N/A #N/A172+=1/0+NA() #DIV/0! #DIV/0!173+=IFERROR(1/0, "x") "x" "x"174+=IFERROR(NA()) (blank) (blank)175+=IFERROR(Data!J2, Data!J4) #N/A #N/A176+=IFNA(1/0, 0) #DIV/0! #DIV/0!177+=ISERR(NA()) FALSE FALSE178+=IF(1/0, 1, 2) #DIV/0! #DIV/0!179+=IF("abc", 1, 2) #VALUE! #VALUE!180+=AND(TRUE, 1/0) #DIV/0! #DIV/0!181+=OR(TRUE, NA()) #N/A #N/A182+=Data!A1+nosuchname #NAME? #NAME?183+=CHOOSE(0, "a") #NUM! #NUM!184+=INDEX(Data!A1:A3, 5) #NUM! #NUM!185+=AVERAGEIF(Data!A1:A7, ">100") #DIV/0! #DIV/0!186+=SWITCH(Data!B4, "north", 1) #N/A #N/A187+=PMT(0, 0, 1000) #NUM! #NUM!188+=COUNT(Data!J1:J4) 0 0189+=COUNTA(Data!J1:J4) 4 4190190 ## Coercion191-=" 3 "*1 3 unconfirmed192-="1,000"*1 1000 unconfirmed193-="$5"*2 10 unconfirmed194-="50%"*2 1 unconfirmed195-="9/26/2026"*1 46291 unconfirmed196-="TRUE"*1 #VALUE! unconfirmed197-=-TRUE -1 unconfirmed198-=Data!A5&"x" "x" unconfirmed199-=Data!A5="" TRUE unconfirmed200-=Data!B6=0 FALSE unconfirmed201-=ISBLANK(Data!B6) FALSE unconfirmed202-=Data!J1+1 4 unconfirmed203-=SUM(Data!J1) 0 unconfirmed204-=SUM("3", 1) 4 unconfirmed205-=SUM(Data!E6, 1) 1 unconfirmed206-=AVERAGE(Data!E6, 3) 3 unconfirmed207-=COUNT("1", 2) 2 unconfirmed208-=COUNT(TRUE) 1 unconfirmed209-=COUNTBLANK(Data!B1:B7) 1 unconfirmed210-="10"<9 FALSE unconfirmed211-=1<"a" TRUE unconfirmed212-=TRUE>1 TRUE unconfirmed213-=TRUE="TRUE" FALSE unconfirmed214-=Data!A5<"a" TRUE unconfirmed215-=""=0 FALSE unconfirmed216-=IF("TRUE", 1, 2) #VALUE! unconfirmed217-=IF(Data!A5, "y", "n") "n" unconfirmed218-=AND("abc") #VALUE! unconfirmed219-=AND(Data!A4) #VALUE! unconfirmed220-=NOT("") #VALUE! unconfirmed221-=ROUND("2.5") 3 unconfirmed222-=LEFT(Data!F1, 3) "462" unconfirmed191+=" 3 "*1 3 3192+="1,000"*1 1000 1000193+="$5"*2 10 10194+="50%"*2 1 1195+="9/26/2026"*1 46291 46291196+="TRUE"*1 #VALUE! #VALUE!197+=-TRUE -1 -1198+=Data!A5&"x" "x" "x"199+=Data!A5="" TRUE TRUE200+=Data!B6=0 FALSE FALSE201+=ISBLANK(Data!B6) FALSE FALSE202+=Data!J1+1 4 4203+=SUM(Data!J1) 0 0204+=SUM("3", 1) 4 4205+=SUM(Data!E6, 1) 1 1206+=AVERAGE(Data!E6, 3) 3 3207+=COUNT("1", 2) 2 2208+=COUNT(TRUE) 1 1209+=COUNTBLANK(Data!B1:B7) 1 1210+="10"<9 FALSE FALSE211+=1<"a" TRUE TRUE212+=TRUE>1 TRUE TRUE213+=TRUE="TRUE" FALSE FALSE214+=Data!A5<"a" TRUE TRUE215+=""=0 FALSE FALSE216+=IF("TRUE", 1, 2) 1 1217+=IF(Data!A5, "y", "n") "n" "n"218+=AND("abc") #VALUE! #VALUE!219+=AND(Data!A4) #VALUE! #VALUE!220+=NOT("") TRUE TRUE221+=ROUND("2.5") 3 3222+=LEFT(Data!F1, 3) "462" "9/2" on purpose: F1 is a date, stored in the XLSX as its serial with a m/d/yyyy format; Sheets' text functions read a date cell as it shows (9/26/2026), 012 reads its serial, as Excel does223223 ## Lookups and criteria224-=VLOOKUP("B*", Data!D1:E3, 2, FALSE) 2.5 unconfirmed225-=VLOOKUP("Blueberry", Data!D1:E3, 2) 2.5 unconfirmed226-=VLOOKUP("Aardvark", Data!D1:E3, 2, TRUE) #N/A unconfirmed227-=VLOOKUP(2.5, Data!C1:D5, 2) "Banana" unconfirmed228-=VLOOKUP("1", Data!C1:D5, 2, FALSE) #N/A unconfirmed229-=VLOOKUP("Apple", Data!D1:E3, 3, FALSE) #REF! unconfirmed230-=VLOOKUP("Apple", Data!D1:E3, 0, FALSE) #VALUE! unconfirmed231-=HLOOKUP("Apple", Data!D1:E2, 2, FALSE) "Banana" unconfirmed232-=MATCH("cherry", Data!D1:D3, 0) 3 unconfirmed233-=MATCH("B*", Data!D1:D3, 0) 2 unconfirmed234-=MATCH(3.2, Data!C1:C7) 3 unconfirmed235-=MATCH(0, Data!C1:C7) #N/A unconfirmed236-=MATCH(35, Data!I1:I3, -1) #N/A unconfirmed237-=MATCH(3, {1, 3, 5}, 0) 2 unconfirmed238-=INDEX(Data!C1:E1, 2) "Apple" unconfirmed239-=INDEX(Data!D1:E3, 4, 1) #REF! unconfirmed240-=ROWS(INDEX(Data!A1:C3, 0, 1)) 1 unconfirmed241-=SUM(INDEX(Data!C1:E3, 0, 1)) 1 unconfirmed242-=XLOOKUP("kiwi", Data!D1:D3, Data!E1:E3) #N/A unconfirmed243-=XLOOKUP("Apple", Data!D1:D5, Data!E1:E5, , 0, -1) 9 unconfirmed244-=XLOOKUP(3.2, Data!C1:C7, Data!D1:D7, , -1) "Cherry" unconfirmed245-=XLOOKUP(3.2, Data!C1:C7, Data!D1:D7, , 1) "date" unconfirmed246-=XLOOKUP("Ch*", Data!D1:D3, Data!E1:E3, , 2) 3.5 unconfirmed247-=XLOOKUP(2, Data!G1:G4, Data!H1:H4, , 0, -1) 400 unconfirmed248-=CHOOSE(2.9, "a", "b", "c") "b" unconfirmed249-=SUMIF(Data!A1:A7, "<>10") 45 unconfirmed250-=SUMIF(Data!C1:C7, ">=" & Data!C5) 18 unconfirmed251-=SUMIFS(Data!C1:C5, Data!B1:B5, "north", Data!A1:A5, ">10") 3 unconfirmed252-=COUNTIF(Data!B1:B7, "?o*") 4 unconfirmed253-=COUNTIF(Data!B1:B7, "Ab~*d") 1 unconfirmed254-=COUNTIF(Data!B1:B7, "<>north") 4 unconfirmed255-=COUNTIF(Data!A1:A7, "") 2 unconfirmed256-=COUNTIF(Data!A1:A7, "10") 1 unconfirmed257-=COUNTIF(Data!J1:J4, 3) 0 unconfirmed258-=COUNTIF(Data!E1:E7, TRUE) 1 unconfirmed259-=COUNTIF(Data!F1:F5, ">" & DATE(2026, 1, 1)) 2 unconfirmed224+=VLOOKUP("B*", Data!D1:E3, 2, FALSE) 2.5 2.5225+=VLOOKUP("Blueberry", Data!D1:E3, 2) 2.5 2.5226+=VLOOKUP("Aardvark", Data!D1:E3, 2, TRUE) #N/A #N/A227+=VLOOKUP(2.5, Data!C1:D5, 2) "Banana" "Banana"228+=VLOOKUP("1", Data!C1:D5, 2, FALSE) #N/A #N/A229+=VLOOKUP("Apple", Data!D1:E3, 3, FALSE) #REF! #REF!230+=VLOOKUP("Apple", Data!D1:E3, 0, FALSE) #VALUE! #VALUE!231+=HLOOKUP("Apple", Data!D1:E2, 2, FALSE) "Banana" "Banana"232+=MATCH("cherry", Data!D1:D3, 0) 3 3233+=MATCH("B*", Data!D1:D3, 0) 2 2234+=MATCH(3.2, Data!C1:C7) 3 3235+=MATCH(0, Data!C1:C7) #N/A #N/A236+=MATCH(35, Data!I1:I3, -1) #N/A #N/A237+=MATCH(3, {1, 3, 5}, 0) 2 2238+=INDEX(Data!C1:E1, 2) "Apple" "Apple"239+=INDEX(Data!D1:E3, 4, 1) #NUM! #NUM!240+=ROWS(INDEX(Data!A1:C3, 0, 1)) 3 3241+=SUM(INDEX(Data!C1:E3, 0, 1)) 6 6242+=XLOOKUP("kiwi", Data!D1:D3, Data!E1:E3) #N/A #N/A243+=XLOOKUP("Apple", Data!D1:D5, Data!E1:E5, , 0, -1) 9 9244+=XLOOKUP(3.2, Data!C1:C7, Data!D1:D7, , -1) "Cherry" "Cherry"245+=XLOOKUP(3.2, Data!C1:C7, Data!D1:D7, , 1) "date" "date"246+=XLOOKUP("Ch*", Data!D1:D3, Data!E1:E3, , 2) 3.5 3.5247+=XLOOKUP(2, Data!G1:G4, Data!H1:H4, , 0, -1) 400 400248+=CHOOSE(2.9, "a", "b", "c") "b" "b"249+=SUMIF(Data!A1:A7, "<>10") 45 45250+=SUMIF(Data!C1:C7, ">=" & Data!C5) 18 18251+=SUMIFS(Data!C1:C5, Data!B1:B5, "north", Data!A1:A5, ">10") 3 3252+=COUNTIF(Data!B1:B7, "?o*") 4 4253+=COUNTIF(Data!B1:B7, "Ab~*d") 1 1254+=COUNTIF(Data!B1:B7, "<>north") 4 4255+=COUNTIF(Data!A1:A7, "") 2 2256+=COUNTIF(Data!A1:A7, "10") 1 1257+=COUNTIF(Data!J1:J4, 3) 1 1258+=COUNTIF(Data!E1:E7, TRUE) 1 1259+=COUNTIF(Data!F1:F5, ">" & DATE(2026, 1, 1)) 2 2260260 ## Arrays and spills261-=SUM(SEQUENCE(3, 3, 0, 2)) 72 unconfirmed262-=INDEX(SEQUENCE(3, 2, 10, 5), 3, 2) 35 unconfirmed263-=ROWS(SEQUENCE(0)) 1 unconfirmed264-=ROWS(FILTER(Data!A1:A7, Data!C1:C7>3)) 4 unconfirmed265-=TEXTJOIN(",", TRUE, FILTER(Data!D1:D5, Data!B1:B5="north")) "Apple,Cherry,Apple" unconfirmed266-=FILTER(Data!A1:A3, Data!A1:A3>100) #N/A unconfirmed267-=TEXTJOIN(",", TRUE, SORT(Data!C1:C5, 1, FALSE)) "5,4,3,2,1" unconfirmed268-=INDEX(SORT(Data!D1:E5, 2, FALSE), 1, 1) "date" unconfirmed269-=TEXTJOIN(",", TRUE, SORT(Data!D1:D5)) "Apple,Apple,Banana,Cherry,date" unconfirmed270-=TEXTJOIN(",", TRUE, UNIQUE(Data!B1:B5)) "north,south,east" unconfirmed271-=TEXTJOIN(",", TRUE, UNIQUE(Data!G1:G4, FALSE, TRUE)) "1,3" unconfirmed261+=SUM(SEQUENCE(3, 3, 0, 2)) 72 72262+=INDEX(SEQUENCE(3, 2, 10, 5), 3, 2) 35 35263+=ROWS(SEQUENCE(0)) #NUM! #NUM!264+=ROWS(FILTER(Data!A1:A7, Data!C1:C7>3)) 4 unconfirmed Sheets read _xlfn._xlws.FILTER and _xlfn._xlws.SORT, which is how Excel writes them, as #NAME?: confirm on the next import265+=TEXTJOIN(",", TRUE, FILTER(Data!D1:D5, Data!B1:B5="north")) "Apple,Cherry,Apple" unconfirmed Sheets read _xlfn._xlws.FILTER and _xlfn._xlws.SORT, which is how Excel writes them, as #NAME?: confirm on the next import266+=FILTER(Data!A1:A3, Data!A1:A3>100) #N/A unconfirmed Sheets read _xlfn._xlws.FILTER and _xlfn._xlws.SORT, which is how Excel writes them, as #NAME?: confirm on the next import267+=TEXTJOIN(",", TRUE, SORT(Data!C1:C5, 1, FALSE)) "5,4,3,2,1" unconfirmed Sheets read _xlfn._xlws.FILTER and _xlfn._xlws.SORT, which is how Excel writes them, as #NAME?: confirm on the next import268+=INDEX(SORT(Data!D1:E5, 2, FALSE), 1, 1) "date" unconfirmed Sheets read _xlfn._xlws.FILTER and _xlfn._xlws.SORT, which is how Excel writes them, as #NAME?: confirm on the next import269+=TEXTJOIN(",", TRUE, SORT(Data!D1:D5)) "Apple,Apple,Banana,Cherry,date" unconfirmed Sheets read _xlfn._xlws.FILTER and _xlfn._xlws.SORT, which is how Excel writes them, as #NAME?: confirm on the next import270+=TEXTJOIN(",", TRUE, UNIQUE(Data!B1:B5)) "north,south,east" "north,south,east"271+=TEXTJOIN(",", TRUE, UNIQUE(Data!G1:G4, FALSE, TRUE)) "1,3" "1,3"272272 =TEXTJOIN(",", TRUE, FLATTEN(Data!C1:D2)) "1,Apple,2,Banana" unconfirmed273273 =ROWS(FLATTEN(Data!C1:D2, Data!G1:G2)) 6 unconfirmed274274 =TEXTJOIN(",", TRUE, SORTN(Data!C1:C7, 3, 0, 1, FALSE)) "7,6,5" unconfirmed275275 =ROWS(SORTN(Data!G1:G4, 2, 1, 1, TRUE)) 3 unconfirmed276-=TEXTJOIN(",", TRUE, CHOOSECOLS(Data!C1:E1, -1, 1)) "1.5,1" unconfirmed277-=ROWS(CHOOSEROWS(Data!A1:A7, 1, 3)) 2 unconfirmed276+=TEXTJOIN(",", TRUE, CHOOSECOLS(Data!C1:E1, -1, 1)) "1.5,1" "1.5,1"277+=ROWS(CHOOSEROWS(Data!A1:A7, 1, 3)) 2 2278278 =SUM(ARRAYFORMULA(Data!C1:C3*Data!G1:G3)) 11 unconfirmed279-=ARRAYFORMULA(SUM(Data!C1:C3^2)) 14 unconfirmed280-=SUM(Data!C1:C3*2) 12 unconfirmed281-=SUMPRODUCT(Data!C1:C3>1) 0 unconfirmed282-=SUMPRODUCT(Data!A1:A4, Data!C1:C4) 140 unconfirmed283-=SUM({1, 2; 3, 4}) 10 unconfirmed284-=INDEX({1, 2; 3, 4}, 2, 1) 3 unconfirmed279+=ARRAYFORMULA(SUM(Data!C1:C3^2)) 14 14280+=SUM(Data!C1:C3*2) 12 #VALUE! on purpose: 012 computes the arguments of functions that take ranges as arrays, as Excel 365 does; Sheets needs ARRAYFORMULA281+=SUMPRODUCT(Data!C1:C3>1) 2 2282+=SUMPRODUCT(Data!A1:A4, Data!C1:C4) 140 140283+=SUM({1, 2; 3, 4}) 10 10284+=INDEX({1, 2; 3, 4}, 2, 1) 3 3285285 ## LET and LAMBDA286-=LET(x, 2, y, x+1, x*y) 6 unconfirmed287-=LET(x, SEQUENCE(3), SUM(x)) 6 unconfirmed288-=LET(x, 1/0, 5) #DIV/0! unconfirmed289-=LAMBDA(x, x+1)() #N/A unconfirmed290-=LET(f, LAMBDA(x, x+1), f(f(1))) 3 unconfirmed291-=SUM(MAP({1, 2, 3}, LAMBDA(x, x*10))) 60 unconfirmed292-=SUM(MAP(Data!A1:A3, Data!C1:C3, LAMBDA(a, b, a+b))) 66 unconfirmed293-=REDUCE(0, Data!C1:C5, LAMBDA(a, b, a+b)) 15 unconfirmed294-=TEXTJOIN(",", TRUE, SCAN(0, {1, 2, 3}, LAMBDA(a, b, a+b))) "1,3,6" unconfirmed295-=SUM(BYROW(Data!C1:E3, LAMBDA(r, COUNT(r)))) 6 unconfirmed296-=TEXTJOIN(",", TRUE, BYCOL(Data!C1:E3, LAMBDA(c, COUNTA(c)))) "3,3,3" unconfirmed297-=SUM(MAKEARRAY(3, 3, LAMBDA(r, c, r*c))) 36 unconfirmed286+=LET(x, 2, y, x+1, x*y) 6 6287+=LET(x, SEQUENCE(3), SUM(x)) 6 6288+=LET(x, 1/0, 5) 5 5289+=LAMBDA(x, x+1)() #N/A #N/A290+=LET(f, LAMBDA(x, x+1), f(f(1))) 3 3291+=SUM(MAP({1, 2, 3}, LAMBDA(x, x*10))) 60 60292+=SUM(MAP(Data!A1:A3, Data!C1:C3, LAMBDA(a, b, a+b))) 66 66293+=REDUCE(0, Data!C1:C5, LAMBDA(a, b, a+b)) 15 15294+=TEXTJOIN(",", TRUE, SCAN(0, {1, 2, 3}, LAMBDA(a, b, a+b))) "1,3,6" "1,3,6"295+=SUM(BYROW(Data!C1:E3, LAMBDA(r, COUNT(r)))) 6 6296+=TEXTJOIN(",", TRUE, BYCOL(Data!C1:E3, LAMBDA(c, COUNTA(c)))) "3,3,3" "3,3,3"297+=SUM(MAKEARRAY(3, 3, LAMBDA(r, c, r*c))) 36 36298298 ## Logic299-=IFS(Data!A1>20, "big", Data!A1>5, "medium") "medium" unconfirmed300-=SWITCH(Data!B2, "north", 1, "south", 2) 2 unconfirmed301-=SWITCH(Data!B4, "north", 1, "south", 2, 0) 0 unconfirmed302-=XOR(Data!A1:A3) TRUE unconfirmed299+=IFS(Data!A1>20, "big", Data!A1>5, "medium") "medium" "medium"300+=SWITCH(Data!B2, "north", 1, "south", 2) 2 2301+=SWITCH(Data!B4, "north", 1, "south", 2, 0) 0 0302+=XOR(Data!A1:A3) TRUE TRUE303303 ## Math, statistics and finance304-=PRODUCT(2, "3") 6 unconfirmed305-=MEDIAN(1, 2, 3, 4) 2.5 unconfirmed306-=MODE(Data!G1:G4) 2 unconfirmed307-=MODE(Data!C1:C5) #N/A unconfirmed308-=STDEV(Data!A1:A7) 14.9303940559741 unconfirmed309-=VAR(Data!A1:A7) 222.916666666667 unconfirmed310-=RANK(2, Data!G1:G4) 2 unconfirmed311-=RANK(99, Data!A1:A7) #N/A unconfirmed312-=ROUND(PMT(0.05/12, 360, 200000), 6) -1073.643246 unconfirmed313-=PMT(0, 10, 1000) -100 unconfirmed314-=ROUND(PV(0.05/12, 360, -1000), 6) 186281.617046 unconfirmed315-=ROUND(NPER(0.01, -1000, 10000), 9) 10.588644459 unconfirmed316-=NPER(0.01, 100, 10000) -69.6607168935748 unconfirmed317-=ROUND(RATE(48, -200, 8000), 9) 0.007701472 unconfirmed318-=ROUND(NPV(0.1, Data!H1:H4), 6) -19.124377 unconfirmed319-=ROUND(IRR(Data!H1:H4), 9) 0.088963395 unconfirmed320-=IRR(Data!C1:C5) #NUM! unconfirmed304+=PRODUCT(2, "3") 6 6305+=MEDIAN(1, 2, 3, 4) 2.5 2.5306+=MODE(Data!G1:G4) 2 2307+=MODE(Data!C1:C5) #N/A #N/A308+=STDEV(Data!A1:A7) 14.9303940559741 14.9303940559741309+=VAR(Data!A1:A7) 222.916666666667 222.916666666667310+=RANK(2, Data!G1:G4) 2 2311+=RANK(99, Data!A1:A7) #N/A #N/A312+=ROUND(PMT(0.05/12, 360, 200000), 6) -1073.643246 -1073.643246313+=PMT(0, 10, 1000) -100 -100314+=ROUND(PV(0.05/12, 360, -1000), 6) 186281.617046 186281.617046315+=ROUND(NPER(0.01, -1000, 10000), 9) 10.588644459 10.588644459316+=NPER(0.01, 100, 10000) -69.6607168935748 -69.6607168935748317+=ROUND(RATE(48, -200, 8000), 9) 0.007701472 0.007701472318+=ROUND(NPV(0.1, Data!H1:H4), 6) -19.124377 -19.124377319+=ROUND(IRR(Data!H1:H4), 9) 0.088963395 0.088963395320+=IRR(Data!C1:C5) #NUM! #NUM!@@ -4,6 +4,8 @@ import (
44 "fmt"55 "strconv"66 "strings"7+8+ "github.com/FelineStateMachine/012/internal/numfmt"79 )810 911 // Binding powers, lowest to highest, following Sheets' precedence.@@ -133,7 +135,7 @@ func (p *parser) prefix() (Node, error) {
133135 if r, ok := p.lines(t, ""); ok {134136 return r, nil135137 }136- v, err := strconv.ParseFloat(t.text, 64)138+ v, err := strconv.ParseFloat(numfmt.Entered(t.text), 64)137139 if err != nil {138140 return nil, &ParseError{Pos: t.pos, Msg: "Invalid number " + t.text}139141 }@@ -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 {@@ -140,9 +140,9 @@ func init() {
140140 &FuncDef{Name: "ABS", Args: "value", Desc: "Absolute value", Min: 1, Max: 1, eval: math1(math.Abs), format: inherit},141141 &FuncDef{Name: "INT", Args: "value", Desc: "Round down to the nearest integer", Min: 1, Max: 1, eval: math1(math.Floor), format: inherit},142142 &FuncDef{Name: "SQRT", Args: "value", Desc: "Square root", Min: 1, Max: 1, eval: math1(math.Sqrt)},143- &FuncDef{Name: "ROUND", Args: "value, [places]", Desc: "Round to a number of decimal places, halves away from zero", Min: 1, Max: 2,143+ &FuncDef{Name: "ROUND", Args: "value, [places]", Desc: "Round to a number of decimal places, halves away from zero, on the digits a cell shows: ROUND(1.5, 14) is 1.5", 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 {@@ -0,0 +1,35 @@
1+package numfmt2+3+// Entered keeps the first 15 significant digits of a number written in4+// decimal (digits, a point, an exponent) and makes the digits after them5+// zeros, as Sheets and Excel do with a number typed into a formula:6+// =123456789012345678 is 123456789012345000. Text with no more than 157+// digits comes back as it is. Numbers typed into cells keep every digit,8+// as files and imports write them with 17 to round-trip.9+func Entered(s string) string {10+ sig := 011+ for i := 0; i < len(s); i++ {12+ switch c := s[i]; {13+ case c == 'e' || c == 'E':14+ return s15+ case c < '0' || c > '9':16+ case sig > 0 || c != '0':17+ sig++18+ if sig > 15 {19+ return zeroDigits(s, i)20+ }21+ }22+ }23+ return s24+}25+26+// zeroDigits makes the digits of s from i up to any exponent zeros.27+func zeroDigits(s string, i int) string {28+ b := []byte(s)29+ for ; i < len(b) && b[i] != 'e' && b[i] != 'E'; i++ {30+ if b[i] >= '1' && b[i] <= '9' {31+ b[i] = '0'32+ }33+ }34+ return string(b)35+}@@ -0,0 +1,166 @@
1+package numfmt2+3+import (4+ "math"5+ "strconv"6+ "strings"7+8+ "github.com/FelineStateMachine/012/internal/locale"9+)10+11+// Fraction patterns: "# ?/?" shows 1.75 as 1 3/4, "# ??/??" allows12+// denominators up to 99, "# ?/8" shows eighths, and "?/?" with no whole13+// part shows 7/4. Placeholders pad as in numbers: ? with a space, 0 with14+// a zero, # with nothing.15+16+// fracLayout is where a fraction section puts each part: the tokens of17+// the whole number (with anything before it), those between it and the18+// numerator, the numerator's and denominator's placeholders, and the19+// rest.20+type fracLayout struct {21+ whole, gap, num, den, rest []ptok22+ fixed int // the denominator written in the pattern, or 023+}24+25+// layoutFraction finds a fraction in a number section: digit26+// placeholders, a /, and placeholders or digits for the denominator.27+func layoutFraction(toks []ptok) (fracLayout, bool) {28+ slash := -129+ for i := 1; i+1 < len(toks); i++ {30+ if toks[i].kind == ptLit && toks[i].s == "/" && toks[i-1].kind == ptDigit && denToken(toks[i+1]) {31+ slash = i32+ break33+ }34+ }35+ if slash < 0 {36+ return fracLayout{}, false37+ }38+ start := slash39+ for start > 0 && toks[start-1].kind == ptDigit {40+ start--41+ }42+ end := slash + 143+ for end < len(toks) && denToken(toks[end]) {44+ end++45+ }46+ l := fracLayout{num: toks[start:slash], den: toks[slash+1 : end], rest: toks[end:]}47+ last := start - 148+ for last >= 0 && toks[last].kind != ptDigit {49+ last--50+ }51+ l.whole, l.gap = toks[:last+1], toks[last+1:start]52+ var d strings.Builder53+ for _, t := range l.den {54+ d.WriteString(t.s)55+ }56+ if n, err := strconv.Atoi(d.String()); err == nil && n > 0 {57+ l.fixed = n58+ }59+ return l, true60+}61+62+// denToken reports whether t can be part of a denominator: a digit63+// placeholder or a digit written out.64+func denToken(t ptok) bool {65+ return t.kind == ptDigit || t.kind == ptLit && len(t.s) == 1 && t.s[0] >= '0' && t.s[0] <= '9'66+}67+68+// hasWhole reports whether the section shows a whole number before the69+// fraction.70+func (l fracLayout) hasWhole() bool { return hasKind(l.whole, ptDigit) }71+72+// formatFraction renders |v| with a fraction section as shown in loc;73+// neg adds a minus sign.74+func formatFraction(v float64, neg bool, l fracLayout, loc *locale.Locale) string {75+ whole := math.Floor(v)76+ n, d := l.approximate(v - whole)77+ if n == d {78+ whole, n = whole+1, 079+ }80+ if !l.hasWhole() {81+ n += int(whole) * d82+ whole = 083+ }84+ var b strings.Builder85+ if neg && (whole != 0 || n != 0) {86+ b.WriteByte('-')87+ }88+ w := ""89+ if l.hasWhole() {90+ w = formatNumber(whole, false, l.whole, loc)91+ } else {92+ w = literals(l.whole)93+ }94+ switch {95+ case n == 0 && l.hasWhole():96+ if strings.TrimSpace(w) == "" {97+ w = "0"98+ }99+ b.WriteString(w)100+ case n == 0 && !l.hasWhole() && l.fixed == 0:101+ b.WriteString(w + "0/1")102+ default:103+ b.WriteString(w)104+ if strings.TrimSpace(w) != "" {105+ b.WriteString(literals(l.gap))106+ }107+ b.WriteString(padDigits(strconv.Itoa(n), l.num, true))108+ b.WriteByte('/')109+ if l.fixed > 0 {110+ b.WriteString(strconv.Itoa(l.fixed))111+ } else {112+ b.WriteString(padDigits(strconv.Itoa(d), l.den, false))113+ }114+ }115+ b.WriteString(literals(l.rest))116+ return b.String()117+}118+119+// approximate is the fraction n/d nearest f (0 <= f < 1): with the120+// pattern's denominator, or the nearest with a denominator of as many121+// digits as its placeholders, the smallest such denominator on a tie.122+func (l fracLayout) approximate(f float64) (n, d int) {123+ if l.fixed > 0 {124+ return int(math.Round(f * float64(l.fixed))), l.fixed125+ }126+ maxD := int(math.Pow10(min(len(l.den), 5))) - 1127+ n, d = int(math.Round(f)), 1128+ best := math.Abs(f - float64(n))129+ for q := 2; q <= maxD && best > 0; q++ {130+ p := math.Round(f * float64(q))131+ if e := math.Abs(f - p/float64(q)); e < best {132+ n, d, best = int(p), q, e133+ }134+ }135+ return n, d136+}137+138+// padDigits fills the placeholders phs with digits: those it has room139+// for are padded, a ? with a space and a 0 with a zero, on the left of a140+// numerator (right) or the right of a denominator (left).141+func padDigits(digits string, phs []ptok, right bool) string {142+ var pad strings.Builder143+ for i := len(digits); i < len(phs); i++ {144+ switch phs[i].s {145+ case "?":146+ pad.WriteByte(' ')147+ case "0":148+ pad.WriteByte('0')149+ }150+ }151+ if right {152+ return pad.String() + digits153+ }154+ return digits + pad.String()155+}156+157+// literals is the text of the literal tokens among toks.158+func literals(toks []ptok) string {159+ var b strings.Builder160+ for _, t := range toks {161+ if t.kind == ptLit {162+ b.WriteString(t.s)163+ }164+ }165+ return b.String()166+}@@ -41,6 +41,24 @@ func TestFormat(t *testing.T) {
4141 {12.5, "General", "12.5"},4242 {5551234, "000-0000", "555-1234"},4343 {1.5, "General.00", "1.5.00"}, // placeholders after General get zeros44+ // Fractions, as Sheets shows them.45+ {0.75, "# ?/?", "3/4"},46+ {1.75, "# ?/?", "1 3/4"},47+ {-1.75, "# ?/?", "-1 3/4"},48+ {0.75, "0 ?/?", "0 3/4"},49+ {2, "# ?/?", "2"},50+ {0, "# ?/?", "0"},51+ {1.99, "# ?/?", "2"},52+ {0.3333, "# ?/?", "1/3"},53+ {3.14159, "# ??/??", "3 14/99"},54+ {1.5, "# ??/??", "1 1/2 "},55+ {0.1234, "# ??/??", "10/81"},56+ {1.3, "# ?/4", "1 1/4"},57+ {0.5, "# ?/8", "4/8"},58+ {0.37, "# ??/100", "37/100"},59+ {1.75, "?/?", "7/4"},60+ {1.5, `# ?/? "cups"`, "1 1/2 cups"},61+ {1234.5, "#,##0 ?/?", "1,234 1/2"},4462 // Dates and times: 46291 is 2026-09-26, a Saturday.4563 {46291, "m/d/yyyy", "9/26/2026"},4664 {46291, "yyyy-mm-dd", "2026-09-26"},@@ -122,3 +140,43 @@ func TestFormatLCID(t *testing.T) {
122140 }123141 }124142 }143+144+func TestRound(t *testing.T) {145+ tests := []struct {146+ x float64147+ places int148+ mode Rounding149+ want float64150+ }{151+ {1.005, 2, HalfUp, 1.01},152+ {2.675, 2, HalfUp, 2.68},153+ {-2.5, 0, HalfUp, -3},154+ {1234.5, -2, HalfUp, 1200},155+ {-1.005, 2, HalfUp, -1.01},156+ {1.5, 14, HalfUp, 1.5}, // exact inputs stay exact157+ {0.1 + 0.2, 15, HalfUp, 0.3},158+ {1.23456789012345, 14, HalfUp, 1.23456789012345}, // Sheets says ...346159+ {1.23456789012345, 15, HalfUp, 1.23456789012345},160+ {2.301, 1, Up, 2.4},161+ {-2.39, 1, Down, -2.3},162+ }163+ for _, tt := range tests {164+ if got := Round(tt.x, tt.places, tt.mode); got != tt.want {165+ t.Errorf("Round(%v, %d, %v) = %v, want %v", tt.x, tt.places, tt.mode, got, tt.want)166+ }167+ }168+}169+170+func TestEntered(t *testing.T) {171+ for in, want := range map[string]string{172+ "123456789012345678": "123456789012345000",173+ "123456789012345": "123456789012345",174+ "0.00012345678901234567": "0.00012345678901234500",175+ "1.2345678901234567e10": "1.2345678901234500e10",176+ "12.5": "12.5",177+ } {178+ if got := Entered(in); got != want {179+ t.Errorf("Entered(%s) = %s, want %s", in, got, want)180+ }181+ }182+}@@ -65,6 +65,9 @@ func FormatIn(v float64, pat string, loc *locale.Locale) string {
6565 if isDatePattern(toks) {6666 return formatDate(v, toks, patternNames(sec, loc))6767 }68+ if l, ok := layoutFraction(toks); ok {69+ return formatFraction(math.Abs(v), neg, l, loc)70+ }6871 return formatNumber(math.Abs(v), neg, toks, loc)6972 }7073 @@ -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,71 @@
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 is half away from zero on the shortest decimal, as in34+ // Sheets and Excel; exact inputs stay exact.35+ {"=ROUND(2.675, 2)=2.68", boolean(true)},36+ {"=ROUND(-1.005, 2)=-1.01", boolean(true)},37+ {"=ROUND(1.5, 14)-1.5=0", boolean(true)},38+ {"=ROUND(0.1+0.2, 15)-0.3=0", boolean(true)},39+ {"=ROUND(-2.5)", num(-3)},40+ // Errors and conditions.41+ {"=IFERROR(NA())", Value{}},42+ {`=ISBLANK(IFERROR(NA()))`, boolean(true)},43+ {`=IF("TRUE", 1, 2)`, num(1)},44+ {`=IF("false", 1, 2)`, num(2)},45+ {`=IF("", 1, 2)`, num(2)},46+ {`=IF("abc", 1, 2)`, ErrValue},47+ {`=NOT("")`, boolean(true)},48+ {`=NOT("TRUE")`, boolean(false)},49+ {`=AND("TRUE", 1)`, boolean(true)},50+ {"=COUNT(J1:J4)", num(0)},51+ {"=COUNTA(J1:J4)", num(4)},52+ {"=COUNT(1/0, 2)", num(1)},53+ {"=SUM(J1:J4)", ErrDiv0},54+ {"=COUNTIF(J1:J4, 3)", num(1)},55+ {`=COUNTIF(J1:J4, "<>3")`, num(3)},56+ {`=COUNTIF(J1:J4, ">2")`, num(0)},57+ // Positions out of range, and whole rows and columns.58+ {`=CHOOSE(0, "a")`, ErrNum},59+ {"=INDEX(A1:A3, 5)", ErrNum},60+ {"=ROWS(INDEX(A1:C3, 0, 1))", num(3)},61+ {"=COLUMNS(INDEX(A1:C3, 2, 0))", num(3)},62+ {"=SUM(INDEX(C1:E3, 0, 1))", num(6)},63+ {"=ROWS(SEQUENCE(0))", ErrNum},64+ {"=ROWS(1/0)", ErrDiv0},65+ // Arrays and names.66+ {"=SUMPRODUCT(C1:C3>1)", num(2)},67+ {"=SUMPRODUCT(C1:C3>1, C1:C3)", num(5)},68+ {"=LET(x, 1/0, 5)", num(5)},69+ {"=LET(x, 1/0, x+1)", ErrDiv0},70+ })71+}@@ -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",