- commit
- e2b994b118564219654779a4fcd67204ce457d74
- parent
- c4827b3b4313d5581711e64890c735d3f1d6d8d3
- parent
- e95283ba98c1144d525666304f7291f249e11bd9
- author
- Dami <dami.etoile@gmail.com>
| M | docs/ | 39 | ++++ |
| A | internal/ | 241 | +++++++++++++++++++++++ |
| A | internal/ | 96 | +++++++++ |
| A | internal/ | 320 | ++++++++++++++++++++++++++++++ |
@@ -69,6 +69,45 @@ formatted values with 012's. Known differences between Sheets and Excel,
6969 and places where excelize departs from Excel, are listed as skips with a7070 reason.7171 72+## The Sheets corpus73+74+012 follows Google Sheets' formula results where people rely on them75+([UX](ux.md)), and `internal/fileio/testdata/sheets_corpus.tsv` holds76+about 300 formulas that pin it down: dates and times, text, rounding and77+floating point, errors and how they propagate, coercion of text,78+booleans and blanks, lookups and criteria, array sizes, LET and LAMBDA,79+and `TEXT` formats. Each line is a formula, 012's result, Sheets'80+result and a note, separated by tabs. Results read `"text"` quoted,81+`12.5`, `TRUE`, `#N/A`, or `(blank)`. Formulas read a small fixture on a82+sheet named `Data`, and each computes one value: an array is measured83+with `ROWS`, `COLUMNS`, `INDEX` or `TEXTJOIN`, so its size is checked84+and nothing spills.85+86+`TestSheetsCorpus` holds 012 to the results recorded for it, and fails87+on a row where Sheets' result differs unless its note starts88+`on purpose:` and says why; that reason also goes in the docs page for89+the feature. Sheets' result reads `unconfirmed` until it is checked in90+Sheets:91+92+```sh93+# Add formulas as lines of their own, then record 012's results.94+go test ./internal/fileio -run SheetsCorpus -update-corpus95+# Write the corpus as a workbook with 012's XLSX exporter.96+go test ./internal/fileio -run SheetsCorpus -corpus-xlsx=$HOME/012-sheets-corpus.xlsx97+# In Sheets (locale United States): File > Import the workbook, then98+# File > Download > CSV of the Corpus sheet. Record Sheets' results:99+go test ./internal/fileio -run SheetsCorpus -update-from="$HOME/Downloads/012-sheets-corpus - Corpus.csv"100+```101+102+In the workbook, column C holds each formula, which Sheets recalculates103+on import; D and E read its result as text and its kind with formulas104+both 012 and Sheets compute, F holds 012's result and G says whether105+they agree (for two errors, compare C and F by eye). The exporter saves106+formulas Excel has nothing like (`SPLIT`, `REGEXMATCH`, `SORTN`,107+`FLATTEN`, `ARRAYFORMULA` inside a formula) as values, so those rows108+leave C empty and say so in the note: type the formula from B into C109+before downloading, or `-update-from` leaves them unconfirmed.110+72111 ## The XLSX differential test73112 74113 `TestXLSXDifferential` (`internal/fileio/xlsx_diff_test.go`) imports a@@ -0,0 +1,241 @@
1+package fileio2+3+import (4+ "flag"5+ "fmt"6+ "os"7+ "strconv"8+ "strings"9+ "testing"10+11+ "github.com/FelineStateMachine/012/internal/sheet"12+)13+14+// The Sheets corpus (testdata/sheets_corpus.tsv) holds formulas whose15+// results 012 matches with Google Sheets on purpose, each with 012's16+// result and Sheets' as checked in Sheets. TestSheetsCorpus holds 012 to17+// its recorded results and asks every difference from Sheets for a18+// reason. The corpus reaches Sheets as an XLSX workbook written by 012's19+// own exporter (sheetscorpus_xlsx_test.go), and Sheets' answers come back20+// as a CSV download of its corpus sheet.21+var (22+ updateCorpus = flag.Bool("update-corpus", false, "record 012's current results in testdata/sheets_corpus.tsv")23+ corpusXLSX = flag.String("corpus-xlsx", "", "write the Sheets corpus as an XLSX workbook to this path, to import into Sheets")24+ corpusFrom = flag.String("update-from", "", "record Sheets' results from a CSV download of the imported corpus sheet")25+)26+27+const (28+ corpusPath = "testdata/sheets_corpus.tsv"29+ unconfirmed = "unconfirmed" // Sheets' result, until checked in Sheets30+ onPurpose = "on purpose:" // starts the note of a deliberate difference31+)32+33+// corpusFixture is the Data sheet the corpus formulas read, typed as a34+// user would: the oracle's fixture, with a text number, an error, a35+// FALSE and a #N/A in column J.36+//37+// A B C D E F G H I J38+// 1 10 north 1 Apple 1.5 9/26/2026 1 -1000 30 '339+// 2 20 south 2 Banana 2.5 2026-01-31 2 300 20 =1/040+// 3 30 north 3 Cherry 3.5 14:30 2 400 10 FALSE41+// 4 apple east 4 date x 2/29/2024 3 500 =NA()42+// 5 north 5 Apple 9 12/31/2025 543+// 6 ="" 6 TRUE44+// 7 -5 Ab*d 745+var corpusFixture = map[string]string{46+ "A1": "10", "A2": "20", "A3": "30", "A4": "apple", "A7": "-5",47+ "B1": "north", "B2": "south", "B3": "north", "B4": "east", "B5": "north", "B6": `=""`, "B7": "Ab*d",48+ "C1": "1", "C2": "2", "C3": "3", "C4": "4", "C5": "5", "C6": "6", "C7": "7",49+ "D1": "Apple", "D2": "Banana", "D3": "Cherry", "D4": "date", "D5": "Apple",50+ "E1": "1.5", "E2": "2.5", "E3": "3.5", "E4": "x", "E5": "9", "E6": "TRUE",51+ "F1": "9/26/2026", "F2": "2026-01-31", "F3": "14:30", "F4": "2/29/2024", "F5": "12/31/2025",52+ "G1": "1", "G2": "2", "G3": "2", "G4": "3",53+ "H1": "-1000", "H2": "300", "H3": "400", "H4": "500",54+ "I1": "30", "I2": "20", "I3": "10",55+ "J1": "'3", "J2": "=1/0", "J3": "FALSE", "J4": "=NA()",56+}57+58+// The corpus sheet's columns: each formula is in C, and D and E read its59+// result as text and its kind with formulas both 012 and Sheets compute,60+// so both answer in the same terms.61+const (62+ colSection = iota63+ colFormula64+ colResult65+ colText66+ colKind67+ colOurs68+ colCheck69+ colNote70+)71+72+var corpusHeaders = []string{"section", "formula", "result", "as text", "kind", "012 result", "check", "note"}73+74+// corpusLine is a line of the corpus file: a formula row, or a comment,75+// section heading or blank line kept as it is.76+type corpusLine struct {77+ text string78+ section string79+ formula string80+ ours string // 012's recorded result81+ sheets string // Sheets' result, or unconfirmed82+ note string83+}84+85+func readCorpus(t *testing.T) []*corpusLine {86+ t.Helper()87+ data, err := os.ReadFile(corpusPath)88+ if err != nil {89+ t.Fatal(err)90+ }91+ var out []*corpusLine92+ section := ""93+ for _, text := range strings.Split(strings.TrimSuffix(string(data), "\n"), "\n") {94+ if !strings.HasPrefix(text, "=") {95+ if h, ok := strings.CutPrefix(text, "## "); ok {96+ section = h97+ }98+ out = append(out, &corpusLine{text: text})99+ continue100+ }101+ f := append(strings.Split(text, "\t"), "", "", "")102+ out = append(out, &corpusLine{section: section, formula: f[0], ours: f[1], sheets: f[2], note: f[3]})103+ }104+ return out105+}106+107+func writeCorpus(t *testing.T, lines []*corpusLine) {108+ t.Helper()109+ var b strings.Builder110+ for _, l := range lines {111+ if l.formula == "" {112+ b.WriteString(l.text + "\n")113+ continue114+ }115+ if l.sheets == "" {116+ l.sheets = unconfirmed117+ }118+ b.WriteString(strings.TrimRight(strings.Join([]string{l.formula, l.ours, l.sheets, l.note}, "\t"), "\t") + "\n")119+ }120+ if err := os.WriteFile(corpusPath, []byte(b.String()), 0o644); err != nil {121+ t.Fatal(err)122+ }123+}124+125+// corpusRows are the formula rows of lines, in order.126+func corpusRows(lines []*corpusLine) []*corpusLine {127+ var rows []*corpusLine128+ for _, l := range lines {129+ if l.formula != "" {130+ rows = append(rows, l)131+ }132+ }133+ return rows134+}135+136+// corpusBook builds the workbook the corpus is computed in: the Corpus137+// sheet, a row per formula under a header row, and the Data sheet.138+func corpusBook(t *testing.T, rows []*corpusLine) *sheet.Sheet {139+ t.Helper()140+ s := sheet.New()141+ book := s.Book()142+ if err := book.RenameSheet(s, "Corpus"); err != nil {143+ t.Fatal(err)144+ }145+ data, err := book.AddSheet("Data", 1)146+ if err != nil {147+ t.Fatal(err)148+ }149+ for a, in := range corpusFixture {150+ at := addr(t, a)151+ setAt(t, data, at.Col, at.Row, in)152+ }153+ for c, h := range corpusHeaders {154+ setAt(t, s, c, 0, "'"+h)155+ }156+ for i, r := range rows {157+ n := strconv.Itoa(i + 2)158+ setAt(t, s, colSection, i+1, r.section)159+ setAt(t, s, colFormula, i+1, "'"+r.formula)160+ setAt(t, s, colResult, i+1, r.formula)161+ setAt(t, s, colText, i+1, `=C`+n+`&""`)162+ setAt(t, s, colKind, i+1, kindFormula("C"+n))163+ }164+ for c, w := range []int{22, 48, 14, 24, 8, 24, 8, 30} {165+ s.SetColWidth(c, w)166+ }167+ return s168+}169+170+// kindFormula names the kind of the value in ref, in words both 012 and171+// Sheets compute alike.172+func kindFormula(ref string) string {173+ return fmt.Sprintf(`=IF(ISERROR(%[1]s), "error", IF(ISNUMBER(%[1]s), "number", IF(ISTEXT(%[1]s), "text", `+174+ `IF(ISLOGICAL(%[1]s), "logical", IF(ISBLANK(%[1]s), "blank", "other")))))`, ref)175+}176+177+// setAt types in into the cell at col and row of s.178+func setAt(t *testing.T, s *sheet.Sheet, col, row int, in string) {179+ t.Helper()180+ a := sheet.Addr{Col: col, Row: row}181+ if err := s.Set(a, in); err != nil {182+ t.Errorf("%s %q: %v", a, in, err)183+ }184+}185+186+// corpusResult reads what 012 computed for the formula on row i of the187+// corpus sheet, in the corpus file's terms.188+func corpusResult(s *sheet.Sheet, i int) string {189+ text := s.Value(sheet.Addr{Col: colText, Row: i + 1})190+ kind := s.Value(sheet.Addr{Col: colKind, Row: i + 1})191+ return encodeResult(text.String(), kind.String())192+}193+194+// encodeResult writes a result, given as text and its kind, as the195+// corpus file does: text quoted, (blank) for an empty cell, and196+// numbers, booleans and errors as they read.197+func encodeResult(text, kind string) string {198+ switch kind {199+ case "number", "logical", "error":200+ return text201+ case "text":202+ return strconv.Quote(text)203+ case "blank":204+ return "(blank)"205+ }206+ return "(" + kind + ") " + text207+}208+209+func TestSheetsCorpus(t *testing.T) {210+ lines := readCorpus(t)211+ rows := corpusRows(lines)212+ s := corpusBook(t, rows)213+ if *corpusFrom != "" {214+ foldSheets(t, rows, *corpusFrom)215+ }216+ for i, r := range rows {217+ got := corpusResult(s, i)218+ if *updateCorpus {219+ r.ours = got220+ }221+ checkCorpusRow(t, r, got)222+ }223+ if *updateCorpus || *corpusFrom != "" {224+ writeCorpus(t, lines)225+ }226+ if *corpusXLSX != "" {227+ exportCorpus(t, s, rows, *corpusXLSX)228+ }229+}230+231+// checkCorpusRow holds 012 to its recorded result, and a difference232+// from Sheets to a note saying why.233+func checkCorpusRow(t *testing.T, r *corpusLine, got string) {234+ t.Helper()235+ if got != r.ours {236+ t.Errorf("%s = %s, recorded %s (go test ./internal/fileio -run SheetsCorpus -update-corpus records it)", r.formula, got, r.ours)237+ }238+ if r.sheets != "" && r.sheets != unconfirmed && r.sheets != r.ours && !strings.HasPrefix(r.note, onPurpose) {239+ t.Errorf("%s = %s in 012 and %s in Sheets: make 012 agree, or say why in a note starting %q", r.formula, r.ours, r.sheets, onPurpose)240+ }241+}@@ -0,0 +1,96 @@
1+package fileio2+3+import (4+ "context"5+ "encoding/csv"6+ "os"7+ "slices"8+ "strconv"9+ "testing"10+11+ "github.com/FelineStateMachine/012/internal/sheet"12+)13+14+// sheetsOnlyNote marks a corpus row whose formula 012's exporter saves as15+// its value, because Excel has nothing the same (formula.go): in Sheets16+// the formula has to be typed into C.17+const sheetsOnlyNote = "saved as a value: type the formula in B into C"18+19+// exportCorpus writes the corpus workbook s as an XLSX file at name with20+// 012's exporter, for importing into Sheets. Sheets recalculates C, D21+// and E on import; F keeps 012's results, and G compares the two.22+func exportCorpus(t *testing.T, s *sheet.Sheet, rows []*corpusLine, name string) {23+ t.Helper()24+ for i, r := range rows {25+ n := strconv.Itoa(i + 2)26+ setAt(t, s, colOurs, i+1, "'"+corpusResult(s, i))27+ setAt(t, s, colCheck, i+1, checkFormula(n))28+ note := r.note29+ if !excelKeeps(r.formula) {30+ setAt(t, s, colResult, i+1, "")31+ note = sheetsOnlyNote32+ }33+ setAt(t, s, colNote, i+1, note)34+ }35+ res, err := Export(context.Background(), name, XLSX, SnapBook(s), ExportOptions{})36+ if err != nil {37+ t.Fatal(err)38+ }39+ for _, n := range res.Notes {40+ t.Log(n)41+ }42+ t.Logf("wrote %d formulas to %s", len(rows), name)43+}44+45+// checkFormula compares, on row n, the result in D and E with 012's in46+// F, writing it as encodeResult does. Errors only say both are errors;47+// the error in C and the one in F are compared by eye.48+func checkFormula(n string) string {49+ d, e, f := "D"+n, "E"+n, "F"+n50+ enc := `IF(` + e + `="text", """" & ` + d + ` & """", IF(` + e + `="blank", "(blank)", ` + d + ` & ""))`51+ return `=IFERROR(IF(EXACT(` + enc + `, ` + f + `), "same", "DIFFERS"), IF(LEFT(` + f + `, 1)="#", "both errors", "DIFFERS"))`52+}53+54+// excelKeeps reports whether 012's exporter keeps formula as a formula.55+func excelKeeps(formula string) bool {56+ _, ok := toExcelFormula(formula, nil)57+ return ok58+}59+60+// foldSheets records Sheets' results from name, a CSV download of the61+// 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.64+func foldSheets(t *testing.T, rows []*corpusLine, name string) {65+ t.Helper()66+ f, err := os.Open(name)67+ if err != nil {68+ t.Fatal(err)69+ }70+ defer f.Close()71+ recs, err := csv.NewReader(f).ReadAll()72+ if err != nil {73+ t.Fatal(err)74+ }75+ if len(recs) == 0 || !slices.Equal(recs[0][:colOurs], corpusHeaders[:colOurs]) {76+ t.Fatalf("%s: expected a CSV download of the corpus sheet, with headers %q", name, corpusHeaders[:colOurs])77+ }78+ sheets := map[string]string{}79+ for _, rec := range recs[1:] {80+ if len(rec) > colKind {81+ sheets[rec[colFormula]] = encodeResult(rec[colText], rec[colKind])82+ }83+ }84+ skipped := 085+ for _, r := range rows {86+ got, ok := sheets[r.formula]87+ if !ok || (got == "(blank)" && !excelKeeps(r.formula)) {88+ skipped++89+ continue90+ }91+ r.sheets = got92+ }93+ if skipped > 0 {94+ t.Logf("%d formulas have no result from Sheets in %s", skipped, name)95+ }96+}@@ -0,0 +1,320 @@
1+# Formulas where 012 matches Google Sheets on purpose (docs/contributing/testing.md).2+# Tab-separated: formula, 012's result, Sheets' result, note.3+# Results read "text" (quoted), 12.5, TRUE, #N/A, or (blank) for an empty cell.4+# Sheets' result is "unconfirmed" until checked in Sheets. A row where it5+# differs from 012's needs a note starting "on purpose:" that says why.6+# Every formula computes one value; array results are measured with ROWS,7+# COLUMNS, INDEX or TEXTJOIN. Data! is the fixture sheet (sheetscorpus_test.go).8+## 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 unconfirmed52+## 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! unconfirmed91+## Regular expressions and SPLIT92+=REGEXMATCH("ABC", "(?i)abc") TRUE unconfirmed93+=REGEXMATCH(123, "1") #VALUE! unconfirmed94+=REGEXEXTRACT("abc", "\d+") #N/A unconfirmed95+=TEXTJOIN("|", TRUE, REGEXEXTRACT("a1b2", "(\d)b(\d)")) "1" unconfirmed96+=REGEXREPLACE("john smith", "(\w+) (\w+)", "$2, $1") "smith, john" unconfirmed97+=COLUMNS(SPLIT("a,b,,c", ",", TRUE, FALSE)) 4 unconfirmed98+=SUM(SPLIT("1,2,3", ",")) 6 unconfirmed99+=COLUMNS(SPLIT("a-b_c", "-_")) 3 unconfirmed100+## 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" unconfirmed123+## 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! unconfirmed166+## 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! unconfirmed190+## 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" unconfirmed223+## 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 unconfirmed260+## 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" unconfirmed272+=TEXTJOIN(",", TRUE, FLATTEN(Data!C1:D2)) "1,Apple,2,Banana" unconfirmed273+=ROWS(FLATTEN(Data!C1:D2, Data!G1:G2)) 6 unconfirmed274+=TEXTJOIN(",", TRUE, SORTN(Data!C1:C7, 3, 0, 1, FALSE)) "7,6,5" unconfirmed275+=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 unconfirmed278+=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 unconfirmed285+## 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 unconfirmed298+## 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 unconfirmed303+## 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! unconfirmed