cat / 012
clone

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

Add named ranges and tracing to the engine

Dami committed 617fae6
commit
617fae6651b524f3bc85b160c160f9b27c0dcf31
parent
e31e05c4c678d894e728340d937c982bca4692c8
author
Dami <dami.etoile@gmail.com>
12 files changed, +691 -34
Minternal/sheet/file.go45+++++-
Minternal/sheet/format_test.go2+-
Minternal/sheet/history.go45++++-
Ainternal/sheet/names.go231+++++++++++++++++++++++++++++
Ainternal/sheet/names_test.go241++++++++++++++++++++++++++++++
Minternal/sheet/ops.go7+-
Minternal/sheet/parse.go25+++-
Minternal/sheet/refs.go23++-
Minternal/sheet/refs_test.go2+-
Minternal/sheet/remote.go2+-
Minternal/sheet/sheet.go48+++++-
Ainternal/sheet/trace.go54+++++++
M internal/sheet/file.go +41 -4
@@ -12,9 +12,11 @@ import (
1212 // FileExt is the extension of the native worksheet format.1313 const FileExt = ".012"1414 15-// fileVersion is the version Write produces. Version 1 files (cells as16-// plain strings, no formatting) still load.17-const fileVersion = 215+// fileVersion is the newest version Write produces and Read accepts.16+// Version 1 files (cells as plain strings, no formatting) still load.17+// Version 3 adds named ranges; a sheet without them is written as version18+// 2, so earlier builds can still open it.19+const fileVersion = 31820 1921 // The file is JSON with one entry per cell, keyed by address. A cell2022 // without formatting is just its input, as in version 1; a formatted cell
@@ -22,9 +24,13 @@ const fileVersion = 2
2224 //2325 // "B2": "Rent",2426 // "C2": {"input": "1450", "format": "currency", "decimals": 2, "bold": true}27+//28+// Named ranges map each name to its range, or to "#REF!" once its cells29+// were deleted: "names": {"Sales": "B2:B20"}.2530 type fileFormat struct {2631 Version int `json:"version"`2732 Widths map[string]int `json:"widths,omitempty"`33+ Names map[string]string `json:"names,omitempty"`2834 Cells map[string]json.RawMessage `json:"cells"`2935 }3036
@@ -96,7 +102,11 @@ func decodeCell(raw json.RawMessage) (string, Format, Style, error) {
96102 // read naturally.97103 func (s *Sheet) Write(w io.Writer) error {98104 var b bytes.Buffer99- fmt.Fprintf(&b, "{\n \"version\": %d,\n", fileVersion)105+ version := 2106+ if len(s.names) > 0 {107+ version = 3108+ }109+ fmt.Fprintf(&b, "{\n \"version\": %d,\n", version)100110 if len(s.widths) > 0 {101111 b.WriteString(` "widths": {`)102112 for i, c := range slices.Sorted(maps.Keys(s.widths)) {
@@ -107,6 +117,16 @@ func (s *Sheet) Write(w io.Writer) error {
107117 }108118 b.WriteString("},\n")109119 }120+ if names := s.Names(); len(names) > 0 {121+ b.WriteString(` "names": {`)122+ for i, n := range names {123+ if i > 0 {124+ b.WriteString(", ")125+ }126+ fmt.Fprintf(&b, "%q: %q", n.Name, n.Ref())127+ }128+ b.WriteString("},\n")129+ }110130 addrs := make([]Addr, 0, len(s.cells))111131 for a := range s.cells {112132 addrs = append(addrs, a)
@@ -148,6 +168,23 @@ func Read(r io.Reader) (*Sheet, error) {
148168 }149169 s.SetColWidth(c, width)150170 }171+ for name, ref := range f.Names {172+ if err := ValidName(name); err != nil {173+ return nil, fmt.Errorf("name %q: %w", name, err)174+ }175+ if _, dup := s.LookupName(name); dup {176+ return nil, fmt.Errorf("name %q is defined twice", name)177+ }178+ n := Name{Name: name, Lost: ref == "#REF!"}179+ if !n.Lost {180+ r, ok := ParseRange(ref)181+ if !ok {182+ return nil, fmt.Errorf("name %q: invalid range %q", name, ref)183+ }184+ n.Range = r185+ }186+ s.putName(nameKey(name), &n)187+ }151188 for name, raw := range f.Cells {152189 a, ok := ParseAddr(name)153190 if !ok {
M internal/sheet/format_test.go +1 -1
M internal/sheet/history.go +36 -9
@@ -26,13 +26,18 @@ type history struct {
2626 2727 type step struct {2828 id int29- label string // what the step did, e.g. "clear B3:B5"30- focus Rect // what the UI selects when the step is undone or redone31- cells map[Addr]*Cell // before the step; nil for blank32- widths map[int]int // before the step; 0 for the default width29+ label string // what the step did, e.g. "clear B3:B5"30+ focus Rect // what the UI selects when the step is undone or redone31+ cells map[Addr]*Cell // before the step; nil for blank32+ widths map[int]int // before the step; 0 for the default width33+ names map[string]*Name // before the step, by key; nil for undefined3334 }3435 35-func (st *step) empty() bool { return len(st.cells) == 0 && len(st.widths) == 0 }36+func newStep(label string, focus Rect) *step {37+ return &step{label: label, focus: focus, cells: map[Addr]*Cell{}, widths: map[int]int{}, names: map[string]*Name{}}38+}39+40+func (st *step) empty() bool { return len(st.cells) == 0 && len(st.widths) == 0 && len(st.names) == 0 }3641 3742 func (c *Cell) clone() *Cell {3843 if c == nil {
@@ -63,7 +68,7 @@ func (s *Sheet) Batch(c Change, fn func() error) error {
6368 func (s *Sheet) change(label string, focus Rect, fn func()) {6469 h := &s.hist6570 if h.depth == 0 {66- h.open = &step{label: label, focus: focus, cells: map[Addr]*Cell{}, widths: map[int]int{}}71+ h.open = newStep(label, focus)6772 }6873 h.depth++6974 defer func() {
@@ -103,6 +108,18 @@ func (s *Sheet) recordWidth(c int) {
103108 }104109 }105110 111+// recordName saves the named range with key k before its first change in112+// the open step.113+func (s *Sheet) recordName(k string) {114+ st := s.hist.open115+ if st == nil {116+ return117+ }118+ if _, seen := st.names[k]; !seen {119+ st.names[k] = s.namePtr(k)120+ }121+}122+106123 // push adds a finished step to the undo stack and clears redo, dropping107124 // no-op changes.108125 func (s *Sheet) push(st *step) {
@@ -117,13 +134,18 @@ func (s *Sheet) push(st *step) {
117134 delete(st.cells, a)118135 }119136 }137+ for k, n := range st.names {138+ if cur := s.namePtr(k); n == nil && cur == nil || n != nil && cur != nil && *n == *cur {139+ delete(st.names, k)140+ }141+ }120142 if st.empty() {121143 return122144 }123145 h.redo = nil124146 h.lastID++125- widthOnly := len(st.cells) == 0126- if top := h.top(); widthOnly && h.mergeWidths && top != nil && len(top.cells) == 0 {147+ widthOnly := len(st.cells) == 0 && len(st.names) == 0148+ if top := h.top(); widthOnly && h.mergeWidths && top != nil && len(top.cells) == 0 && len(top.names) == 0 {127149 for c, w := range st.widths {128150 if _, ok := top.widths[c]; !ok {129151 top.widths[c] = w
@@ -193,7 +215,8 @@ func (s *Sheet) swap(from, to *[]*step) (Change, bool) {
193215 *from = (*from)[:len(*from)-1]194216 // The inverse keeps the step's ID: on the redo stack, an ID names the195217 // state the step leads back to.196- inv := &step{id: st.id, label: st.label, focus: st.focus, cells: make(map[Addr]*Cell, len(st.cells)), widths: map[int]int{}}218+ inv := newStep(st.label, st.focus)219+ inv.id = st.id197220 changed := make([]Addr, 0, len(st.cells))198221 for a, c := range st.cells {199222 inv.cells[a] = s.cells[a].clone()
@@ -204,6 +227,10 @@ func (s *Sheet) swap(from, to *[]*step) (Change, bool) {
204227 inv.widths[c] = s.widths[c]205228 s.setWidth(c, w)206229 }230+ for k, n := range st.names {231+ inv.names[k] = s.namePtr(k)232+ changed = append(changed, s.putName(k, n)...)233+ }207234 s.recalc(changed)208235 *to = append(*to, inv)209236 s.hist.mergeWidths = false
A internal/sheet/names.go +231 -0
@@ -0,0 +1,231 @@
1+package sheet2+3+import (4+ "cmp"5+ "errors"6+ "fmt"7+ "maps"8+ "slices"9+ "strings"10+)11+12+// Named ranges, as in Sheets' Data > Named ranges: a name such as Sales13+// stands for a range in formulas, =SUM(Sales). Formulas keep the name and14+// resolve it when they're evaluated, so redefining or deleting a name15+// recalculates every formula that uses it. Inserting, deleting and moving16+// cells moves a name's range like any other range reference.17+18+// Name is a named range.19+type Name struct {20+ Name string // as the user spelled it; formulas match it in any case21+ Range Rect22+ // Lost is set when every cell of the range was deleted: formulas using23+ // the name show #REF!, as in Sheets. Range is then zero.24+ Lost bool25+}26+27+// Ref is what the name stands for, e.g. "B2:B20", or "#REF!" when lost.28+func (n Name) Ref() string {29+ if n.Lost {30+ return "#REF!"31+ }32+ return n.Range.String()33+}34+35+// maxNameLen is Sheets' limit on the length of a name.36+const maxNameLen = 25037+38+func nameKey(name string) string { return strings.ToUpper(name) }39+40+// ValidName checks that name can be a named range, following Sheets'41+// rules: letters, digits, _ and ., starting with a letter or _, and not42+// something a formula would read as a cell or a boolean.43+func ValidName(name string) error {44+ switch {45+ case name == "":46+ return errors.New("Enter a name")47+ case len(name) > maxNameLen:48+ return fmt.Errorf("A name can be at most %d characters", maxNameLen)49+ case !isLetter(name[0]) && name[0] != '_':50+ return errors.New("A name must start with a letter or _")51+ }52+ for i := range len(name) {53+ if c := name[i]; !isLetter(c) && !isDigit(c) && c != '_' && c != '.' {54+ return errors.New("A name can only have letters, digits, _ and .")55+ }56+ }57+ switch k := nameKey(name); {58+ case k == "TRUE" || k == "FALSE":59+ return errors.New("TRUE and FALSE can't be names")60+ case looksLikeRef(k):61+ return errors.New("A name can't look like a cell reference, e.g. A1 or R1C1")62+ }63+ return nil64+}65+66+// looksLikeRef reports whether an upper-case name reads as a cell in A1 or67+// R1C1 style, even beyond this sheet's edges, so names stay unambiguous in68+// other spreadsheets too.69+func looksLikeRef(k string) bool {70+ letters := strings.TrimLeft(k, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")71+ if letters != k && letters != "" && strings.Trim(letters, "0123456789") == "" {72+ return true73+ }74+ rest, ok := strings.CutPrefix(k, "R")75+ if !ok {76+ rest, ok = k, strings.HasPrefix(k, "C")77+ }78+ if !ok {79+ return false80+ }81+ rest = strings.TrimLeft(rest, "0123456789")82+ rest, _ = strings.CutPrefix(rest, "C")83+ return strings.Trim(rest, "0123456789") == ""84+}85+86+// Names returns the named ranges, sorted by name.87+func (s *Sheet) Names() []Name {88+ out := slices.Collect(maps.Values(s.names))89+ slices.SortFunc(out, func(a, b Name) int { return cmp.Compare(nameKey(a.Name), nameKey(b.Name)) })90+ return out91+}92+93+// LookupName finds a named range, ignoring case.94+func (s *Sheet) LookupName(name string) (Name, bool) {95+ n, ok := s.names[nameKey(name)]96+ return n, ok97+}98+99+// NameUsers returns how many formulas mention the name.100+func (s *Sheet) NameUsers(name string) int { return len(s.nameUsers[nameKey(name)]) }101+102+// DefineName names the range r, as one undo step.103+func (s *Sheet) DefineName(name string, r Rect) error {104+ if err := ValidName(name); err != nil {105+ return err106+ }107+ if old, ok := s.LookupName(name); ok {108+ return fmt.Errorf("%s already names %s", old.Name, old.Ref())109+ }110+ s.change("name "+r.String()+" "+name, r, func() { s.putName(nameKey(name), &Name{Name: name, Range: r}) })111+ return nil112+}113+114+// EditName renames the named range old and points it at r, as one undo115+// step. Formulas that use it are rewritten to the new name, as in Sheets.116+func (s *Sheet) EditName(old, name string, r Rect) error {117+ cur, ok := s.LookupName(old)118+ if !ok {119+ return fmt.Errorf("There's no range named %s", old)120+ }121+ if err := ValidName(name); err != nil {122+ return err123+ }124+ if other, ok := s.LookupName(name); ok && nameKey(name) != nameKey(old) {125+ return fmt.Errorf("%s already names %s", other.Name, other.Ref())126+ }127+ s.change("edit "+name, r, func() {128+ from, to := nameKey(cur.Name), nameKey(name)129+ if cur.Name != name {130+ s.renameInFormulas(from, name)131+ }132+ if from != to {133+ s.putName(from, nil)134+ }135+ s.putName(to, &Name{Name: name, Range: r})136+ })137+ return nil138+}139+140+// DeleteName removes a named range, as one undo step. Formulas that use141+// it show #NAME? until it is defined again.142+func (s *Sheet) DeleteName(name string) error {143+ cur, ok := s.LookupName(name)144+ if !ok {145+ return fmt.Errorf("There's no range named %s", name)146+ }147+ s.change("delete "+cur.Name, cur.Range, func() { s.putName(nameKey(name), nil) })148+ return nil149+}150+151+// renameInFormulas rewrites the formulas that use the name with key from152+// to spell it to instead.153+func (s *Sheet) renameInFormulas(from, to string) {154+ rw := refRewrite{name: func(n nameNode) Node {155+ if nameKey(n.name) == from {156+ return nameNode{to}157+ }158+ return n159+ }}160+ for _, a := range slices.Collect(maps.Keys(s.nameUsers[from])) {161+ s.place(a, s.cells[a].rewritten(rw))162+ }163+}164+165+// namePtr returns a copy of the named range with key k, or nil.166+func (s *Sheet) namePtr(k string) *Name {167+ if n, ok := s.names[k]; ok {168+ return &n169+ }170+ return nil171+}172+173+// putName stores (or with nil removes) the named range with key k,174+// recording it for undo, and returns the formulas that use it, which need175+// recalculating. Every change to names goes through here.176+func (s *Sheet) putName(k string, n *Name) []Addr {177+ s.recordName(k)178+ if n == nil {179+ delete(s.names, k)180+ } else {181+ s.names[k] = *n182+ }183+ users := slices.Collect(maps.Keys(s.nameUsers[k]))184+ if s.hist.open != nil {185+ s.hist.dirty = append(s.hist.dirty, users...)186+ }187+ return users188+}189+190+// remapNames moves named ranges along with the cells they cover, when191+// rows or columns are inserted or deleted or cells are moved. A range192+// whose cells are all gone is lost.193+func (s *Sheet) remapNames(rng func(Rect) (Rect, bool)) {194+ for k, n := range s.names {195+ if n.Lost {196+ continue197+ }198+ next := n199+ if r, ok := rng(n.Range); ok {200+ next.Range = r201+ } else {202+ next.Range, next.Lost = Rect{}, true203+ }204+ if next != n {205+ s.putName(k, &next)206+ }207+ }208+}209+210+// bound returns the cell's formula with its names replaced by the ranges211+// they stand for: what is evaluated and what dependencies are traced212+// through. Undefined names stay, and evaluate to #NAME?.213+func (s *Sheet) bound(c *Cell) Node {214+ if len(c.names) == 0 {215+ return c.expr216+ }217+ const fixed = absCol | absRow218+ n, _ := rewrite(c.expr, refRewrite{name: func(nn nameNode) Node {219+ nm, ok := s.names[nameKey(nn.name)]220+ switch {221+ case !ok:222+ return nn223+ case nm.Lost:224+ return refErrNode{}225+ case nm.Range.From == nm.Range.To:226+ return refNode{nm.Range.From, fixed}227+ }228+ return rangeNode{nm.Range, [2]absFlags{fixed, fixed}}229+ }})230+ return n231+}
A internal/sheet/names_test.go +241 -0
@@ -0,0 +1,241 @@
1+package sheet2+3+import (4+ "reflect"5+ "strings"6+ "testing"7+)8+9+func rect(s string) Rect {10+ r, _ := ParseRange(s)11+ return r12+}13+14+func TestValidName(t *testing.T) {15+ for _, tc := range []struct {16+ name string17+ want string // start of the error, "" for valid18+ }{19+ {"Sales", ""},20+ {"_tax.rate", ""},21+ {"Q1_2026", ""},22+ {"R2D2", ""},23+ {"CAT", ""},24+ {"", "Enter a name"},25+ {"1st", "A name must start"},26+ {"net profit", "A name can only"},27+ {"$A", "A name must start"},28+ {"true", "TRUE and FALSE"},29+ {"A1", "A name can't look like a cell"},30+ {"xfd1048576", "A name can't look like a cell"},31+ {"R1C1", "A name can't look like a cell"},32+ {"rc", "A name can't look like a cell"},33+ {"C", "A name can't look like a cell"},34+ {strings.Repeat("a", 251), "A name can be at most"},35+ } {36+ err := ValidName(tc.name)37+ switch {38+ case tc.want == "" && err != nil:39+ t.Errorf("%q: %v", tc.name, err)40+ case tc.want != "" && (err == nil || !strings.HasPrefix(err.Error(), tc.want)):41+ t.Errorf("%q: got %v, want %q", tc.name, err, tc.want)42+ }43+ }44+}45+46+func TestNamesInFormulas(t *testing.T) {47+ s := sheetOf(t, map[string]string{48+ "B1": "10", "B2": "20", "B3": "30", "D1": "0.5",49+ "C1": "=SUM(sales)", "C2": "=Rate*2", "C3": "=AVERAGE(Sales)*Rate",50+ })51+ if v := s.Value(at("C1")); v != ErrName {52+ t.Fatalf("undefined name: %v", v)53+ }54+ if err := s.DefineName("Sales", rect("B1:B3")); err != nil {55+ t.Fatal(err)56+ }57+ if err := s.DefineName("Rate", rect("D1")); err != nil {58+ t.Fatal(err)59+ }60+ check := func(when string, want map[string]float64) {61+ t.Helper()62+ for a, n := range want {63+ if v := s.Value(at(a)); v.Kind != Number || v.Num != n {64+ t.Errorf("%s: %s = %v, want %v", when, a, v, n)65+ }66+ }67+ }68+ check("defined", map[string]float64{"C1": 60, "C2": 1, "C3": 10})69+ s.Set(at("B2"), "50")70+ s.Set(at("D1"), "1")71+ check("inputs changed", map[string]float64{"C1": 90, "C2": 2, "C3": 30})72+73+ if err := s.EditName("sales", "Sales", rect("B1:B2")); err != nil {74+ t.Fatal(err)75+ }76+ check("range edited", map[string]float64{"C1": 60})77+ if s.Value(at("B3")).Num != 30 {78+ t.Error("B3 changed")79+ }80+ if err := s.DeleteName("RATE"); err != nil {81+ t.Fatal(err)82+ }83+ if v := s.Value(at("C2")); v != ErrName {84+ t.Errorf("deleted name: C2 = %v", v)85+ }86+ if err := s.DefineName("SALES", rect("A1")); err == nil || err.Error() != "Sales already names B1:B2" {87+ t.Errorf("duplicate: %v", err)88+ }89+}90+91+func TestRenameRewritesFormulas(t *testing.T) {92+ s := sheetOf(t, map[string]string{"A1": "4", "B1": "=Old*2+SUM(old)", "B2": "=Other"})93+ s.DefineName("Old", rect("A1"))94+ if err := s.EditName("Old", "Fresh", rect("A1")); err != nil {95+ t.Fatal(err)96+ }97+ if got := s.Cell(at("B1")).Input; got != "=Fresh*2+SUM(Fresh)" {98+ t.Errorf("B1 = %q", got)99+ }100+ if s.Value(at("B1")).Num != 12 || s.Cell(at("B2")).Input != "=Other" {101+ t.Errorf("B1 %v B2 %q", s.Value(at("B1")), s.Cell(at("B2")).Input)102+ }103+ if _, ok := s.LookupName("old"); ok {104+ t.Error("old name kept")105+ }106+ s.Undo()107+ if got := s.Cell(at("B1")).Input; got != "=Old*2+SUM(old)" || s.Value(at("B1")).Num != 12 {108+ t.Errorf("undo: B1 = %q %v", got, s.Value(at("B1")))109+ }110+}111+112+func TestNamesFollowStructureChanges(t *testing.T) {113+ for _, tc := range []struct {114+ name string115+ op func(s *Sheet)116+ want string // the name's range after the change117+ }{118+ {"insert rows above", func(s *Sheet) { s.InsertRows(0, 2) }, "B4:B6"},119+ {"insert rows inside", func(s *Sheet) { s.InsertRows(2, 1) }, "B2:B5"},120+ {"insert columns left", func(s *Sheet) { s.InsertCols(0, 1) }, "C2:C4"},121+ {"delete rows inside", func(s *Sheet) { s.DeleteRows(2, 1) }, "B2:B3"},122+ {"delete all rows", func(s *Sheet) { s.DeleteRows(1, 3) }, "#REF!"},123+ {"delete the column", func(s *Sheet) { s.DeleteCols(1, 1) }, "#REF!"},124+ {"move the range", func(s *Sheet) { s.Move(rect("B2:B4"), at("D5")) }, "D5:D7"},125+ {"move part of it", func(s *Sheet) { s.Move(rect("B2:B3"), at("D5")) }, "B2:B4"},126+ } {127+ t.Run(tc.name, func(t *testing.T) {128+ s := sheetOf(t, map[string]string{"B2": "1", "B3": "2", "B4": "3", "A10": "=SUM(Data)"})129+ s.DefineName("Data", rect("B2:B4"))130+ tc.op(s)131+ n, _ := s.LookupName("data")132+ if n.Ref() != tc.want {133+ t.Fatalf("range %s, want %s", n.Ref(), tc.want)134+ }135+ // The formula still reads the name and follows it.136+ for _, a := range s.Addrs() {137+ if c := s.Cell(a); c.IsFormula() && c.Input != "=SUM(Data)" {138+ t.Errorf("formula %s", c.Input)139+ }140+ }141+ if n.Lost {142+ if v := s.Value(s.Addrs()[len(s.Addrs())-1]); v != ErrRef {143+ t.Errorf("lost name evaluates to %v", v)144+ }145+ }146+ s.Undo()147+ if n, _ := s.LookupName("Data"); n.Ref() != "B2:B4" {148+ t.Errorf("undo: %s", n.Ref())149+ }150+ })151+ }152+}153+154+func TestNamesUndoRedo(t *testing.T) {155+ s := sheetOf(t, map[string]string{"A1": "5", "B1": "=Price"})156+ s.DefineName("Price", rect("A1"))157+ if s.Value(at("B1")).Num != 5 {158+ t.Fatalf("B1 = %v", s.Value(at("B1")))159+ }160+ c, ok := s.Undo()161+ if !ok || c.Label != "name A1 Price" || s.Value(at("B1")) != ErrName || len(s.Names()) != 0 {162+ t.Fatalf("undo: %v %v B1=%v names %v", c, ok, s.Value(at("B1")), s.Names())163+ }164+ s.Redo()165+ if s.Value(at("B1")).Num != 5 {166+ t.Fatalf("redo: B1 = %v", s.Value(at("B1")))167+ }168+ s.DeleteName("price")169+ s.Undo()170+ if n, ok := s.LookupName("PRICE"); !ok || n.Name != "Price" || s.Value(at("B1")).Num != 5 {171+ t.Errorf("undo delete: %v %v", n, s.Value(at("B1")))172+ }173+}174+175+func TestNameCycles(t *testing.T) {176+ s := sheetOf(t, map[string]string{"A1": "=SUM(Loop)", "A2": "1"})177+ s.DefineName("Loop", rect("A1:A2"))178+ if !s.Circular || s.Value(at("A1")) != ErrRef {179+ t.Errorf("circular %v A1 %v", s.Circular, s.Value(at("A1")))180+ }181+}182+183+func TestNamesFileRoundTrip(t *testing.T) {184+ s := sheetOf(t, map[string]string{"A1": "3", "A2": "4", "B1": "=SUM(Pair)", "B2": "=Gone"})185+ s.DefineName("Pair", rect("A1:A2"))186+ s.DefineName("Gone", rect("C5"))187+ s.DeleteCols(2, 1)188+ var b strings.Builder189+ if err := s.Write(&b); err != nil {190+ t.Fatal(err)191+ }192+ out := b.String()193+ for _, want := range []string{`"version": 3`, `"names": {"Gone": "#REF!", "Pair": "A1:A2"}`} {194+ if !strings.Contains(out, want) {195+ t.Errorf("missing %s in\n%s", want, out)196+ }197+ }198+ r, err := Read(strings.NewReader(out))199+ if err != nil {200+ t.Fatal(err)201+ }202+ if !reflect.DeepEqual(r.Names(), s.Names()) || r.Value(at("B1")).Num != 7 || r.Value(at("B2")) != ErrRef {203+ t.Errorf("read back %v B1=%v B2=%v", r.Names(), r.Value(at("B1")), r.Value(at("B2")))204+ }205+ if r.CanUndo() {206+ t.Error("loading is undoable")207+ }208+209+ for _, bad := range []string{210+ `{"version": 3, "names": {"A1": "B2"}, "cells": {}}`,211+ `{"version": 3, "names": {"x": "B2:"}, "cells": {}}`,212+ `{"version": 3, "names": {"x": "B2", "X": "B3"}, "cells": {}}`,213+ } {214+ if _, err := Read(strings.NewReader(bad)); err == nil {215+ t.Errorf("accepted %s", bad)216+ }217+ }218+}219+220+func TestPrecedentsAndDependents(t *testing.T) {221+ s := sheetOf(t, map[string]string{222+ "A1": "1", "A2": "2", "B1": "3",223+ "C1": "=A1+SUM(A1:A2)+Both+A1", "C2": "=A2*2", "C3": "=C1", "D1": "text",224+ })225+ s.DefineName("Both", rect("A2:B2"))226+ if got, want := s.Precedents(at("C1")), []Rect{rect("A1"), rect("A1:A2"), rect("A2:B2")}; !reflect.DeepEqual(got, want) {227+ t.Errorf("precedents of C1 = %v, want %v", got, want)228+ }229+ if got := s.Precedents(at("D1")); got != nil {230+ t.Errorf("precedents of text: %v", got)231+ }232+ if got, want := s.Dependents(at("A2")), []Addr{at("C1"), at("C2")}; !reflect.DeepEqual(got, want) {233+ t.Errorf("dependents of A2 = %v, want %v", got, want)234+ }235+ if got, want := s.Dependents(at("B2")), []Addr{at("C1")}; !reflect.DeepEqual(got, want) {236+ t.Errorf("dependents of B2 through a name = %v, want %v", got, want)237+ }238+ if got := s.Dependents(at("B1")); len(got) != 0 {239+ t.Errorf("dependents of B1 = %v", got)240+ }241+}
M internal/sheet/ops.go +6 -1
M internal/sheet/parse.go +22 -3
@@ -24,7 +24,7 @@ type (
2424 abs [2]absFlags2525 }2626 refErrNode struct{} // a reference to deleted cells: #REF!27- nameNode struct{ name string } // an identifier that isn't a cell; #NAME? until names exist27+ nameNode struct{ name string } // a named range as spelled in the formula; #NAME? if undefined2828 emptyArg struct{} // an omitted argument, as in XLOOKUP(a, b, c, , 1)2929 unaryNode struct {3030 op string
@@ -188,6 +188,7 @@ const (
188188 )189189 190190 type parser struct {191+ src string // the formula without its "=", for names' original spelling191192 toks []token192193 pos int193194 }
@@ -201,7 +202,7 @@ func Parse(src string) (Node, error) {
201202 if err != nil {202203 return nil, shift(err, offset)203204 }204- p := &parser{toks: toks}205+ p := &parser{src: body, toks: toks}205206 n, err := p.expr(0)206207 if err == nil && p.peek().kind != tokEOF {207208 err = &ParseError{p.peek().pos, "Unexpected " + p.peek().text}
@@ -316,7 +317,8 @@ func (p *parser) ident(t token) (Node, error) {
316317 case "FALSE":317318 return boolLit{false}, nil318319 }319- return nameNode{t.text}, nil320+ // Names match case-insensitively but print as written.321+ return nameNode{p.src[t.pos : t.pos+len(t.text)]}, nil320322 }321323 if p.isOp(":") || p.isOp("..") {322324 sep := p.next()
@@ -386,6 +388,23 @@ func (p *parser) call(t token) (Node, error) {
386388 return n, nil387389 }388390 391+// walkNames calls fn for every name in n.392+func walkNames(n Node, fn func(nameNode)) {393+ switch n := n.(type) {394+ case nameNode:395+ fn(n)396+ case unaryNode:397+ walkNames(n.x, fn)398+ case binaryNode:399+ walkNames(n.l, fn)400+ walkNames(n.r, fn)401+ case callNode:402+ for _, a := range n.args {403+ walkNames(a, fn)404+ }405+ }406+}407+389408 // walkRefs calls fn for every single-cell reference and range in n.390409 func walkRefs(n Node, ref func(Addr), rng func(Rect)) {391410 switch n := n.(type) {
M internal/sheet/refs.go +18 -5
@@ -5,11 +5,12 @@ package sheet
55 // the cells that moved, as Sheets does. A reference that can't be kept66 // becomes #REF!.77 8-// refRewrite maps the references in a formula. Either function may return9-// refErrNode.8+// refRewrite maps the references in a formula. Any function may return9+// refErrNode; a nil function leaves those nodes alone.1010 type refRewrite struct {11- ref func(refNode) Node12- rng func(rangeNode) Node11+ ref func(refNode) Node12+ rng func(rangeNode) Node13+ name func(nameNode) Node1314 }1415 1516 // rewrite returns n with its references mapped, and whether anything
@@ -17,11 +18,23 @@ type refRewrite struct {
1718 func rewrite(n Node, rw refRewrite) (Node, bool) {1819 switch n := n.(type) {1920 case refNode:21+ if rw.ref == nil {22+ return n, false23+ }2024 out := rw.ref(n)2125 return out, out != Node(n)2226 case rangeNode:27+ if rw.rng == nil {28+ return n, false29+ }2330 out := rw.rng(n)2431 return out, out != Node(n)32+ case nameNode:33+ if rw.name == nil {34+ return n, false35+ }36+ out := rw.name(n)37+ return out, out != Node(n)2538 case unaryNode:2639 x, ok := rewrite(n.x, rw)2740 if ok {
@@ -119,7 +132,7 @@ func (c *Cell) withFormula(n Node) *Cell {
119132 // rewritten returns c with its references rewritten, or c itself if none120133 // changed.121134 func (c *Cell) rewritten(rw refRewrite) *Cell {122- if c.expr == nil || (len(c.refs) == 0 && len(c.ranges) == 0) {135+ if c.expr == nil || (len(c.refs) == 0 && len(c.ranges) == 0 && len(c.names) == 0) {123136 return c124137 }125138 n, changed := rewrite(c.expr, rw)
M internal/sheet/refs_test.go +1 -1
M internal/sheet/remote.go +1 -1
@@ -80,6 +80,6 @@ func (s *Sheet) RemoteCalls(a Addr) []RemoteCall {
8080 }8181 }8282 }83- walk(c.expr)83+ walk(s.bound(c))8484 return calls8585 }
M internal/sheet/sheet.go +39 -9
@@ -28,11 +28,12 @@ type Cell struct {
2828 Format Format2929 Style Style3030 31- auto Format // format inferred from a formula, shown when Format is Automatic32- expr Node // nil for text33- refs []Addr // single-cell references in expr34- ranges []Rect // range references in expr35- volatile bool // expr calls TODAY, NOW, RAND...31+ auto Format // format inferred from a formula, shown when Format is Automatic32+ expr Node // nil for text33+ refs []Addr // single-cell references in expr34+ ranges []Rect // range references in expr35+ names []string // names in expr, as keys of Sheet.names36+ volatile bool // expr calls TODAY, NOW, RAND...3637 }3738 3839 // IsFormula reports whether the cell holds a formula.
@@ -56,6 +57,12 @@ type Sheet struct {
5657 rangeUsers map[Addr]struct{}5758 volatile map[Addr]struct{} // formulas recalculated on every change5859 60+ // names are the named ranges by upper-case name (see names.go), and61+ // nameUsers the formula cells that mention each name, defined or not,62+ // so defining a name recalculates the formulas waiting for it.63+ names map[string]Name64+ nameUsers map[string]map[Addr]struct{}65+5966 // Circular is set when the last recalculation found a cycle.6067 Circular bool6168
@@ -70,6 +77,8 @@ func New() *Sheet {
7077 dependents: make(map[Addr]map[Addr]struct{}),7178 rangeUsers: make(map[Addr]struct{}),7279 volatile: make(map[Addr]struct{}),80+ names: make(map[string]Name),81+ nameUsers: make(map[string]map[Addr]struct{}),7382 }7483 }7584
@@ -252,11 +261,12 @@ func formattingOnly(f Format, st Style) *Cell {
252261 253262 // setExpr sets c's expression and the references indexed from it.254263 func (c *Cell) setExpr(n Node) {255- c.expr, c.refs, c.ranges, c.volatile = n, nil, nil, false264+ c.expr, c.refs, c.ranges, c.names, c.volatile = n, nil, nil, nil, false256265 if n != nil {257266 walkRefs(n,258267 func(r Addr) { c.refs = append(c.refs, r) },259268 func(r Rect) { c.ranges = append(c.ranges, r) })269+ walkNames(n, func(nn nameNode) { c.names = append(c.names, nameKey(nn.name)) })260270 c.volatile = isVolatile(n)261271 }262272 }
@@ -283,6 +293,12 @@ func (s *Sheet) place(a Addr, c *Cell) {
283293 if c.volatile {284294 s.volatile[a] = struct{}{}285295 }296+ for _, k := range c.names {297+ if s.nameUsers[k] == nil {298+ s.nameUsers[k] = make(map[Addr]struct{})299+ }300+ s.nameUsers[k][a] = struct{}{}301+ }286302 }287303 288304 // EraseRange clears the contents of every cell in r, keeping their
@@ -320,6 +336,12 @@ func (s *Sheet) unlink(a Addr) {
320336 delete(s.dependents, r)321337 }322338 }339+ for _, k := range old.names {340+ delete(s.nameUsers[k], a)341+ if len(s.nameUsers[k]) == 0 {342+ delete(s.nameUsers, k)343+ }344+ }323345 delete(s.rangeUsers, a)324346 delete(s.volatile, a)325347 delete(s.cells, a)
@@ -366,6 +388,13 @@ func (s *Sheet) recalc(changed []Addr) {
366388 }367389 }368390 }391+ for k, users := range s.nameUsers {392+ if nm, ok := s.names[k]; ok && !nm.Lost && nm.Range.Contains(a) {393+ for u := range users {394+ queue = append(queue, u)395+ }396+ }397+ }369398 }370399 371400 s.Circular = false
@@ -391,9 +420,10 @@ func (s *Sheet) recalc(changed []Addr) {
391420 case c.expr == nil:392421 c.Value = Value{Kind: Text, Str: strings.TrimPrefix(c.Input, "'")}393422 default:394- c.Value = eval(c.expr, compute)395- if _, lit := c.expr.(numLit); !lit {396- c.auto = inferFormat(c.expr, s.DisplayFormat)423+ expr := s.bound(c)424+ c.Value = eval(expr, compute)425+ if _, lit := expr.(numLit); !lit {426+ c.auto = inferFormat(expr, s.DisplayFormat)397427 }398428 }399429 state[a] = done
A internal/sheet/trace.go +54 -0