cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

File fixtures: workbooks each release saved open and save back byte for byte

Dami committed cecd28e
The .012 format is written down at version 5 with what stays compatible,
fixtures/new holds hand-written workbooks using everything the format
stores, v0.3.0 is saved by this build and v0.2.0 by that tag's writer,
and releasing adds a release's fixtures with go test -fixtures.
commit
cecd28e78723b3871966f2aca2ddc3e3289c4b79
parent
92e5253fa14897097fc97c6f1c28f2d8bd5d5af4
author
Dami <dami.etoile@gmail.com>
kind
test risk score 0.7 of 3, Jev's estimate
13 files changed, +711 -5
Mdocs/contributing/releasing.md37+++++++-
Mdocs/contributing/testing.md5+-
Mdocs/files/format.md26+++++-
Ainternal/sheet/fixtures_test.go140+++++++++++++++++++++++++++++
Ainternal/sheet/testdata/fixtures/new/plain.01210++
Ainternal/sheet/testdata/fixtures/new/single.01221++++
Ainternal/sheet/testdata/fixtures/new/workbook.012145++++++++++++++++++++++++++++++
Ainternal/sheet/testdata/fixtures/v0.2.0/plain.01210++
Ainternal/sheet/testdata/fixtures/v0.2.0/single.01220++++
Ainternal/sheet/testdata/fixtures/v0.2.0/workbook.012127++++++++++++++++++++++++++
Ainternal/sheet/testdata/fixtures/v0.3.0/plain.01210++
Ainternal/sheet/testdata/fixtures/v0.3.0/single.01221++++
Ainternal/sheet/testdata/fixtures/v0.3.0/workbook.012144++++++++++++++++++++++++++++++
M docs/contributing/releasing.md +34 -3
@@ -21,7 +21,8 @@ flowchart TD
2121 small -->|no| stress[make stress, make stress-report]2222 small -->|yes| xlsx2323 stress -->|the run recorded on the commit| xlsx[XLSX opened in Excel, LibreOffice, Sheets]24- xlsx --> tag[git tag -a, git push]24+ xlsx --> fixtures[this release's file fixtures committed]25+ fixtures --> tag[git tag -a, git push]2526 tag -->|the module proxy| install[go install ...@v1.2.3]2627 tag --> dist[make dist]2728 dist --> archives[dist/: archives, SHA256SUMS]
@@ -42,7 +43,9 @@ flowchart TD
4243 Excel, LibreOffice Calc and Google Sheets: it opens without a repair4344 prompt, and values, formulas and formats read as they did in 012. The4445 automated checks read XLSX with excelize and 012's own reader only.45-4. **Tag** with the release notes as the message, what changed since the46+4. **File fixtures**: the workbooks this release saves, committed (see47+ [File fixtures](#file-fixtures)).48+5. **Tag** with the release notes as the message, what changed since the4649 previous tag for a user, with the install line last:4750 4851 ```sh
@@ -52,7 +55,35 @@ flowchart TD
5255 5356 The Go module proxy picks the tag up, so `go install5457 github.com/FelineStateMachine/012/cmd/012@v1.2.3` works from then on.55-5. **Binaries**: `make dist VERSION=v1.2.3` on the tagged commit.58+6. **Binaries**: `make dist VERSION=v1.2.3` on the tagged commit.59+60+## File fixtures61+62+`internal/sheet/testdata/fixtures` holds workbooks as each release saved63+them, a folder per release (`v0.2.0/`), and64+`TestFixturesOpenAndSaveUnchanged` opens every one and saves it again,65+failing unless the bytes come back the same. A later build that can't66+open a release's file, or saves it differently, fails `make check`67+([The .012 format](../files/format.md#versions) says what that68+promises).69+70+The workbooks a release saves are the ones in `fixtures/new`, written by71+hand to use everything the format stores: formats, column and row72+formats, widths, heights, merges, borders, freeze, filters, names,73+notes, protection, rules and validation, charts, pivot tables, notebook74+sheets with shell regions and linked files, macros, the locale and75+decimal arithmetic. A release that adds to the format adds what it76+adds there. Then, on the commit to be tagged:77+78+```sh79+go test ./internal/sheet -run TestFixtures -fixtures v1.2.380+```81+82+saves them with that build into `fixtures/v1.2.3`, to commit with the83+release. The test refuses a folder that exists. A released folder is84+never edited or replaced: when a later build saves one differently, the85+build is what changes. The folder of a release not yet tagged may be86+deleted and saved again.5687 5788 ## make dist5889
M docs/contributing/testing.md +4 -1
M docs/files/format.md +25 -1
@@ -6,7 +6,9 @@ sidebar_position: 4
66 # The .012 format77 88 `.012` files are JSON with one line per cell, keyed by address, so diffs9-read naturally and files merge reasonably in version control. A cell9+read naturally and files merge reasonably in version control. This page10+is the format at version 5, the newest; [Versions](#versions) says what11+each version adds and what 012 promises about old files. A cell1012 without formatting is just what was typed; a formatted cell is a small1113 object:1214
@@ -48,6 +50,13 @@ spacing, and the case of field names don't matter. A cell given twice
4850 takes its last entry, though an invalid earlier one is refused; the4951 `version` or the `sheets` list given twice around cells is refused.5052 53+012 writes one form of each workbook: the fields in the order this page54+shows them, cells by row and then column, widths and heights by55+position, column and row formats with the whole sheet's first, then56+columns', then rows', names by name, and charts, rules, regions and57+macros in the order they were made. Saving a file 012 wrote, without58+changing it, gives back the same bytes.59+5160 012 reads and writes the file as a stream, each cell straight into or5261 out of its sheet, so opening a file takes about the memory of the5362 workbook however large the file; what that costs, and why there is no
@@ -68,6 +77,21 @@ still load.
6877 | 4 | Several sheets, or a formula naming a sheet | `sheets`, a list; see [Several sheets](#several-sheets) |6978 | 5 | A pivot table | a sheet's `pivot`; see [Pivot tables](#pivot-tables) |7079 80+What stays the same from release to release:81+82+- Every file a release saved opens in every later build, and saving it83+ unchanged gives back its bytes. Each release keeps workbooks it saved84+ as fixtures (`internal/sheet/testdata/fixtures/<release>`), which every85+ build's tests open and save again86+ ([Releasing](../contributing/releasing.md#file-fixtures)).87+- Fields are only added, never renamed or given another meaning; the88+ version rises only for a field older builds can't do without, and the89+ rest are listed under [Fields that need no version](#fields-that-need-no-version).90+- An older build ignores a field it doesn't know, but refuses a value it91+ doesn't know in a field it does (a chart type, a rule's condition such92+ as a date period), rather than open the file with the rule or chart93+ quietly changed.94+7195 ## Fields that need no version7296 7397 Builds that don't know these fields ignore them (and drop them if they
A internal/sheet/fixtures_test.go +140 -0
@@ -0,0 +1,140 @@
1+package sheet2+3+import (4+ "bytes"5+ "flag"6+ "fmt"7+ "os"8+ "path/filepath"9+ "regexp"10+ "strings"11+ "testing"12+)13+14+// saveFixtures names a release: go test -run TestFixtures -fixtures15+// v1.2.3 saves the workbooks in testdata/fixtures/new with this build16+// into testdata/fixtures/v1.2.3. See docs/contributing/releasing.md.17+var saveFixtures = flag.String("fixtures", "", "save testdata/fixtures/new into testdata/fixtures/<release>")18+19+var releaseDir = regexp.MustCompile(`^v\d+\.\d+\.\d+$`)20+21+// TestFixturesOpenAndSaveUnchanged opens every workbook a release saved22+// (testdata/fixtures/<release>) and saves it again: the bytes must be23+// the file's own. The writer puts every map in a fixed order (cells by24+// row then column, widths and lines by column, names by name), so byte25+// order never legitimately varies and the comparison is exact.26+// v0.2.0's fixtures were saved by that tag from fixtures/new, less the27+// rule and validation options it refuses (date periods, top values,28+// duplicates, averages, data bars, icon sets, dropdown display and29+// checkbox values).30+func TestFixturesOpenAndSaveUnchanged(t *testing.T) {31+ if *saveFixtures != "" {32+ writeFixtures(t, *saveFixtures)33+ }34+ dirs, err := os.ReadDir("testdata/fixtures")35+ if err != nil {36+ t.Fatal(err)37+ }38+ n := 039+ for _, d := range dirs {40+ if !d.IsDir() || !releaseDir.MatchString(d.Name()) {41+ continue42+ }43+ files, _ := filepath.Glob(filepath.Join("testdata/fixtures", d.Name(), "*.012"))44+ for _, path := range files {45+ n++46+ t.Run(d.Name()+"/"+filepath.Base(path), func(t *testing.T) {47+ want, err := os.ReadFile(path)48+ if err != nil {49+ t.Fatal(err)50+ }51+ if got := resave(t, want); !bytes.Equal(got, want) {52+ t.Errorf("saved differently:\n%s", lineDiff(string(want), string(got)))53+ }54+ })55+ }56+ }57+ if n == 0 {58+ t.Fatal("no fixtures in testdata/fixtures/v*")59+ }60+}61+62+// TestFixtureSourcesStable checks that the workbooks the next release's63+// fixtures are made from open, and that what this build saves of them64+// saves again unchanged, so -fixtures always makes fixtures that pass.65+func TestFixtureSourcesStable(t *testing.T) {66+ files, _ := filepath.Glob("testdata/fixtures/new/*.012")67+ if len(files) == 0 {68+ t.Fatal("no workbooks in testdata/fixtures/new")69+ }70+ for _, path := range files {71+ t.Run(filepath.Base(path), func(t *testing.T) {72+ src, err := os.ReadFile(path)73+ if err != nil {74+ t.Fatal(err)75+ }76+ once := resave(t, src)77+ if twice := resave(t, once); !bytes.Equal(once, twice) {78+ t.Errorf("saved differently the second time:\n%s", lineDiff(string(once), string(twice)))79+ }80+ })81+ }82+}83+84+// resave opens a file and returns what this build saves of it.85+func resave(t *testing.T, data []byte) []byte {86+ t.Helper()87+ wb, err := ReadBook(bytes.NewReader(data))88+ if err != nil {89+ t.Fatalf("open: %v", err)90+ }91+ var out bytes.Buffer92+ if err := wb.Write(&out); err != nil {93+ t.Fatalf("save: %v", err)94+ }95+ return out.Bytes()96+}97+98+// writeFixtures saves testdata/fixtures/new with this build as the99+// fixtures of release, refusing to replace a release's fixtures.100+func writeFixtures(t *testing.T, release string) {101+ if !releaseDir.MatchString(release) {102+ t.Fatalf("-fixtures %q: want a release such as v1.2.3", release)103+ }104+ dir := filepath.Join("testdata/fixtures", release)105+ if _, err := os.Stat(dir); err == nil {106+ t.Fatalf("%s exists: a release's fixtures are never replaced", dir)107+ }108+ if err := os.MkdirAll(dir, 0o755); err != nil {109+ t.Fatal(err)110+ }111+ files, _ := filepath.Glob("testdata/fixtures/new/*.012")112+ for _, path := range files {113+ src, err := os.ReadFile(path)114+ if err != nil {115+ t.Fatal(err)116+ }117+ if err := os.WriteFile(filepath.Join(dir, filepath.Base(path)), resave(t, src), 0o644); err != nil {118+ t.Fatal(err)119+ }120+ }121+}122+123+// lineDiff lists the lines of want and got that differ, by line number.124+func lineDiff(want, got string) string {125+ w, g := strings.Split(want, "\n"), strings.Split(got, "\n")126+ var b strings.Builder127+ for i := range max(len(w), len(g)) {128+ var wl, gl string129+ if i < len(w) {130+ wl = w[i]131+ }132+ if i < len(g) {133+ gl = g[i]134+ }135+ if wl != gl {136+ fmt.Fprintf(&b, "%d:\n want %s\n got %s\n", i+1, wl, gl)137+ }138+ }139+ return b.String()140+}
A internal/sheet/testdata/fixtures/new/plain.012 +10 -0
A internal/sheet/testdata/fixtures/new/single.012 +21 -0
A internal/sheet/testdata/fixtures/new/workbook.012 +145 -0
@@ -0,0 +1,145 @@
1+{2+ "version": 5,3+ "names": {"Units": "Sales!C2:C9", "Lost": "#REF!", "Rate": "Lists!B1"},4+ "macroOrigin": "fixture-machine",5+ "macros": [6+ {"name": "Bold header", "key": "1", "api": 1, "source": "select(\"A1:E1\")\nrun(\"format.bold\")\n"},7+ {"name": "Fill totals", "api": 1, "source": "set(\"F2\", \"=SUM(C2:C9)\")\n"}8+ ],9+ "shellHistory": ["ls", "open data.csv | first 3"],10+ "active": 0,11+ "sheets": [12+ {13+ "name": "Sales",14+ "widths": {"A": 12, "B": 10, "E": 14},15+ "heights": {"1": 2, "12": 3},16+ "lines": {"A:XFD": {"italic": true}, "D:D": {"format": "currency", "decimals": 2}, "1:1": {"bold": true, "align": "center", "borders": {"bottom": "double"}}},17+ "freeze": {"rows": 1, "cols": 1},18+ "filter": {"range": "A1:E9", "columns": {"B": {"hidden": ["West"]}, "C": {"condition": "gt", "value": "2"}}},19+ "protected": [{"range": "E2:E9", "description": "Totals"}],20+ "merges": ["A11:C11", "A12:A13"],21+ "cells": {22+ "A1": "Date",23+ "B1": "Region",24+ "C1": "Units",25+ "D1": "Price",26+ "E1": "Total",27+ "A2": {"input": "2026-01-05", "format": "date"},28+ "B2": "North",29+ "C2": "3",30+ "D2": "12.5",31+ "E2": {"input": "=C2*D2", "format": "currency", "decimals": 2, "own": true},32+ "A3": {"input": "2026-01-12", "format": "date"},33+ "B3": {"input": "South", "note": "Includes returns"},34+ "C3": "5",35+ "D3": "9.75",36+ "E3": "=C3*D3",37+ "A4": {"input": "2026-02-02", "format": "date"},38+ "B4": "West",39+ "C4": "1",40+ "D4": "40",41+ "E4": "=C4*D4",42+ "A5": {"input": "2026-02-16", "format": "date"},43+ "B5": "North",44+ "C5": "7",45+ "D5": "12.5",46+ "E5": "=C5*D5",47+ "A6": {"input": "2026-03-01", "format": "date"},48+ "B6": "East",49+ "C6": {"input": "2", "bold": true, "italic": true, "underline": true, "strikethrough": true},50+ "D6": "22",51+ "E6": "=C6*D6",52+ "A7": {"input": "2026-03-09", "format": "date"},53+ "B7": "South",54+ "C7": {"input": "4", "align": "left", "valign": "top"},55+ "D7": "9.75",56+ "E7": "=C7*D7",57+ "A8": {"input": "2026-03-20", "format": "date"},58+ "B8": "East",59+ "C8": {"input": "6", "borders": {"top": "thin", "bottom": "double", "left": "thick", "right": "thin", "bottomColor": "red", "leftColor": "blue"}},60+ "D8": "22",61+ "E8": "=C8*D8",62+ "A9": {"input": "2026-03-31", "format": "date"},63+ "B9": "West",64+ "C9": "8",65+ "D9": "40",66+ "E9": "=C9*D9",67+ "G1": "Share",68+ "G2": {"input": "=C2/SUM(Units)", "format": "percent", "decimals": 1},69+ "G3": {"input": "=C3/SUM(Units)", "format": "custom", "pattern": "0.00%;[Red]-0.00%"},70+ "H1": "Regions",71+ "H2": "=SORT(UNIQUE(B2:B9))",72+ "I1": "Scaled",73+ "I2": "=LET(r, Rate, MAP(C2:C4, LAMBDA(x, x*r)))",74+ "A11": {"input": "A long title merged across three columns", "wrap": "wrap", "align": "center"},75+ "A12": {"input": "Tall", "valign": "middle"},76+ "B12": {"input": "clipped text that does not spill over", "wrap": "clip"},77+ "C12": "TRUE",78+ "D12": "=IFERROR(1/0, \"none\")",79+ "E12": {"input": "=Lists!B1*2", "format": "number", "decimals": 3},80+ "F12": "'00123",81+ "F13": "=HYPERLINK(\"https://example.com\", \"site\")"82+ },83+ "charts": [84+ {"type":"column","data":"A1:C9","at":"K2","width":40,"height":12,"header":true,"labels":true,"title":"Units by date"},85+ {"type":"line","data":"C1:E9","at":"K16","width":36,"height":10,"header":true,"stack":"stacked","trend":true,"min":0,"max":400,"gridlines":false,"legend":"none"},86+ {"type":"pie","data":"B1:C5","at":"K28","width":30,"height":10,"header":true,"labels":true,"legend":"right"},87+ {"type":"scatter","data":"C1:D9","at":"K40","width":30,"height":10,"header":true,"trend":true,"log":true},88+ {"type":"bar","data":"B1:C9","at":"K52","width":30,"height":10,"header":true,"stack":"percent"},89+ {"type":"area","data":"C1:D9","at":"K64","width":30,"height":10,"byRow":true,"header":true}90+ ],91+ "conditionalFormats": [92+ {"ranges":"C2:C9","condition":"gt","values":["5"],"text":"red","bold":true},93+ {"ranges":"B2:B9","condition":"contains","values":["th"],"fill":"yellow","italic":true},94+ {"ranges":"A2:A9","condition":"date_is","values":["this week"],"underline":true},95+ {"ranges":"E2:E9","condition":"formula","values":["=$C2>4"],"text":"green","strikethrough":true},96+ {"ranges":"C2:C9","condition":"between","values":["2","4"],"fill":"cyan"},97+ {"ranges":"C2:C9","condition":"top","values":["3"],"fill":"magenta"},98+ {"ranges":"D2:D9","condition":"duplicate","text":"blue"},99+ {"ranges":"C2:C9","condition":"above_average","bold":true},100+ {"ranges":"D2:D9","scale":[{"type":"min","color":"green"},{"type":"percentile","value":"50","color":"yellow"},{"type":"max","color":"red"}]},101+ {"ranges":"E2:E9","dataBar":{"color":"blue","min":{"type":"min"},"max":{"type":"num","value":"400"},"barOnly":true}},102+ {"ranges":"C2:C9,D2:D9","iconSet":{"icons":"arrows","points":[{"type":"percent","value":"33"},{"type":"percent","value":"67"}],"reverse":true}}103+ ],104+ "validations": [105+ {"ranges":"B2:B9","criteria":"list","items":["North","South","East","West"],"help":"Pick a region","display":"chip"},106+ {"ranges":"C2:C9","criteria":"number","condition":"between","values":["0","80"],"reject":true},107+ {"ranges":"A2:A9","criteria":"date","condition":"gt","values":["2025-12-31"]},108+ {"ranges":"C12","criteria":"checkbox","items":["TRUE","FALSE"]},109+ {"ranges":"F2:F9","criteria":"range","source":"Lists!A1:A4","display":"plain"},110+ {"ranges":"G5:G9","criteria":"length","condition":"le","values":["10"]},111+ {"ranges":"H12","criteria":"formula","values":["=H12<>\"\""]}112+ ]113+ },114+ {115+ "name": "Lists",116+ "hidden": true,117+ "protected": [{"sheet": true}],118+ "cells": {119+ "A1": "North",120+ "A2": "South",121+ "A3": "East",122+ "A4": "West",123+ "B1": {"input": "1.5", "note": "Rate used by Sales!I2"}124+ }125+ },126+ {127+ "name": "Pivot Table 1",128+ "cells": {},129+ "pivot": {"source":"Sales!A1:E9","rows":[{"column":"B"},{"column":"A","order":"desc","sortBy":1}],"columns":[{"column":"C"}],"values":[{"column":"E","summarize":"sum"},{"column":"C","summarize":"counta","showAs":"percent_of_total","name":"Share"}],"filters":[{"column":"B","hidden":["West"]}],"rowTotals":true,"columnTotals":false}130+ },131+ {132+ "name": "Notebook 1",133+ "widths": {"A": 20},134+ "cells": {},135+ "notebook": true,136+ "regions": [137+ {"name":"r1","command":"ls","at":"A1","rows":4,"cols":3},138+ {"name":"big","command":"$r1 | where size > 1kb","at":"A8","rows":3,"cols":3,"reads":["r1"],"input":"Sales!A1:E9","sort":[{"column":3,"desc":true}]},139+ {"name":"app","at":"F2","path":"logs/app.csv","window":500},140+ {"name":"db","at":"F20","path":"data.sqlite","format":"SQLite","table":"orders"},141+ {"name":"q","at":"M20","path":"data.sqlite","format":"SQLite","query":"select * from orders limit 5"}142+ ]143+ }144+ ]145+}
A internal/sheet/testdata/fixtures/v0.2.0/plain.012 +10 -0
A internal/sheet/testdata/fixtures/v0.2.0/single.012 +20 -0
A internal/sheet/testdata/fixtures/v0.2.0/workbook.012 +127 -0
@@ -0,0 +1,127 @@
1+{2+ "version": 5,3+ "names": {"Lost": "#REF!", "Rate": "Lists!B1", "Units": "Sales!C2:C9"},4+ "macroOrigin": "fixture-machine",5+ "macros": [6+ {"name": "Bold header", "key": "1", "api": 1, "source": "select(\"A1:E1\")\nrun(\"format.bold\")\n"},7+ {"name": "Fill totals", "api": 1, "source": "set(\"F2\", \"=SUM(C2:C9)\")\n"}8+ ],9+ "sheets": [10+ {11+ "name": "Sales",12+ "widths": {"A": 12, "E": 14},13+ "lines": {"A:XFD": {"italic":true}, "D:D": {"format":"currency","decimals":2}, "1:1": {"bold":true,"align":"center"}},14+ "freeze": {"rows":1,"cols":1},15+ "filter": {"range":"A1:E9","columns":{"B":{"hidden":["West"]},"C":{"condition":"gt","value":"2"}}},16+ "protected": [{"range":"E2:E9","description":"Totals"}],17+ "cells": {18+ "A1": "Date",19+ "B1": "Region",20+ "C1": "Units",21+ "D1": "Price",22+ "E1": "Total",23+ "G1": "Share",24+ "H1": "Regions",25+ "I1": "Scaled",26+ "A2": {"input":"2026-01-05","format":"date"},27+ "B2": "North",28+ "C2": "3",29+ "D2": "12.5",30+ "E2": {"input":"=C2*D2","format":"currency","decimals":2,"own":true},31+ "G2": {"input":"=C2/SUM(Units)","format":"percent","decimals":1},32+ "H2": "=SORT(UNIQUE(B2:B9))",33+ "I2": "=LET(r, Rate, MAP(C2:C4, LAMBDA(x, x*r)))",34+ "A3": {"input":"2026-01-12","format":"date"},35+ "B3": {"input":"South","note":"Includes returns"},36+ "C3": "5",37+ "D3": "9.75",38+ "E3": "=C3*D3",39+ "G3": {"input":"=C3/SUM(Units)","format":"custom","pattern":"0.00%;[Red]-0.00%"},40+ "A4": {"input":"2026-02-02","format":"date"},41+ "B4": "West",42+ "C4": "1",43+ "D4": "40",44+ "E4": "=C4*D4",45+ "A5": {"input":"2026-02-16","format":"date"},46+ "B5": "North",47+ "C5": "7",48+ "D5": "12.5",49+ "E5": "=C5*D5",50+ "A6": {"input":"2026-03-01","format":"date"},51+ "B6": "East",52+ "C6": {"input":"2","bold":true,"italic":true,"underline":true,"strikethrough":true},53+ "D6": "22",54+ "E6": "=C6*D6",55+ "A7": {"input":"2026-03-09","format":"date"},56+ "B7": "South",57+ "C7": {"input":"4","align":"left"},58+ "D7": "9.75",59+ "E7": "=C7*D7",60+ "A8": {"input":"2026-03-20","format":"date"},61+ "B8": "East",62+ "C8": "6",63+ "D8": "22",64+ "E8": "=C8*D8",65+ "A9": {"input":"2026-03-31","format":"date"},66+ "B9": "West",67+ "C9": "8",68+ "D9": "40",69+ "E9": "=C9*D9",70+ "A11": {"input":"A long title merged across three columns","align":"center"},71+ "A12": "Tall",72+ "B12": "clipped text that does not spill over",73+ "C12": "TRUE",74+ "D12": "=IFERROR(1/0, \"none\")",75+ "E12": {"input":"=Lists!B1*2","format":"number","decimals":3},76+ "F12": "'00123",77+ "F13": "=HYPERLINK(\"https://example.com\", \"site\")"78+ },79+ "charts": [80+ {"type":"column","data":"A1:C9","at":"K2","width":40,"height":12,"header":true,"labels":true,"title":"Units by date"},81+ {"type":"line","data":"C1:E9","at":"K16","width":36,"height":10,"header":true,"stack":"stacked","trend":true,"min":0,"max":400,"gridlines":false,"legend":"none"},82+ {"type":"pie","data":"B1:C5","at":"K28","width":30,"height":10,"header":true,"labels":true,"legend":"right"},83+ {"type":"scatter","data":"C1:D9","at":"K40","width":30,"height":10,"header":true,"trend":true,"log":true},84+ {"type":"bar","data":"B1:C9","at":"K52","width":30,"height":10,"header":true,"stack":"percent"},85+ {"type":"area","data":"C1:D9","at":"K64","width":30,"height":10,"byRow":true,"header":true}86+ ],87+ "conditionalFormats": [88+ {"ranges":"C2:C9","condition":"gt","values":["5"],"text":"red","bold":true},89+ {"ranges":"B2:B9","condition":"contains","values":["th"],"fill":"yellow","italic":true},90+ {"ranges":"E2:E9","condition":"formula","values":["=$C2\u003e4"],"text":"green","strikethrough":true},91+ {"ranges":"C2:C9","condition":"between","values":["2","4"],"fill":"cyan"},92+ {"ranges":"D2:D9","scale":[{"type":"min","color":"green"},{"type":"percentile","value":"50","color":"yellow"},{"type":"max","color":"red"}]}93+ ],94+ "validations": [95+ {"ranges":"B2:B9","criteria":"list","items":["North","South","East","West"],"help":"Pick a region"},96+ {"ranges":"C2:C9","criteria":"number","condition":"between","values":["0","80"],"reject":true},97+ {"ranges":"A2:A9","criteria":"date","condition":"gt","values":["2025-12-31"]},98+ {"ranges":"C12","criteria":"checkbox"},99+ {"ranges":"F2:F9","criteria":"range","source":"Lists!A1:A4"},100+ {"ranges":"G5:G9","criteria":"length","condition":"le","values":["10"]},101+ {"ranges":"H12","criteria":"formula","values":["=H12\u003c\u003e\"\""]}102+ ]103+ },104+ {105+ "name": "Lists",106+ "hidden": true,107+ "protected": [{"sheet":true}],108+ "cells": {109+ "A1": "North",110+ "B1": {"input":"1.5","note":"Rate used by Sales!I2"},111+ "A2": "South",112+ "A3": "East",113+ "A4": "West"114+ }115+ },116+ {117+ "name": "Pivot Table 1",118+ "cells": {},119+ "pivot": {"source":"Sales!A1:E9","rows":[{"column":"B"},{"column":"A","order":"desc","sortBy":1}],"columns":[{"column":"C"}],"values":[{"column":"E","summarize":"sum"},{"column":"C","summarize":"counta","showAs":"percent_of_total","name":"Share"}],"filters":[{"column":"B","hidden":["West"]}],"rowTotals":true,"columnTotals":false}120+ },121+ {122+ "name": "Notebook 1",123+ "widths": {"A": 20},124+ "cells": {}125+ }126+ ]127+}
A internal/sheet/testdata/fixtures/v0.3.0/plain.012 +10 -0
A internal/sheet/testdata/fixtures/v0.3.0/single.012 +21 -0
A internal/sheet/testdata/fixtures/v0.3.0/workbook.012 +144 -0
@@ -0,0 +1,144 @@
1+{2+ "version": 5,3+ "names": {"Lost": "#REF!", "Rate": "Lists!B1", "Units": "Sales!C2:C9"},4+ "macroOrigin": "fixture-machine",5+ "macros": [6+ {"name": "Bold header", "key": "1", "api": 1, "source": "select(\"A1:E1\")\nrun(\"format.bold\")\n"},7+ {"name": "Fill totals", "api": 1, "source": "set(\"F2\", \"=SUM(C2:C9)\")\n"}8+ ],9+ "shellHistory": ["ls","open data.csv | first 3"],10+ "sheets": [11+ {12+ "name": "Sales",13+ "widths": {"A": 12, "E": 14},14+ "heights": {"1": 2, "12": 3},15+ "lines": {"A:XFD": {"italic":true}, "D:D": {"format":"currency","decimals":2}, "1:1": {"bold":true,"align":"center","borders":{"bottom":"double"}}},16+ "freeze": {"rows":1,"cols":1},17+ "filter": {"range":"A1:E9","columns":{"B":{"hidden":["West"]},"C":{"condition":"gt","value":"2"}}},18+ "protected": [{"range":"E2:E9","description":"Totals"}],19+ "merges": ["A11:C11","A12:A13"],20+ "cells": {21+ "A1": "Date",22+ "B1": "Region",23+ "C1": "Units",24+ "D1": "Price",25+ "E1": "Total",26+ "G1": "Share",27+ "H1": "Regions",28+ "I1": "Scaled",29+ "A2": {"input":"2026-01-05","format":"date"},30+ "B2": "North",31+ "C2": "3",32+ "D2": "12.5",33+ "E2": {"input":"=C2*D2","format":"currency","decimals":2,"own":true},34+ "G2": {"input":"=C2/SUM(Units)","format":"percent","decimals":1},35+ "H2": "=SORT(UNIQUE(B2:B9))",36+ "I2": "=LET(r, Rate, MAP(C2:C4, LAMBDA(x, x*r)))",37+ "A3": {"input":"2026-01-12","format":"date"},38+ "B3": {"input":"South","note":"Includes returns"},39+ "C3": "5",40+ "D3": "9.75",41+ "E3": "=C3*D3",42+ "G3": {"input":"=C3/SUM(Units)","format":"custom","pattern":"0.00%;[Red]-0.00%"},43+ "A4": {"input":"2026-02-02","format":"date"},44+ "B4": "West",45+ "C4": "1",46+ "D4": "40",47+ "E4": "=C4*D4",48+ "A5": {"input":"2026-02-16","format":"date"},49+ "B5": "North",50+ "C5": "7",51+ "D5": "12.5",52+ "E5": "=C5*D5",53+ "A6": {"input":"2026-03-01","format":"date"},54+ "B6": "East",55+ "C6": {"input":"2","bold":true,"italic":true,"underline":true,"strikethrough":true},56+ "D6": "22",57+ "E6": "=C6*D6",58+ "A7": {"input":"2026-03-09","format":"date"},59+ "B7": "South",60+ "C7": {"input":"4","align":"left","valign":"top"},61+ "D7": "9.75",62+ "E7": "=C7*D7",63+ "A8": {"input":"2026-03-20","format":"date"},64+ "B8": "East",65+ "C8": {"input":"6","borders":{"top":"thin","bottom":"double","left":"thick","right":"thin","bottomColor":"red","leftColor":"blue"}},66+ "D8": "22",67+ "E8": "=C8*D8",68+ "A9": {"input":"2026-03-31","format":"date"},69+ "B9": "West",70+ "C9": "8",71+ "D9": "40",72+ "E9": "=C9*D9",73+ "A11": {"input":"A long title merged across three columns","align":"center","wrap":"wrap"},74+ "A12": {"input":"Tall","valign":"middle"},75+ "B12": {"input":"clipped text that does not spill over","wrap":"clip"},76+ "C12": "TRUE",77+ "D12": "=IFERROR(1/0, \"none\")",78+ "E12": {"input":"=Lists!B1*2","format":"number","decimals":3},79+ "F12": "'00123",80+ "F13": "=HYPERLINK(\"https://example.com\", \"site\")"81+ },82+ "charts": [83+ {"type":"column","data":"A1:C9","at":"K2","width":40,"height":12,"header":true,"labels":true,"title":"Units by date"},84+ {"type":"line","data":"C1:E9","at":"K16","width":36,"height":10,"header":true,"stack":"stacked","trend":true,"min":0,"max":400,"gridlines":false,"legend":"none"},85+ {"type":"pie","data":"B1:C5","at":"K28","width":30,"height":10,"header":true,"labels":true,"legend":"right"},86+ {"type":"scatter","data":"C1:D9","at":"K40","width":30,"height":10,"header":true,"trend":true,"log":true},87+ {"type":"bar","data":"B1:C9","at":"K52","width":30,"height":10,"header":true,"stack":"percent"},88+ {"type":"area","data":"C1:D9","at":"K64","width":30,"height":10,"byRow":true,"header":true}89+ ],90+ "conditionalFormats": [91+ {"ranges":"C2:C9","condition":"gt","values":["5"],"text":"red","bold":true},92+ {"ranges":"B2:B9","condition":"contains","values":["th"],"fill":"yellow","italic":true},93+ {"ranges":"A2:A9","condition":"date_is","values":["this week"],"underline":true},94+ {"ranges":"E2:E9","condition":"formula","values":["=$C2\u003e4"],"text":"green","strikethrough":true},95+ {"ranges":"C2:C9","condition":"between","values":["2","4"],"fill":"cyan"},96+ {"ranges":"C2:C9","condition":"top","values":["3"],"fill":"magenta"},97+ {"ranges":"D2:D9","condition":"duplicate","text":"blue"},98+ {"ranges":"C2:C9","condition":"above_average","bold":true},99+ {"ranges":"D2:D9","scale":[{"type":"min","color":"green"},{"type":"percentile","value":"50","color":"yellow"},{"type":"max","color":"red"}]},100+ {"ranges":"E2:E9","dataBar":{"color":"blue","min":{"type":"min"},"max":{"type":"num","value":"400"},"barOnly":true}},101+ {"ranges":"C2:C9,D2:D9","iconSet":{"icons":"arrows","points":[{"type":"percent","value":"33"},{"type":"percent","value":"67"}],"reverse":true}}102+ ],103+ "validations": [104+ {"ranges":"B2:B9","criteria":"list","items":["North","South","East","West"],"help":"Pick a region","display":"chip"},105+ {"ranges":"C2:C9","criteria":"number","condition":"between","values":["0","80"],"reject":true},106+ {"ranges":"A2:A9","criteria":"date","condition":"gt","values":["2025-12-31"]},107+ {"ranges":"C12","criteria":"checkbox","items":["TRUE","FALSE"]},108+ {"ranges":"F2:F9","criteria":"range","source":"Lists!A1:A4","display":"plain"},109+ {"ranges":"G5:G9","criteria":"length","condition":"le","values":["10"]},110+ {"ranges":"H12","criteria":"formula","values":["=H12\u003c\u003e\"\""]}111+ ]112+ },113+ {114+ "name": "Lists",115+ "hidden": true,116+ "protected": [{"sheet":true}],117+ "cells": {118+ "A1": "North",119+ "B1": {"input":"1.5","note":"Rate used by Sales!I2"},120+ "A2": "South",121+ "A3": "East",122+ "A4": "West"123+ }124+ },125+ {126+ "name": "Pivot Table 1",127+ "cells": {},128+ "pivot": {"source":"Sales!A1:E9","rows":[{"column":"B"},{"column":"A","order":"desc","sortBy":1}],"columns":[{"column":"C"}],"values":[{"column":"E","summarize":"sum"},{"column":"C","summarize":"counta","showAs":"percent_of_total","name":"Share"}],"filters":[{"column":"B","hidden":["West"]}],"rowTotals":true,"columnTotals":false}129+ },130+ {131+ "name": "Notebook 1",132+ "widths": {"A": 20},133+ "cells": {},134+ "notebook": true,135+ "regions": [136+ {"name":"r1","command":"ls","at":"A1","rows":4,"cols":3},137+ {"name":"big","command":"$r1 | where size \u003e 1kb","at":"A8","rows":3,"cols":3,"reads":["r1"],"input":"Sales!A1:E9","sort":[{"column":3,"desc":true}]},138+ {"name":"app","at":"F2","path":"logs/app.csv","window":500},139+ {"name":"db","at":"F20","path":"data.sqlite","format":"SQLite","table":"orders"},140+ {"name":"q","at":"M20","path":"data.sqlite","format":"SQLite","query":"select * from orders limit 5"}141+ ]142+ }143+ ]144+}