cat / 012
clone

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

Put each sheet's cells behind a cellStore

Dami committed 6b40936
Cells were a bare map used directly across the engine. A small store
type with get, set, delete, len, all and inRange now owns it, and no
code outside store.go touches the map, so the representation can
change later without the rest of the engine noticing. inRange picks
between probing each address and scanning the store, the choice
RangeStats made on its own; cellsIn and the pushed-off check for
inserts use it, so they no longer scan every cell for a small range.
The representation is unchanged.
commit
6b40936f4c473559e9214ad4892c0ba134cd37c4
parent
cbb6aa77a43924ec885a6f8eb53f2d599acc8b1e
author
Dami <dami.etoile@gmail.com>
21 files changed, +135 -89
Minternal/sheet/cellformat.go4+-
Minternal/sheet/chart.go2+-
Minternal/sheet/explain.go2+-
Minternal/sheet/file.go6+-
Minternal/sheet/filter.go2+-
Minternal/sheet/find.go4+-
Minternal/sheet/functions_link.go2+-
Minternal/sheet/history.go6+-
Minternal/sheet/history_test.go2+-
Minternal/sheet/names.go2+-
Minternal/sheet/nav.go27+++----------
Minternal/sheet/observe.go2+-
Minternal/sheet/ops.go40++++++++++----------
Minternal/sheet/recalc.go10++--
Minternal/sheet/remote.go2+-
Minternal/sheet/series.go4+-
Minternal/sheet/sheet.go28++++++-------
Minternal/sheet/sort.go4+-
Ainternal/sheet/store.go61++++++++++++++++++++++++++++++
Minternal/sheet/trace.go4+-
Minternal/sheet/workbook.go10++--
M internal/sheet/cellformat.go +2 -2
M internal/sheet/chart.go +1 -1
M internal/sheet/explain.go +1 -1
M internal/sheet/file.go +3 -3
M internal/sheet/filter.go +1 -1
M internal/sheet/find.go +2 -2
M internal/sheet/functions_link.go +1 -1
M internal/sheet/history.go +3 -3
M internal/sheet/history_test.go +1 -1
M internal/sheet/names.go +1 -1
M internal/sheet/nav.go +7 -20
@@ -5,7 +5,7 @@ package sheet
55 func (s *Sheet) UsedRange() (Rect, bool) {66 var last Addr77 found := false8- for a, c := range s.cells {8+ for a, c := range s.cells.all() {99 if c.Blank() {1010 continue1111 }
@@ -22,7 +22,7 @@ func (s *Sheet) UsedRange() (Rect, bool) {
2222 // block, it goes to the next filled cell. With nothing filled ahead it2323 // stops at the edge of the worksheet. Rows the filter hides are skipped.2424 func (s *Sheet) Edge(a Addr, dc, dr int) Addr {25- filled := func(a Addr) bool { return !s.cells[a].Blank() }25+ filled := func(a Addr) bool { return !s.cells.get(a).Blank() }2626 step := func(a Addr) Addr {2727 n := Addr{Col: a.Col + dc, Row: a.Row + dr}2828 for dr != 0 && n.Valid() && s.RowHidden(n.Row) {
@@ -72,10 +72,8 @@ type statsCache struct {
7272 ok bool7373 }7474 75-// RangeStats computes Stats over r, visiting whichever is cheaper: the76-// cells in r one lookup at a time, or every stored cell (iterating the77-// map costs about a quarter of a lookup per cell). The result is kept78-// until a cell or value changes.75+// RangeStats computes Stats over r. The result is kept until a cell or76+// value changes.7977 func (s *Sheet) RangeStats(r Rect) Stats {8078 if c := s.stats; c.ok && c.r == r && c.version == s.version {8179 return c.st
@@ -88,20 +86,9 @@ func (s *Sheet) RangeStats(r Rect) Stats {
8886 st.Sum += c.Value.Num8987 }9088 }91- area := (r.To.Col - r.From.Col + 1) * (r.To.Row - r.From.Row + 1)92- if area <= len(s.cells)/4 {93- for row := r.From.Row; row <= r.To.Row; row++ {94- for col := r.From.Col; col <= r.To.Col; col++ {95- if c := s.cells[Addr{Col: col, Row: row}]; !c.Blank() {96- add(c)97- }98- }99- }100- } else {101- for a, c := range s.cells {102- if r.Contains(a) && !c.Blank() {103- add(c)104- }89+ for _, c := range s.cells.inRange(r) {90+ if !c.Blank() {91+ add(c)10592 }10693 }10794 s.stats = statsCache{r: r, version: s.version, st: st, ok: true}
M internal/sheet/observe.go +1 -1
M internal/sheet/ops.go +20 -20
@@ -25,8 +25,8 @@ const maxFill = 1 << 20
2525 // stay as they are.2626 func (s *Sheet) remap(label string, focus Rect, cell func(Addr) (Addr, bool), rng func(Rect) (Rect, bool)) {2727 rw := formula.Relocate(s.onThis(s), cell, rng)28- next := make(map[Addr]*Cell, len(s.cells))29- for a, c := range s.cells {28+ next := make(map[Addr]*Cell, s.cells.len())29+ for a, c := range s.cells.all() {3030 if to, ok := cell(a); ok {3131 next[to] = c.rewritten(rw)3232 }
@@ -36,7 +36,7 @@ func (s *Sheet) remap(label string, focus Rect, cell func(Addr) (Addr, bool), rn
3636 if l.s == s {3737 continue3838 }39- c := l.s.cells[l.a]39+ c := l.s.cells.get(l.a)4040 if nc := c.rewritten(formula.Relocate(s.onThis(l.s), cell, rng)); nc != c {4141 others[l] = nc4242 }
@@ -45,13 +45,13 @@ func (s *Sheet) remap(label string, focus Rect, cell func(Addr) (Addr, bool), rn
4545 for l, c := range others {4646 l.s.place(l.a, c)4747 }48- for a, c := range s.cells {48+ for a, c := range s.cells.all() {4949 if next[a] != c {5050 s.place(a, next[a])5151 }5252 }5353 for a, c := range next {54- if s.cells[a] != c {54+ if s.cells.get(a) != c {5555 s.place(a, c)5656 }5757 }
@@ -85,14 +85,14 @@ func (s *Sheet) insert(rows bool, at, n int) error {
8585 if rows {8686 size = MaxRows8787 }88- for a := range s.cells {89- line := a.Col90- if rows {91- line = a.Row92- }93- if line >= at && line >= size-n {94- return ErrPushedOff95- }88+ // The lines pushed past the edge must be empty.89+ first := max(at, size-n)90+ edge := colRect(first, size-1)91+ if rows {92+ edge = rowRect(first, size-1)93+ }94+ for range s.cells.inRange(edge) {95+ return ErrPushedOff9696 }9797 s.restructure(rows, formula.Span{At: at, N: n, Size: size})9898 return nil
@@ -201,20 +201,20 @@ func (s *Sheet) MoveTo(dst *Sheet, src Rect, to Addr) (Rect, error) {
201201 202202 moved := map[Addr]*Cell{}203203 for _, a := range s.cellsIn(src) {204- moved[shift(a)] = s.cells[a].rewritten(rw(s, dst))204+ moved[shift(a)] = s.cells.get(a).rewritten(rw(s, dst))205205 }206206 rest := map[loc]*Cell{}207207 keep := func(l loc) {208208 if src.Contains(l.a) && l.s == s || d.Contains(l.a) && l.s == dst {209209 return210210 }211- c := l.s.cells[l.a]211+ c := l.s.cells.get(l.a)212212 if nc := c.rewritten(rw(l.s, l.s)); nc != c {213213 rest[l] = nc214214 }215215 }216216 for _, t := range []*Sheet{s, dst} {217- for a := range t.cells {217+ for a := range t.cells.all() {218218 keep(loc{t, a})219219 }220220 }
@@ -299,7 +299,7 @@ type Clip struct {
299299 func (s *Sheet) Copy(r Rect) *Clip {300300 c := &Clip{Src: r, cells: map[Addr]*Cell{}}301301 for _, a := range s.cellsIn(r) {302- c.cells[Addr{Col: a.Col - r.From.Col, Row: a.Row - r.From.Row}] = s.cells[a].clone()302+ c.cells[Addr{Col: a.Col - r.From.Col, Row: a.Row - r.From.Row}] = s.cells.get(a).clone()303303 }304304 return c305305 }
@@ -363,7 +363,7 @@ func (s *Sheet) Paste(c *Clip, dst Rect, values bool) (Rect, error) {
363363 func (s *Sheet) pasteCell(a Addr, c *Cell, dc, dr int, values bool) {364364 switch {365365 case c == nil:366- if s.cells[a] != nil {366+ if s.cells.get(a) != nil {367367 s.place(a, nil)368368 }369369 case values:
@@ -445,7 +445,7 @@ func (s *Sheet) FillEntry(r Rect, origin Addr, input string) error {
445445 if err := s.put(origin, input); err != nil {446446 return err447447 }448- c := s.cells[origin].clone()448+ c := s.cells.get(origin).clone()449449 for row := r.From.Row; row <= r.To.Row; row++ {450450 for col := r.From.Col; col <= r.To.Col; col++ {451451 if a := (Addr{Col: col, Row: row}); a != origin {
@@ -469,7 +469,7 @@ func (s *Sheet) seriesStart(r Rect, down bool) (Rect, bool) {
469469 }470470 used := map[int]bool{}471471 for _, a := range s.cellsIn(r) {472- if !s.cells[a].Blank() {472+ if !s.cells.get(a).Blank() {473473 used[line(a)] = true474474 }475475 }
M internal/sheet/recalc.go +5 -5
M internal/sheet/remote.go +1 -1
M internal/sheet/series.go +2 -2
M internal/sheet/sheet.go +13 -15
@@ -54,7 +54,7 @@ type Sheet struct {
5454 name string5555 live bool // in the workbook's list; false once deleted5656 57- cells map[Addr]*Cell57+ cells cellStore // see store.go5858 widths map[int]int5959 6060 // dependents maps a cell to the formula cells that reference it
@@ -90,11 +90,11 @@ func New() *Sheet {
9090 9191 // Cell returns the cell at a, or nil if it has neither contents nor9292 // formatting. Use Blank to test for contents.93-func (s *Sheet) Cell(a Addr) *Cell { return s.cells[a] }93+func (s *Sheet) Cell(a Addr) *Cell { return s.cells.get(a) }9494 9595 // Value returns the computed value at a.9696 func (s *Sheet) Value(a Addr) Value {97- if c := s.cells[a]; c != nil {97+ if c := s.cells.get(a); c != nil {9898 return c.Value9999 }100100 return Value{}
@@ -103,7 +103,7 @@ func (s *Sheet) Value(a Addr) Value {
103103 // Len returns the number of non-blank cells.104104 func (s *Sheet) Len() int {105105 n := 0106- for _, c := range s.cells {106+ for _, c := range s.cells.all() {107107 if !c.Blank() {108108 n++109109 }
@@ -113,8 +113,8 @@ func (s *Sheet) Len() int {
113113 114114 // Addrs returns every non-blank cell in row-major order.115115 func (s *Sheet) Addrs() []Addr {116- out := make([]Addr, 0, len(s.cells))117- for a, c := range s.cells {116+ out := make([]Addr, 0, s.cells.len())117+ for a, c := range s.cells.all() {118118 if !c.Blank() {119119 out = append(out, a)120120 }
@@ -214,7 +214,7 @@ func (s *Sheet) Set(a Addr, input string) error {
214214 func (s *Sheet) put(a Addr, input string) error {215215 var f Format216216 var st Style217- if old := s.cells[a]; old != nil {217+ if old := s.cells.get(a); old != nil {218218 f, st = old.Format, old.Style219219 }220220 c, err := newCell(input, f, st, true)
@@ -299,7 +299,7 @@ func (s *Sheet) place(a Addr, c *Cell) {
299299 if c == nil {300300 return301301 }302- s.cells[a] = c302+ s.cells.set(a, c)303303 for _, r := range c.refs {304304 if s.dependents[r] == nil {305305 s.dependents[r] = make(map[Addr]struct{})
@@ -320,7 +320,7 @@ func (s *Sheet) place(a Addr, c *Cell) {
320320 func (s *Sheet) EraseRange(r Rect) {321321 s.change("clear "+r.String(), r, func() {322322 for _, a := range s.cellsIn(r) {323- if c := s.cells[a]; !c.Blank() {323+ if c := s.cells.get(a); !c.Blank() {324324 s.place(a, formattingOnly(c.Format, c.Style))325325 }326326 }
@@ -330,17 +330,15 @@ func (s *Sheet) EraseRange(r Rect) {
330330 // cellsIn returns the cells in r that have contents or formatting.331331 func (s *Sheet) cellsIn(r Rect) []Addr {332332 var out []Addr333- for a := range s.cells {334- if r.Contains(a) {335- out = append(out, a)336- }333+ for a := range s.cells.inRange(r) {334+ out = append(out, a)337335 }338336 return out339337 }340338 341339 // unlink removes the cell at a and its dependency edges.342340 func (s *Sheet) unlink(a Addr) {343- old := s.cells[a]341+ old := s.cells.get(a)344342 if old == nil {345343 return346344 }
@@ -355,5 +353,5 @@ func (s *Sheet) unlink(a Addr) {
355353 }356354 s.rangeUsers.remove(a, old.ranges)357355 delete(s.volatile, a)358- delete(s.cells, a)356+ s.cells.delete(a)359357 }
M internal/sheet/sort.go +2 -2
A internal/sheet/store.go +61 -0
@@ -0,0 +1,61 @@
1+package sheet2+3+import (4+ "iter"5+ "maps"6+)7+8+// cellStore holds a sheet's cells: those with contents or formatting, by9+// address. The rest of the engine reads and writes cells only through10+// these methods, never the map beneath, so the representation can change11+// (to column blocks of compact values, with formulas and formats in side12+// tables; see docs/limits.md) without touching it.13+//14+// Iteration order is unspecified. Cells may be set or deleted while15+// iterating, with a map's rules: a deleted cell not yet reached isn't16+// yielded, a new one may or may not be.17+type cellStore struct {18+ m map[Addr]*Cell19+}20+21+func newCellStore() cellStore { return cellStore{m: make(map[Addr]*Cell)} }22+23+// get returns the cell at a, or nil.24+func (st *cellStore) get(a Addr) *Cell { return st.m[a] }25+26+// set stores c, which isn't nil, at a.27+func (st *cellStore) set(a Addr, c *Cell) { st.m[a] = c }28+29+// delete removes the cell at a, if any.30+func (st *cellStore) delete(a Addr) { delete(st.m, a) }31+32+// len is the number of cells stored, formatting-only ones included.33+func (st *cellStore) len() int { return len(st.m) }34+35+// all yields every stored cell.36+func (st *cellStore) all() iter.Seq2[Addr, *Cell] { return maps.All(st.m) }37+38+// inRange yields the stored cells in r, visiting whichever is cheaper:39+// each address of r (a lookup each), or every stored cell (iterating the40+// map costs about a quarter of a lookup per cell).41+func (st *cellStore) inRange(r Rect) iter.Seq2[Addr, *Cell] {42+ return func(yield func(Addr, *Cell) bool) {43+ area := (r.To.Col - r.From.Col + 1) * (r.To.Row - r.From.Row + 1)44+ if area <= len(st.m)/4 {45+ for row := r.From.Row; row <= r.To.Row; row++ {46+ for col := r.From.Col; col <= r.To.Col; col++ {47+ a := Addr{Col: col, Row: row}48+ if c := st.m[a]; c != nil && !yield(a, c) {49+ return50+ }51+ }52+ }53+ return54+ }55+ for a, c := range st.m {56+ if r.Contains(a) && !yield(a, c) {57+ return58+ }59+ }60+ }61+}
M internal/sheet/trace.go +2 -2
M internal/sheet/workbook.go +5 -5
@@ -79,7 +79,7 @@ func (w *Workbook) newSheet(name string) *Sheet {
7979 return &Sheet{8080 wb: w,8181 name: name,82- cells: make(map[Addr]*Cell),82+ cells: newCellStore(),8383 widths: make(map[int]int),8484 dependents: make(map[Addr]map[Addr]struct{}),8585 volatile: make(map[Addr]struct{}),
@@ -207,7 +207,7 @@ func (w *Workbook) DuplicateSheet(s *Sheet) (*Sheet, error) {
207207 return nil, errors.New("That sheet was deleted")208208 }209209 cp := w.newSheet(w.freeName("Copy of " + s.name))210- for a, c := range s.cells {210+ for a, c := range s.cells.all() {211211 c := c.clone()212212 c.setExpr(c.expr) // fresh reference slices, shared with nothing213213 cp.place(a, c)
@@ -285,7 +285,7 @@ func (w *Workbook) renameRefs(old, name string) {
285285 },286286 }287287 for _, l := range w.crossList() {288- if c := l.s.cells[l.a]; c.readsSheet(old) {288+ if c := l.s.cells.get(l.a); c.readsSheet(old) {289289 l.s.place(l.a, c.rewritten(rw))290290 }291291 }
@@ -328,7 +328,7 @@ func (w *Workbook) attach(s *Sheet) {
328328 }329329 s.live = true330330 w.byKey[formula.SheetKey(s.name)] = s331- for a, c := range s.cells {331+ for a, c := range s.cells.all() {332332 w.index(loc{s, a}, c)333333 }334334 w.structural = true
@@ -344,7 +344,7 @@ func (w *Workbook) detach(s *Sheet) {
344344 if w.byKey[formula.SheetKey(s.name)] == s {345345 delete(w.byKey, formula.SheetKey(s.name))346346 }347- for a, c := range s.cells {347+ for a, c := range s.cells.all() {348348 w.unindex(loc{s, a}, c)349349 }350350 w.structural = true