- commit
- 3ab9a995a14d9395c18b3321e4437d74d7c23f0b
- parent
- ea777836507b88d74b5a9f13b8c477cc50fe8bbf
- author
- Dami <dami.etoile@gmail.com>
- kind
- feat risk score 1.5 of 3, Jev's estimate
| M | docs/ | 3 | ++- |
| M | internal/ | 5 | +++-- |
| M | internal/ | 5 | ++++- |
| M | internal/ | 18 | ++++++++++++++++++ |
| M | internal/ | 8 | +++++--- |
| M | internal/ | 29 | +++++++++++++++++++++++++++++ |
| M | internal/ | 2 | ++ |
| M | internal/ | 5 | +++-- |
| M | internal/ | 22 | ---------------------- |
| M | internal/ | 22 | ++++++++++++++++++++++ |
@@ -42,6 +42,7 @@ What it compares:
4242 | Cells | `input` (what was typed: a formula or an entry), `value` (what a formula computes, or an array spilled), `format` (the cell's formatting, by its fields in the file), `note` |4343 | Notebooks | Each [notebook](../nushell/notebooks.md) cell, lined up by its source: `added`, `removed`, or which field changed (`source`, `output`, `error`) |4444 | Regions | Each [linked file](following.md) or notebook output sent to a sheet, by name: added, removed, or which field changed |45+| Tables | Each [table](../sheets/tables.md), by name, the same way: `Q3 table Sales range A1:C9 → A1:C10` |4546 | Layout | Every other field of a sheet, by its name in [the file](format.md): `widths`, `heights` and `lines` key by key, the rest (`charts`, `conditionalFormats`, `freeze`) whole |4647 | Workbook | Named ranges, macros, the locale and decimal arithmetic |4748 @@ -105,7 +106,7 @@ on its own:
105106 106107 ```mermaid107108 flowchart TD108- part["A part: a cell's input, format or note; a sheet's name; a column's width; a region; a notebook's cells; a named range"]109+ part["A part: a cell's input, format or note; a sheet's name; a column's width; a region; a table; a notebook's cells; a named range"]109110 part --> same{"Did both sides change it?"}110111 same -- "One side, or neither" --> take["Take the side that changed it"]111112 same -- "Both, alike" --> take@@ -92,12 +92,13 @@ func formatText(raw json.RawMessage) string {
9292 return strings.Join(parts, ", ")9393 }9494 95-// sheetDiff compares a sheet's cells, regions and other fields; name is95+// sheetDiff compares a sheet's cells, regions, tables and other fields; name is9696 // what the changes call the sheet.9797 func sheetDiff(a, b side, name string) []Change {9898 out := cellChanges(a, b, name)9999 out = append(out, notebookChanges(name, field(a.raw, "notebookCells"), field(b.raw, "notebookCells"))...)100100 out = append(out, listChanges(KindRegion, name, field(a.raw, "regions"), field(b.raw, "regions"))...)101+ out = append(out, listChanges(KindTable, name, field(a.raw, "tables"), field(b.raw, "tables"))...)101102 keys := map[string]bool{}102103 for _, s := range []*rawSheet{a.raw, b.raw} {103104 for k := range s.fields {@@ -105,7 +106,7 @@ func sheetDiff(a, b side, name string) []Change {
105106 }106107 }107108 for _, k := range slices.Sorted(maps.Keys(keys)) {108- if k != "regions" && k != "notebookCells" {109+ if k != "regions" && k != "tables" && k != "notebookCells" {109110 out = append(out, fieldChanges(KindLayout, name, k, a.raw.fields[k], b.raw.fields[k])...)110111 }111112 }@@ -52,6 +52,7 @@ const (
5252 KindSheet = "sheet" // a sheet added, removed, renamed or moved5353 KindCell = "cell" // a cell's input, value, format or note5454 KindRegion = "region" // a notebook region or linked file55+ KindTable = "table" // a table5556 KindLayout = "layout" // another field of a sheet: widths, rules, charts5657 KindName = "name" // a named range5758 KindMacro = "macro" // a macro@@ -75,7 +76,7 @@ type Change struct {
7576 }7677 7778 // Where names the change's place as the text output does: Q3!B7,78-// sheet Q3, Q3 region r1, Q3 widths, name Sales.79+// sheet Q3, Q3 region r1, Q3 table Sales, Q3 widths, name Sales.7980 func (c Change) Where() string {8081 switch c.Kind {8182 case KindCell:@@ -84,6 +85,8 @@ func (c Change) Where() string {
8485 return "sheet " + c.Sheet8586 case KindRegion:8687 return c.Sheet + " region " + c.Item88+ case KindTable:89+ return c.Sheet + " table " + c.Item8790 case KindLayout, KindNotebook:8891 return c.Sheet + " " + c.Item8992 }@@ -143,6 +143,24 @@ workbook locale en-US → de-DE
143143 }144144 }145145 146+func TestCompareTables(t *testing.T) {147+ cells := []map[string]string{{"A1": "Item", "B1": "Cost", "A2": "Rent", "B2": "5"}}148+ a := file(t, []string{"S"}, cells, func(w *sheet.Workbook) {149+ w.Sheet(0).CreateTable("Costs", sheet.NewRect(addr("A1"), addr("B2")))150+ w.Sheet(0).CreateTable("Old", sheet.NewRect(addr("D1"), addr("D2")))151+ })152+ b := file(t, []string{"S"}, cells, func(w *sheet.Workbook) {153+ w.Sheet(0).CreateTable("Costs", sheet.NewRect(addr("A1"), addr("B3")))154+ })155+ want := `S!D1 input - Column1156+S table Costs range A1:B2 → A1:B3157+S table Old removed - D1:D2158+`159+ if got := lines(t, a, b); got != want {160+ t.Errorf("got\n%s\nwant\n%s", got, want)161+ }162+}163+146164 func TestCompareNotebooks(t *testing.T) {147165 nb := func(cells string) []byte {148166 return []byte(`{"version": 4, "sheets": [{"name": "Sheet1", "cells": {}}, {"name": "Notebook", "tab": "notebook", "notebookCells": [` + cells + `], "cells": {}}]}`)@@ -114,9 +114,9 @@ func named(raw json.RawMessage) map[string]json.RawMessage {
114114 return out115115 }116116 117-// summary is what the text output shows of a region or macro added or118-// removed: the file a region follows or that it's a notebook's output;119-// a macro's script's first line.117+// summary is what the text output shows of a region, table or macro118+// added or removed: the file a region follows or that it's a notebook's119+// output; a table's range; a macro's script's first line.120120 func summary(raw json.RawMessage) string {121121 fields, _ := object(raw)122122 var s string@@ -125,6 +125,8 @@ func summary(raw json.RawMessage) string {
125125 return s126126 case string(fields["output"]) == "true":127127 return "a notebook cell's output"128+ case json.Unmarshal(fields["range"], &s) == nil && s != "":129+ return s128130 case json.Unmarshal(fields["source"], &s) == nil && s != "":129131 return firstLine(s)130132 }@@ -38,6 +38,35 @@ func input(w *sheet.Workbook, ref string) string {
3838 return c.Input3939 }4040 41+// Tables merge one by one: one side resizing a table and the other42+// adding one keeps both.43+func TestMergeTables(t *testing.T) {44+ cells := []map[string]string{{"A1": "Item", "B1": "Cost", "A2": "Rent", "B2": "5", "D1": "Tag", "D2": "x"}}45+ withTables := func(ts map[string]string) func(w *sheet.Workbook) {46+ return func(w *sheet.Workbook) {47+ for name, r := range ts {48+ rr, _ := sheet.ParseRange(r)49+ if err := w.Sheet(0).CreateTable(name, rr); err != nil {50+ t.Fatal(err)51+ }52+ }53+ }54+ }55+ base := file(t, []string{"S"}, cells, withTables(map[string]string{"Costs": "A1:B2"}))56+ ours := file(t, []string{"S"}, cells, withTables(map[string]string{"Costs": "A1:B3"}))57+ theirs := file(t, []string{"S"}, cells, withTables(map[string]string{"Costs": "A1:B2", "Tags": "D1:D2"}))58+ w, said := merge(t, base, ours, theirs)59+ if len(said) > 0 {60+ t.Errorf("conflicts %v", said)61+ }62+ s := w.Sheet(0)63+ costs, _ := s.TableAt(addr("A1"))64+ tags, _ := s.TableAt(addr("D1"))65+ if costs.Range.String() != "A1:B3" || tags.Name != "Tags" {66+ t.Errorf("tables %+v %+v", costs, tags)67+ }68+}69+4170 func TestMergeCells(t *testing.T) {4271 base := file(t, []string{"Sheet1"}, []map[string]string{{"A1": "Item", "B1": "1", "B2": "2", "B3": "3", "B4": "=SUM(B1:B3)"}}, nil)4372 ours := file(t, []string{"Sheet1"}, []map[string]string{{"A1": "Item", "B1": "10", "B2": "2", "B3": "30", "B4": "=SUM(B1:B3)", "C1": "ours"}}, nil)@@ -150,6 +150,8 @@ func (m *merger) sheet(b, o, t *rawSheet) *rawSheet {
150150 switch k {151151 case "regions":152152 v = m.list(b.fields[k], o.fields[k], t.fields[k], func(key string) string { return out.name + " region " + key })153+ case "tables":154+ v = m.list(b.fields[k], o.fields[k], t.fields[k], func(key string) string { return out.name + " table " + key })153155 case "notebookCells":154156 v = m.notebook(out.name, b.fields[k], o.fields[k], t.fields[k])155157 case "widths", "heights", "lines":@@ -140,9 +140,10 @@ func Dump(w io.Writer, b *Book) error {
140140 }141141 continue142142 }143- if k == "regions" {143+ if k == "regions" || k == "tables" {144+ what := strings.TrimSuffix(k, "s")144145 for _, name := range slices.Sorted(maps.Keys(named(s.fields[k]))) {145- fmt.Fprintf(bw, "%s region %s %s\n", s.name, name, named(s.fields[k])[name])146+ fmt.Fprintf(bw, "%s %s %s %s\n", s.name, what, name, named(s.fields[k])[name])146147 }147148 continue148149 }@@ -238,28 +238,6 @@ func TestMonochromeNotebook(t *testing.T) {
238238 }239239 }240240 241-// A table's header row reads without color: bold and underlined, and242-// the context line names the table on its cells. Its bands are243-// decoration, with no state to read.244-func TestMonochromeTable(t *testing.T) {245- m := newModel()246- m.sheet.Set(addr("B1"), "Item")247- m.sheet.Set(addr("B2"), "Rent")248- if err := m.sheet.CreateTable("Costs", rectOf("B1:B3")); err != nil {249- t.Fatal(err)250- }251- if c := cellAt(m, addr("B1")); !every(c, true, func(c monoCell) bool { return c.bold && c.underline == "1" }) {252- t.Errorf("a table's header: %+v", c)253- }254- if c := cellAt(m, addr("B2")); !every(c, true, plain) {255- t.Errorf("a table's first row: %+v", c)256- }257- m.cur = addr("B2")258- if l := cellText(monoLine(m, contextLine)); !strings.Contains(l, "Table Costs, column Item") {259- t.Errorf("the table isn't named: %q", l)260- }261-}262-263241 func TestMonochromeError(t *testing.T) {264242 m := newModel()265243 m.sheet.Set(addr("B1"), "=1/0")@@ -199,6 +199,28 @@ run("table.banded")
199199 }200200 }201201 202+// A table's header row reads without color: bold and underlined, and203+// the context line names the table on its cells. Its bands are204+// decoration, with no state to read.205+func TestMonochromeTable(t *testing.T) {206+ m := newModel()207+ m.sheet.Set(addr("B1"), "Item")208+ m.sheet.Set(addr("B2"), "Rent")209+ if err := m.sheet.CreateTable("Costs", rectOf("B1:B3")); err != nil {210+ t.Fatal(err)211+ }212+ if c := cellAt(m, addr("B1")); !every(c, true, func(c monoCell) bool { return c.bold && c.underline == "1" }) {213+ t.Errorf("a table's header: %+v", c)214+ }215+ if c := cellAt(m, addr("B2")); !every(c, true, plain) {216+ t.Errorf("a table's first row: %+v", c)217+ }218+ m.cur = addr("B2")219+ if l := cellText(monoLine(m, contextLine)); !strings.Contains(l, "Table Costs, column Item") {220+ t.Errorf("the table isn't named: %q", l)221+ }222+}223+202224 // wantShown checks the value the cell at a shows.203225 func wantShown(t *testing.T, m *Model, a, want string) {204226 t.Helper()