PrecedentLinks and DependentLinks find what a cell comes from and what reads it, through references, ranges, named ranges, nu. regions, spills and the files or notebook cells regions show; dependents are capped so a cell read by millions of formulas costs the cap. Reads asks the same of one formula, for marking what's on screen. EvaluateSteps evaluates a formula once with each operator and call wrapped in a probe that records its value where it stands, so stepping through it shows what the one evaluator computed, IF's untaken branch left alone.
- commit
- c39110254a94bac339e8dc7b8bfc188a14396ef7
- parent
- 84dcbde042ed2496505781a75cdebf9c63ea8a98
- author
- Dami <dami.etoile@gmail.com>
- kind
- feat risk score 1.9 of 3, Jev's estimate
| M | internal/ | 7 | +- |
| A | internal/ | 170 | ++++++++++++++++++++++++ |
| A | internal/ | 58 | ++++++++ |
| A | internal/ | 215 | ++++++++++++++++++++++++++++++ |
| A | internal/ | 118 | ++++++++++++++++ |
| A | internal/ | 149 | +++++++++++++++++++++ |
| M | internal/ | 164 | +++++++++++++++++------ |
| A | internal/ | 101 | ++++++++++++++ |
| A | internal/ | 170 | ++++++++++++++++++++++++ |
@@ -170,10 +170,13 @@ func eval(n Node, get lookup) Value {
170170 }171171 172172 // evalOther evaluates what formulas rarely hold: array literals, names173-// LET and LAMBDA bind, LAMBDA calls, and arguments standing in for174-// arrays being mapped over.173+// LET and LAMBDA bind, LAMBDA calls, arguments standing in for arrays174+// being mapped over, and parts of a formula being stepped through175+// (probe.go).175176 func evalOther(n Node, get lookup) Value {176177 switch n := n.(type) {178+ case probe:179+ return n.eval(get)177180 case formula.Array:178181 return get.literal(n)179182 case formula.Local:@@ -0,0 +1,170 @@
1+package functions2+3+import (4+ "slices"5+6+ "github.com/FelineStateMachine/012/internal/formula"7+)8+9+// Parts of a formula evaluated where they stand, for stepping through it10+// as Excel's Evaluate Formula does (the engine's steps.go). Rather than a11+// second evaluator, the formula is evaluated once as usual with each12+// part wrapped in a probe, which records what the part computed there:13+// in its array context or not, inside LET with its names bound, and not14+// at all in an IF's branch not taken. Only operators, calls and LAMBDA15+// calls are wrapped: functions look at their arguments' nodes only to16+// tell references, ranges, names and literals apart, which stay as they17+// are, so a probe changes nothing a function sees.18+19+// Part is what one part of a formula computed the first time it was20+// computed.21+type Part struct {22+ Value Value23+ // Array holds the first rows and columns of an array the part24+ // computed, with Rows and Cols its whole size; nil otherwise.25+ Array *Array26+ // Lambda is set when the part computed a LAMBDA.27+ Lambda bool28+ // Reached is false for a part the formula never computed.29+ Reached bool30+}31+32+// previewSize is how many rows and columns of an array a Part keeps.33+const previewSize = 834+35+// probe is a part of a formula being recorded (see Part).36+type probe struct {37+ x Node38+ i int39+ parts []Part40+}41+42+// EvalParts evaluates n in the cell at, as EvalCell does, and returns43+// what it computed, with the array it spills, and what each part at44+// paths computed. A path is child45+// indexes from the root, in formula.EachChild's order; the empty path is46+// the whole formula. A path to a reference, name or literal records47+// nothing: those are read where they stand.48+func EvalParts(n Node, paths [][]int, get *Reader, at Addr) (Part, []Part) {49+ parts := make([]Part, len(paths))50+ for i, p := range paths {51+ n = wrapAt(n, p, probe{i: i, parts: parts})52+ }53+ top := Part{Value: EvalCell(n, get, at), Reached: true}54+ if sp := get.Spilled(); sp != nil {55+ top.Array = preview(sp)56+ }57+ return top, parts58+}59+60+// probed reports whether a part of this kind is wrapped in a probe.61+func probed(n Node) bool {62+ switch n.(type) {63+ case formula.Unary, formula.Binary, formula.Call, formula.Invoke, decUnary, decBinary:64+ return true65+ }66+ return false67+}68+69+// ReplaceAt returns a copy of n with the node at path, child indexes in70+// formula.EachChild's order, replaced by what f makes of it. The nodes71+// on the way are copied; n itself is left as it is.72+func ReplaceAt(n Node, path []int, f func(Node) Node) Node {73+ if len(path) == 0 {74+ return f(n)75+ }76+ return withChild(n, path[0], func(k Node) Node { return ReplaceAt(k, path[1:], f) })77+}78+79+// wrapAt returns n with the node at path wrapped in p, when it is one80+// probed.81+func wrapAt(n Node, path []int, p probe) Node {82+ if len(path) == 0 {83+ if !probed(n) {84+ return n85+ }86+ p.x = n87+ return p88+ }89+ return withChild(n, path[0], func(k Node) Node { return wrapAt(k, path[1:], p) })90+}91+92+// withChild returns a copy of n with its child i replaced by f's result,93+// looking through probes already in place.94+func withChild(n Node, i int, f func(Node) Node) Node {95+ switch n := n.(type) {96+ case probe:97+ n.x = withChild(n.x, i, f)98+ return n99+ case formula.Unary:100+ n.X = f(n.X)101+ return n102+ case decUnary:103+ n.X = f(n.X)104+ return n105+ case formula.Binary:106+ n.L, n.R = pick(i, n.L, n.R, f)107+ return n108+ case decBinary:109+ n.L, n.R = pick(i, n.L, n.R, f)110+ return n111+ case formula.Call:112+ n.Args = slices.Clone(n.Args)113+ n.Args[i] = f(n.Args[i])114+ return n115+ case formula.Invoke:116+ if i == 0 {117+ n.Fn = f(n.Fn)118+ return n119+ }120+ n.Args = slices.Clone(n.Args)121+ n.Args[i-1] = f(n.Args[i-1])122+ return n123+ case formula.Array:124+ rows := slices.Clone(n.Rows)125+ for r, row := range rows {126+ if i < len(row) {127+ rows[r] = slices.Clone(row)128+ rows[r][i] = f(row[i])129+ break130+ }131+ i -= len(row)132+ }133+ n.Rows = rows134+ return n135+ }136+ return n137+}138+139+// pick applies f to l or r, by i.140+func pick(i int, l, r Node, f func(Node) Node) (Node, Node) {141+ if i == 0 {142+ return f(l), r143+ }144+ return l, f(r)145+}146+147+// eval computes the part and records it, the first time.148+func (p probe) eval(get lookup) Value {149+ v := eval(p.x, get)150+ if pt := &p.parts[p.i]; !pt.Reached {151+ pt.Value, pt.Reached = v, true152+ if arr := get.arrayOf(v); arr != nil {153+ pt.Array = preview(arr)154+ }155+ pt.Lambda = get.lambdaOf(v) != nil156+ }157+ return v158+}159+160+// preview copies the first rows and columns of a.161+func preview(a *Array) *Array {162+ rows, cols := min(a.Rows, previewSize), min(a.Cols, previewSize)163+ out := &Array{Rows: a.Rows, Cols: a.Cols, DRows: rows, DCols: cols, V: make([]Value, rows*cols), Fill: a.Fill}164+ for r := range rows {165+ for c := range cols {166+ out.V[r*cols+c] = a.At(r, c)167+ }168+ }169+ return out170+}@@ -0,0 +1,58 @@
1+package sheet2+3+// overlapping calls fn with each formula one of whose ranges shares a4+// cell with r, as readers does for one cell; a formula may come more5+// than once. It walks r's columns or the indexed ones, whichever are6+// fewer.7+func (x *rangeIndex) overlapping(r Rect, fn func(Addr)) {8+ if r.From == r.To {9+ x.readers(r.From, fn)10+ return11+ }12+ if r.To.Col-r.From.Col < len(x.byCol) {13+ for c := r.From.Col; c <= r.To.Col; c++ {14+ if iv := x.byCol[c]; iv != nil {15+ iv.overlap(r, fn)16+ }17+ }18+ } else {19+ for c, iv := range x.byCol {20+ if c >= r.From.Col && c <= r.To.Col {21+ iv.overlap(r, fn)22+ }23+ }24+ }25+ x.wide.overlap(r, fn)26+}27+28+// overlap calls fn with the users of every range sharing a cell with r.29+func (iv *intervals) overlap(r Rect, fn func(Addr)) {30+ if len(iv.users) == 0 {31+ return32+ }33+ if iv.sorted == nil {34+ iv.build()35+ }36+ iv.queryRect(0, len(iv.sorted), r, fn)37+}38+39+// queryRect is query for the rows of r rather than one row.40+func (iv *intervals) queryRect(lo, hi int, r Rect, fn func(Addr)) {41+ for lo < hi {42+ mid := (lo + hi) / 243+ if iv.maxEnd[mid] < r.From.Row {44+ return // nothing below reaches r's rows45+ }46+ iv.queryRect(lo, mid, r, fn)47+ g := iv.sorted[mid]48+ if g.From.Row > r.To.Row {49+ return // this and everything after start below r50+ }51+ if _, over := intersectRect(g, r); over {52+ for u := range iv.users[g] {53+ fn(u)54+ }55+ }56+ lo = mid + 157+ }58+}@@ -0,0 +1,215 @@
1+package sheet2+3+import (4+ "slices"5+6+ "github.com/FelineStateMachine/012/internal/formula"7+ "github.com/FelineStateMachine/012/internal/functions"8+)9+10+// Steps is a formula evaluated one part at a time, as Excel's Evaluate11+// Formula: the part computed next is marked in the formula, computing it12+// puts its value in its place, and a reference to another formula can be13+// stepped into. The values come from one evaluation by the function14+// library with each part recorded where it stands (functions.EvalParts),15+// so they are what the formula computes, context and all, and a branch16+// IF didn't take is never computed. References are read as the formula17+// reads them: the cell's value.18+type Steps struct {19+ sheet *Sheet20+ at Addr21+ expr Node // the formula as written, which Text prints22+ parts []part // in the order they're computed: operands first, left to right23+ next int // the part computed next; len(parts) once done24+ top functions.Part25+ index map[string]int // parts by pathKey, while references are read26+}27+28+// part is one part of the formula: an operator, a call or a reference.29+type part struct {30+ path []int31+ node Node // as written32+ functions.Part33+ into loc // a reference to a formula's cell, which can be stepped into34+}35+36+// EvaluateSteps starts evaluating the formula at a step by step, or37+// returns nil when a holds no formula.38+func (s *Sheet) EvaluateSteps(a Addr) *Steps {39+ c, _, _ := s.cells.peek(a)40+ if c == nil || !c.IsFormula() {41+ return nil42+ }43+ st := &Steps{sheet: s, at: a, expr: c.expr}44+ var paths [][]int45+ st.collect(c.expr, nil, func(p []int, n Node) {46+ st.parts = append(st.parts, part{path: p, node: n})47+ paths = append(paths, p)48+ })49+ run := s.wb.arith(s.bound(c))50+ var parts []functions.Part51+ st.top, parts = functions.EvalParts(run, paths, s.wb.values(s).lib, a)52+ for i := range st.parts {53+ st.parts[i].Part = parts[i]54+ }55+ st.index = make(map[string]int, len(st.parts))56+ for i, p := range st.parts {57+ st.index[pathKey(p.path)] = i58+ }59+ st.readRefs()60+ st.index = nil61+ st.parts = slices.DeleteFunc(st.parts, func(p part) bool { return !p.Reached })62+ return st63+}64+65+// collect calls fn with the path of each part of n, operands before66+// what they're operands of: operators, calls, LAMBDA calls, and67+// references to one cell, written or named.68+func (st *Steps) collect(n Node, path []int, fn func([]int, Node)) {69+ i := 070+ formula.EachChild(n, func(k Node) {71+ st.collect(k, append(slices.Clip(path), i), fn)72+ i++73+ })74+ switch n := n.(type) {75+ case formula.Unary, formula.Binary, formula.Call, formula.Invoke, formula.Ref:76+ fn(path, n)77+ case formula.Name:78+ if nm, ok := st.sheet.wb.names[nameKey(n.Name)]; ok && !nm.Gone() && nm.Range.From == nm.Range.To {79+ fn(path, n)80+ }81+ }82+}83+84+// readRefs reads the references among the parts, as the formula reads85+// them, where the formula reached them.86+func (st *Steps) readRefs() {87+ for i := range st.parts {88+ p := &st.parts[i]89+ t, a, ok := st.refCell(p.node)90+ if !ok || !st.reached(p.path) {91+ continue92+ }93+ p.Reached = true94+ if t == nil {95+ p.Value = ErrRef96+ continue97+ }98+ p.Value = t.Value(a)99+ if c, _, _ := t.cells.peek(a); c != nil && c.IsFormula() {100+ p.into = loc{t, a}101+ }102+ }103+}104+105+// refCell is the cell a reference or single-cell name points at, and106+// its sheet (nil for a sheet no sheet has the name of).107+func (st *Steps) refCell(n Node) (*Sheet, Addr, bool) {108+ s := st.sheet109+ switch n := n.(type) {110+ case formula.Ref:111+ return s.wb.resolve(s, n.Sheet), n.Addr, true112+ case formula.Name:113+ nm := s.wb.names[nameKey(n.Name)]114+ return nm.Sheet, nm.Range.From, true115+ }116+ return nil, Addr{}, false117+}118+119+// reached reports whether the formula computed what's at path, a120+// reference: when the part it's in was computed, and, for an argument of121+// IF, IFERROR, IFNA or CHOOSE, when that call took it.122+func (st *Steps) reached(path []int) bool {123+ for up := len(path) - 1; up >= 0; up-- {124+ i, ok := st.index[pathKey(path[:up])]125+ if !ok {126+ continue // an array literal: go on out127+ }128+ o := &st.parts[i]129+ if !o.Reached {130+ return false131+ }132+ if up == len(path)-1 {133+ return st.taken(o, path[up])134+ }135+ return true136+ }137+ return true138+}139+140+// taken reports whether the call o, computed, took its argument arg.141+func (st *Steps) taken(o *part, arg int) bool {142+ call, ok := o.node.(formula.Call)143+ if !ok || arg == 0 {144+ return true145+ }146+ first := st.valueAt(append(slices.Clip(o.path), 0), call.Args[0])147+ switch call.Fn.Signature().Name {148+ case "IF":149+ if first.Kind == Error || first.Kind == Text {150+ return false151+ }152+ return (first.Num != 0) == (arg == 1)153+ case "IFERROR":154+ return first.Kind == Error155+ case "IFNA":156+ return first == ErrNA157+ case "CHOOSE":158+ return first.Kind == Number && int(first.Num) == arg159+ }160+ return true161+}162+163+// valueAt is what the node n at path computed: a part's recorded value,164+// or a literal's own.165+func (st *Steps) valueAt(path []int, n Node) Value {166+ if i, ok := st.index[pathKey(path)]; ok && st.parts[i].Reached {167+ return st.parts[i].Value168+ }169+ if t, a, ok := st.refCell(n); ok && t != nil {170+ return t.Value(a)171+ }172+ return functions.EvalAt(n, st.sheet.wb.values(st.sheet).lib, st.at)173+}174+175+// Cell is the formula's cell.176+func (st *Steps) Cell() (*Sheet, Addr) { return st.sheet, st.at }177+178+// Done reports whether every part is computed.179+func (st *Steps) Done() bool { return st.next >= len(st.parts) }180+181+// Progress is how many parts are computed, of how many.182+func (st *Steps) Progress() (done, total int) { return st.next, len(st.parts) }183+184+// Step computes the next part.185+func (st *Steps) Step() {186+ if !st.Done() {187+ st.next++188+ }189+}190+191+// Restart goes back to the formula as written.192+func (st *Steps) Restart() { st.next = 0 }193+194+// Next is the part computed next, as written, and its value as the195+// formula would write it; ok is false once done.196+func (st *Steps) Next() (expr, value string, ok bool) {197+ if st.Done() {198+ return "", "", false199+ }200+ p := st.parts[st.next]201+ return formula.Expr(p.node), partText(p.Part), true202+}203+204+// Result is the formula's value, an array it spills written whole.205+func (st *Steps) Result() string { return partText(st.top) }206+207+// Into starts stepping through the formula the next part refers to,208+// when it is a reference to a formula's cell; nil otherwise.209+func (st *Steps) Into() *Steps {210+ if st.Done() || st.parts[st.next].into.s == nil {211+ return nil212+ }213+ l := st.parts[st.next].into214+ return l.s.EvaluateSteps(l.a)215+}@@ -0,0 +1,118 @@
1+package sheet2+3+import (4+ "strings"5+ "testing"6+)7+8+// shown writes what Steps shows: values in [], the next part in _.9+func shown(st *Steps) string {10+ var out strings.Builder11+ in := false12+ for _, sg := range st.Text() {13+ if sg.Next != in {14+ out.WriteByte('_')15+ in = sg.Next16+ }17+ t := sg.Text18+ if sg.Value {19+ t = "[" + t + "]"20+ }21+ out.WriteString(t)22+ }23+ if in {24+ out.WriteByte('_')25+ }26+ return out.String()27+}28+29+// walk steps through the formula at a, returning what it shows before30+// each step and at the end.31+func walk(t *testing.T, s *Sheet, a string) []string {32+ t.Helper()33+ st := s.EvaluateSteps(at(a))34+ if st == nil {35+ t.Fatalf("%s: no steps", a)36+ }37+ var out []string38+ for !st.Done() {39+ out = append(out, shown(st))40+ st.Step()41+ }42+ return append(out, shown(st))43+}44+45+func TestSteps(t *testing.T) {46+ s := sheetOf(t, map[string]string{47+ "A1": "5", "A2": "2", "A3": "x",48+ "B1": "=A1+A2*3",49+ "B2": "=IF(A1>3, A1*2, A2+1)",50+ "B3": "=(A1+A2)*2",51+ "B4": "=SUM(A1:A2*2)",52+ "B5": "=LET(x, A1, x*2)",53+ "B6": "=A3&D9",54+ "B7": "=IFERROR(1/0, A2)",55+ "B8": "=SEQUENCE(2)",56+ })57+ for _, tc := range []struct {58+ cell string59+ want string60+ }{61+ {"B1", "=_A1_+A2*3 | =[5]+_A2_*3 | =[5]+_[2]*3_ | =_[5]+[6]_ | [11]"},62+ // The branch IF doesn't take is never computed.63+ {"B2", "=IF(_A1_>3,A1*2,A2+1) | =IF(_[5]>3_,A1*2,A2+1) | =IF([TRUE],_A1_*2,A2+1) | =IF([TRUE],_[5]*2_,A2+1) | =_IF([TRUE],[10],A2+1)_ | [10]"},64+ // The part computed next keeps its parentheses.65+ {"B3", "=(_A1_+A2)*2 | =([5]+_A2_)*2 | =_([5]+[2])_*2 | =_[7]*2_ | [14]"},66+ // In an array context a part computes an array.67+ {"B4", "=SUM(_A1:A2*2_) | =_SUM([{10;4}])_ | [14]"},68+ {"B5", "=LET(x,_A1_,x*2) | =LET(x,[5],_x*2_) | =_LET(x,[5],[10])_ | [10]"},69+ {"B6", `=_A3_&D9 | =["x"]&_D9_ | =_["x"]&[0]_ | ["x"]`},70+ {"B7", "=IFERROR(_1/0_,A2) | =IFERROR([#DIV/0!],_A2_) | =_IFERROR([#DIV/0!],[2])_ | [2]"},71+ {"B8", "=_SEQUENCE(2)_ | [{1;2}]"},72+ } {73+ if got := strings.Join(walk(t, s, tc.cell), " | "); got != tc.want {74+ t.Errorf("%s:\n got %s\nwant %s", tc.cell, got, tc.want)75+ }76+ }77+ if s.EvaluateSteps(at("A1")) != nil {78+ t.Error("steps for a number")79+ }80+}81+82+func TestStepsDecimal(t *testing.T) {83+ s := sheetOf(t, map[string]string{"A1": "0.1", "B1": "=A1+0.2"})84+ s.SetDecimal(true)85+ if got := strings.Join(walk(t, s, "B1"), " | "); got != "=_A1_+0.2 | =_[0.1]+0.2_ | [0.3]" {86+ t.Errorf("decimal: %s", got)87+ }88+}89+90+func TestStepsInto(t *testing.T) {91+ s := sheetOf(t, map[string]string{"A1": "4", "B1": "=A1*2", "C1": "=B1+Rate"})92+ s.DefineName("Rate", rect("A1"))93+ st := s.EvaluateSteps(at("C1"))94+ if expr, v, _ := st.Next(); expr != "B1" || v != "8" {95+ t.Errorf("next %q = %q", expr, v)96+ }97+ in := st.Into()98+ if in == nil {99+ t.Fatal("no stepping into B1")100+ }101+ if _, a := in.Cell(); a != at("B1") || shown(in) != "=_A1_*2" {102+ t.Errorf("into %v: %s", a, shown(in))103+ }104+ if in.Into() != nil {105+ t.Error("stepped into a number")106+ }107+ st.Step()108+ if expr, v, _ := st.Next(); expr != "Rate" || v != "4" {109+ t.Errorf("a name: %q = %q", expr, v)110+ }111+ if done, total := st.Progress(); done != 1 || total != 3 {112+ t.Errorf("progress %d of %d", done, total)113+ }114+ st.Restart()115+ if shown(st) != "=_B1_+Rate" {116+ t.Errorf("restart: %s", shown(st))117+ }118+}@@ -0,0 +1,149 @@
1+package sheet2+3+import (4+ "slices"5+ "strconv"6+ "strings"7+8+ "github.com/FelineStateMachine/012/internal/formula"9+ "github.com/FelineStateMachine/012/internal/functions"10+)11+12+// How Steps shows the formula: as written, with the parts computed so13+// far written as their values and the part computed next marked. The14+// formula printer writes it, from a copy of the formula in which each15+// value stands as a name made of a marker, so the parentheses come out16+// as they would; the marked part is found by printing the copy twice,17+// once with a marker in its place.18+19+// Segment is a piece of the formula Steps shows.20+type Segment struct {21+ Text string22+ // Value marks a value a part computed, written in its place.23+ Value bool24+ // Next marks the part computed next.25+ Next bool26+}27+28+// Markers in the printed formula: a value, \x01 and its index and \x02,29+// and the part computed next, \x03.30+const (31+ valueOpen = "\x01"32+ valueClose = "\x02"33+ nextMark = "\x03"34+)35+36+// Text is the formula as it stands: "=" and its text, with the parts37+// computed so far as their values and the next part marked, or once38+// every part is computed, the formula's value.39+func (st *Steps) Text() []Segment {40+ if st.Done() {41+ return []Segment{{Text: st.Result(), Value: true}}42+ }43+ shown, values := st.expr, []string(nil)44+ var replaced [][]int45+ for i := st.next - 1; i >= 0; i-- {46+ p := st.parts[i]47+ if inside(p.path, replaced) {48+ continue49+ }50+ replaced = append(replaced, p.path)51+ mark := formula.Name{Name: valueOpen + strconv.Itoa(len(values)) + valueClose}52+ values = append(values, partText(p.Part))53+ shown = functions.ReplaceAt(shown, p.path, func(Node) Node { return mark })54+ }55+ whole := formula.Expr(shown)56+ marked := formula.Expr(functions.ReplaceAt(shown, st.parts[st.next].path, func(Node) Node { return formula.Name{Name: nextMark} }))57+ at := strings.Index(marked, nextMark)58+ after := marked[at+len(nextMark):]59+ end := len(whole) - len(after)60+ out := []Segment{{Text: "="}}61+ out = appendValues(out, whole[:at], values, false)62+ out = appendValues(out, whole[at:end], values, true)63+ return appendValues(out, whole[end:], values, false)64+}65+66+// inside reports whether path is within one of paths.67+func inside(path []int, paths [][]int) bool {68+ for _, p := range paths {69+ if len(p) <= len(path) && slices.Equal(p, path[:len(p)]) {70+ return true71+ }72+ }73+ return false74+}75+76+// appendValues splits text at its value markers into segments.77+func appendValues(out []Segment, text string, values []string, next bool) []Segment {78+ for text != "" {79+ i := strings.Index(text, valueOpen)80+ if i < 0 {81+ return append(out, Segment{Text: text, Next: next})82+ }83+ if i > 0 {84+ out = append(out, Segment{Text: text[:i], Next: next})85+ }86+ j := strings.Index(text, valueClose)87+ k, _ := strconv.Atoi(text[i+len(valueOpen) : j])88+ out = append(out, Segment{Text: values[k], Value: true, Next: next})89+ text = text[j+len(valueClose):]90+ }91+ return out92+}93+94+// partText writes what a part computed as a formula would write it: a95+// number plainly, text in quotes, an array in braces (its first rows and96+// columns, then …). A blank reads as 0, as it does in arithmetic.97+func partText(p functions.Part) string {98+ switch {99+ case p.Lambda:100+ return "LAMBDA"101+ case p.Array != nil:102+ return arrayText(p.Array)103+ }104+ return valueText(p.Value)105+}106+107+func valueText(v Value) string {108+ switch v.Kind {109+ case Empty:110+ return "0"111+ case Text:112+ return `"` + strings.ReplaceAll(v.Str, `"`, `""`) + `"`113+ }114+ return v.String()115+}116+117+func arrayText(a *functions.Array) string {118+ var b strings.Builder119+ b.WriteByte('{')120+ for r := range a.DRows {121+ if r > 0 {122+ b.WriteByte(';')123+ }124+ for c := range a.DCols {125+ if c > 0 {126+ b.WriteByte(',')127+ }128+ b.WriteString(valueText(a.At(r, c)))129+ }130+ if a.Cols > a.DCols {131+ b.WriteString(",…")132+ }133+ }134+ if a.Rows > a.DRows {135+ b.WriteString(";…")136+ }137+ b.WriteByte('}')138+ return b.String()139+}140+141+// pathKey is a path as a map key.142+func pathKey(path []int) string {143+ var b strings.Builder144+ for _, i := range path {145+ b.WriteString(strconv.Itoa(i))146+ b.WriteByte('.')147+ }148+ return b.String()149+}@@ -1,8 +1,8 @@
11 package sheet22 33 import (4- "cmp"54 "slices"5+ "strings"66 77 "github.com/FelineStateMachine/012/internal/formula"88 )@@ -10,7 +10,10 @@ import (
1010 // Tracing, as Excel's Trace Precedents and Trace Dependents: which cells1111 // a formula reads, and which formulas read a cell, on any sheet. Only1212 // direct links are followed; tracing again from a found cell goes a level13-// further.13+// further. The dependency indexes recalculation keeps answer both ways,14+// so tracing costs what it finds, not the sheet: a cell's precedents are15+// its formula's references, and its dependents come from the indexes by16+// cell, by range and by name (tracedeps.go).1417 1518 // Target is a traced cell or range and the sheet it is on.1619 type Target struct {@@ -18,61 +21,134 @@ type Target struct {
1821 Range Rect1922 }2023 24+// LinkKind is how a traced cell is linked to the one traced.25+type LinkKind uint826+27+const (28+ // LinkRef is a reference or range the formula writes out, or a29+ // formula reading the cell through one.30+ LinkRef LinkKind = iota31+ // LinkName is a named range the formula uses: Link.Name is its name.32+ LinkName33+ // LinkRegion is a region's table the formula names, nu.sales:34+ // Link.Name is how the formula names it.35+ LinkRegion36+ // LinkSpill is the array formula whose values the cell shows, among37+ // precedents, or the cells a formula spills into, among dependents.38+ LinkSpill39+ // LinkFile is the linked file a region's cells come from: Link.Name40+ // is its path, and Target the region's cells.41+ LinkFile42+ // LinkOutput is the notebook cell whose output a region shows:43+ // Link.Name is the cell's name, Target.Sheet the notebook tab and44+ // Target.Range.From.Row the cell's index there.45+ LinkOutput46+)47+48+// Link is a precedent or dependent: where it is, and how it's linked.49+type Link struct {50+ Target51+ Kind LinkKind52+ Name string // see LinkKind53+}54+55+// OnGrid reports whether the link is cells of a sheet's grid rather than56+// a notebook cell.57+func (l Link) OnGrid() bool { return !l.Sheet.IsNotebook() }58+2159 // Precedents returns the cells and ranges the formula at a reads, in the22-// order the formula mentions them, with named ranges resolved and23-// repeats dropped. References to sheets that don't exist are left out. It24-// is empty for anything but a formula.25-func (s *Sheet) Precedents(a Addr) []Target {26- c := s.cells.get(a)27- if c == nil || !c.IsFormula() {28- return nil29- }60+// order the formula mentions them; see PrecedentLinks.61+func (s *Sheet) Precedents(a Addr) []Target { return targetsOf(s.PrecedentLinks(a)) }62+63+// Dependents returns the formula cells that read a directly; see64+// DependentLinks.65+func (s *Sheet) Dependents(a Addr) []Target {66+ links, _ := s.DependentLinks(a, 0)67+ return targetsOf(links)68+}69+70+func targetsOf(links []Link) []Target {3071 var out []Target31- add := func(sheet string, r Rect) {32- t := Target{s.wb.resolve(s, sheet), r}33- if t.Sheet != nil && !slices.Contains(out, t) {34- out = append(out, t)72+ for _, l := range links {73+ if l.OnGrid() {74+ out = append(out, l.Target)3575 }3676 }37- formula.WalkRefs(s.bound(c), func(sheet string, a Addr) { add(sheet, Rect{From: a, To: a}) }, add)3877 return out3978 }4079 41-// Dependents returns the formula cells that read a directly, through a42-// reference, a range or a named range: those on this sheet first in43-// row-major order, then those on other sheets in tab order.44-func (s *Sheet) Dependents(a Addr) []Target {45- w := s.wb46- seen := map[loc]bool{}47- for d := range s.dependents[a] {48- seen[loc{s, d}] = true80+// PrecedentLinks returns what the cell at a comes from. For a formula,81+// the cells and ranges it reads in the order it mentions them, named82+// ranges and regions as their cells, with repeats dropped and references83+// to sheets that don't exist left out. For a cell an array spilled into,84+// the formula that spilled it; for a region's cell, the linked file or85+// notebook cell the region shows.86+func (s *Sheet) PrecedentLinks(a Addr) []Link {87+ var out []Link88+ add := func(l Link) {89+ if l.Sheet != nil && !slices.ContainsFunc(out, func(o Link) bool { return o.Target == l.Target && o.Kind == l.Kind }) {90+ out = append(out, l)91+ }92+ }93+ if c, _, _ := s.cells.peek(a); c != nil && c.IsFormula() {94+ s.walkPrecedents(c.expr, add)4995 }50- s.rangeUsers.readers(a, func(u Addr) { seen[loc{s, u}] = true })51- for k, users := range w.nameUsers {52- if n, ok := w.names[k]; ok && !n.Lost && n.Sheet == s && n.Range.Contains(a) {53- for u := range users {54- seen[u] = true96+ if anchor, ok := s.SpillAnchor(a); ok {97+ add(Link{Target: Target{s, Rect{From: anchor, To: anchor}}, Kind: LinkSpill})98+ }99+ if r, ok := s.RegionAt(a); ok {100+ add(s.regionSource(r))101+ }102+ return out103+}104+105+// walkPrecedents calls add with each reference, range and name in n, in106+// the order they're written.107+func (s *Sheet) walkPrecedents(n Node, add func(Link)) {108+ w := s.wb109+ switch n := n.(type) {110+ case formula.Ref:111+ add(Link{Target: Target{w.resolve(s, n.Sheet), Rect{From: n.Addr, To: n.Addr}}})112+ case formula.Range:113+ add(Link{Target: Target{w.resolve(s, n.Sheet), n.Rect}})114+ case formula.Name:115+ if nm, ok := w.names[nameKey(n.Name)]; ok {116+ if !nm.Gone() {117+ add(Link{Target: Target{nm.Sheet, nm.Range}, Kind: LinkName, Name: nm.Name})55118 }119+ return56120 }57- }58- for u := range w.crossUsers {59- if w.crossReads(u, loc{s, a}) {60- seen[u] = true121+ if t, r, ok := w.regionName(nameKey(n.Name)); ok && r != (Rect{}) {122+ add(Link{Target: Target{t, r}, Kind: LinkRegion, Name: s.regionSpelling(n.Name)})61123 }124+ default:125+ formula.EachChild(n, func(k Node) { s.walkPrecedents(k, add) })62126 }63- out := make([]Target, 0, len(seen))64- for l := range seen {65- out = append(out, Target{l.s, Rect{From: l.a, To: l.a}})127+}128+129+// regionSpelling is how formulas name the region a name like NU.SALES130+// finds: nu. and the region's own name.131+func (s *Sheet) regionSpelling(name string) string {132+ if _, r, ok := s.wb.Region(name[len(regionPrefix):]); ok {133+ return r.FormulaName()66134 }67- rank := func(t *Sheet) int {68- if t == s {69- return -1135+ return strings.ToLower(name)136+}137+138+// regionSource is where a region's cells come from: its linked file, or139+// the notebook cell whose output it shows (on the notebook's tab, when140+// there is one; otherwise the region itself).141+func (s *Sheet) regionSource(r Region) Link {142+ area := Target{s, s.covered(r)}143+ if r.Linked() {144+ return Link{Target: area, Kind: LinkFile, Name: r.File.Path}145+ }146+ for _, nb := range s.wb.sheets {147+ for i, c := range nb.regions.cells {148+ if strings.EqualFold(c.Name(), r.Name) {149+ return Link{Target: Target{nb, Rect{From: Addr{Row: i}, To: Addr{Row: i}}}, Kind: LinkOutput, Name: c.Name()}150+ }70151 }71- return w.Index(t)72152 }73- slices.SortFunc(out, func(x, y Target) int {74- return cmp.Or(cmp.Compare(rank(x.Sheet), rank(y.Sheet)),75- cmp.Compare(x.Range.From.Row, y.Range.From.Row), cmp.Compare(x.Range.From.Col, y.Range.From.Col))76- })77- return out153+ return Link{Target: area, Kind: LinkOutput, Name: r.Name}78154 }@@ -0,0 +1,101 @@
1+package sheet2+3+import (4+ "strings"5+ "testing"6+)7+8+// links writes links as they'd be listed: the range, with its sheet when9+// not s, and the kind's name where it isn't a plain reference.10+func links(s *Sheet, ls []Link) string {11+ var out []string12+ for _, l := range ls {13+ ref := l.Range.String()14+ if l.Sheet != s {15+ ref = Qualified(l.Sheet.Name(), l.Range)16+ }17+ switch l.Kind {18+ case LinkName, LinkRegion, LinkFile, LinkOutput:19+ ref = l.Name + "=" + ref20+ case LinkSpill:21+ ref = "spill " + ref22+ }23+ out = append(out, ref)24+ }25+ return strings.Join(out, " ")26+}27+28+func TestPrecedentLinks(t *testing.T) {29+ s := sheetOf(t, map[string]string{30+ "A1": "1", "A2": "2", "B1": "=SEQUENCE(3)", "C1": "=Rate*A1+SUM(B1:B3)", "C2": "=missing!A1+A1",31+ })32+ s.DefineName("Rate", rect("A2"))33+ if got, want := links(s, s.PrecedentLinks(at("C1"))), "Rate=A2 A1 B1:B3"; got != want {34+ t.Errorf("C1 = %q, want %q", got, want)35+ }36+ if got, want := links(s, s.PrecedentLinks(at("C2"))), "A1"; got != want {37+ t.Errorf("an unknown sheet: %q, want %q", got, want)38+ }39+ // A spilled cell comes from its anchor.40+ if got, want := links(s, s.PrecedentLinks(at("B3"))), "spill B1"; got != want {41+ t.Errorf("B3 = %q, want %q", got, want)42+ }43+ if got := s.PrecedentLinks(at("A1")); got != nil {44+ t.Errorf("a number: %v", got)45+ }46+}47+48+func TestDependentLinks(t *testing.T) {49+ s := sheetOf(t, map[string]string{50+ "A1": "=SEQUENCE(3)", "C1": "=A2*2", "C2": "=SUM(A1:A9)", "C3": "=A1", "D5": "=B1",51+ })52+ other, _ := s.Book().AddSheet("Other", 1)53+ other.Set(at("A1"), "=Sheet1!A3")54+ s.DefineName("Mid", rect("A2"))55+ other.Set(at("B1"), "=Mid")56+ // The anchor's dependents read any cell it spills into.57+ got, more := s.DependentLinks(at("A1"), 0)58+ if want := "spill A1:A3 C1 C2 C3 Other!A1 Other!B1"; links(s, got) != want || more {59+ t.Errorf("A1 = %q (more %v), want %q", links(s, got), more, want)60+ }61+ if got, _ := s.DependentLinks(at("A2"), 0); links(s, got) != "C1 C2 Other!B1" {62+ t.Errorf("A2 = %q", links(s, got))63+ }64+ // A cap finds that many and says there are more.65+ got, more = s.DependentLinks(at("A1"), 2)66+ if len(got) != 3 || !more {67+ t.Errorf("capped: %q, more %v", links(s, got), more)68+ }69+ if !s.Reads(at("C2"), s, rect("A3")) || !other.Reads(at("B1"), s, rect("A1:A2")) || s.Reads(at("D5"), s, rect("A1:A3")) {70+ t.Error("Reads")71+ }72+ if s.Reads(at("A2"), s, rect("A1")) {73+ t.Error("a spilled cell reads its anchor")74+ }75+}76+77+func TestTraceRegions(t *testing.T) {78+ s := New()79+ nb := sent(t, s, "files", "B2")80+ s.Set(at("A1"), "=ROWS(nu.files)")81+ apply(t, s, LiveOp{Region: "files", Reset: true, Header: liveRow("n"), Rows: []LiveRow{liveRow("a"), liveRow("b")}})82+ if got, want := links(s, s.PrecedentLinks(at("A1"))), "nu.files=B2:B4"; got != want {83+ t.Errorf("A1 = %q, want %q", got, want)84+ }85+ // The region's cells come from the notebook cell.86+ if got, want := links(s, s.PrecedentLinks(at("B3"))), "files="+Qualified(nb.Name(), rect("A1")); got != want {87+ t.Errorf("B3 = %q, want %q", got, want)88+ }89+ if got, _ := s.DependentLinks(at("B4"), 0); links(s, got) != "A1" {90+ t.Errorf("B4's dependents = %q", links(s, got))91+ }92+ if !s.Reads(at("A1"), s, rect("B3")) {93+ t.Error("Reads through a region")94+ }95+ if _, err := s.AddLinked(at("F1"), LinkSource{Path: "log.csv"}); err != nil {96+ t.Fatal(err)97+ }98+ if got, want := links(s, s.PrecedentLinks(at("F1"))), "log.csv=F1"; got != want {99+ t.Errorf("F1 = %q, want %q", got, want)100+ }101+}@@ -0,0 +1,170 @@
1+package sheet2+3+import (4+ "cmp"5+ "slices"6+)7+8+// Dependents, for tracing (trace.go): the formulas reading a cell, found9+// through the indexes recalculation keeps, and Reads, which asks the10+// same of one formula, so what draws the sheet can mark the dependents it11+// shows without listing them all.12+13+// DependentLinks returns the formulas that read the cell at a directly:14+// through a reference, a range, a named range or a region's name. When15+// the formula at a spills, the cells it spills into come first, and the16+// formulas reading any of them count too. Formulas on this sheet come in17+// row-major order, then those on other sheets in tab order. With max > 018+// at most max formulas are found, and more reports that there were19+// others: a cell read by a million formulas costs what max does.20+func (s *Sheet) DependentLinks(a Addr, max int) (links []Link, more bool) {21+ subject := Rect{From: a, To: a}22+ if area, ok := s.SpillArea(a); ok && area != subject {23+ subject = area24+ links = append(links, Link{Target: Target{s, area}, Kind: LinkSpill})25+ }26+ f := &depFinder{max: max, seen: map[loc]struct{}{}}27+ s.readersOf(subject, f)28+ found := make([]Link, 0, len(f.seen))29+ for l := range f.seen {30+ found = append(found, Link{Target: Target{l.s, Rect{From: l.a, To: l.a}}})31+ }32+ w := s.wb33+ rank := func(t *Sheet) int {34+ if t == s {35+ return -136+ }37+ return w.Index(t)38+ }39+ slices.SortFunc(found, func(x, y Link) int {40+ return cmp.Or(cmp.Compare(rank(x.Sheet), rank(y.Sheet)),41+ cmp.Compare(x.Range.From.Row, y.Range.From.Row), cmp.Compare(x.Range.From.Col, y.Range.From.Col))42+ })43+ return append(links, found...), f.more44+}45+46+// depFinder collects dependents, up to max when it's positive.47+type depFinder struct {48+ max int49+ seen map[loc]struct{}50+ more bool51+}52+53+func (f *depFinder) add(l loc) {54+ if _, ok := f.seen[l]; ok || f.more {55+ return56+ }57+ if f.max > 0 && len(f.seen) >= f.max {58+ f.more = true59+ return60+ }61+ f.seen[l] = struct{}{}62+}63+64+// readersOf adds the formulas that read any cell of r, on s.65+func (s *Sheet) readersOf(r Rect, f *depFinder) {66+ s.refReaders(r, f)67+ s.rangeUsers.overlapping(r, func(u Addr) { f.add(loc{s, u}) })68+ w := s.wb69+ for k, users := range w.nameUsers {70+ if f.more || !w.namesOverlap(k, s, r) {71+ continue72+ }73+ for u := range users {74+ f.add(u)75+ }76+ }77+ for u := range w.crossUsers {78+ if f.more {79+ return80+ }81+ if c := u.s.cells.get(u.a); c != nil && w.xrefsOverlap(c, s, r) {82+ f.add(u)83+ }84+ }85+}86+87+// refReaders adds the formulas on s with a single-cell reference into88+// r, walking whichever is smaller: r's cells or the cells referenced.89+func (s *Sheet) refReaders(r Rect, f *depFinder) {90+ add := func(users map[Addr]struct{}) {91+ for u := range users {92+ f.add(loc{s, u})93+ }94+ }95+ if n := (r.To.Row - r.From.Row + 1) * (r.To.Col - r.From.Col + 1); n <= len(s.dependents) {96+ for row := r.From.Row; row <= r.To.Row && !f.more; row++ {97+ for col := r.From.Col; col <= r.To.Col; col++ {98+ add(s.dependents[Addr{Col: col, Row: row}])99+ }100+ }101+ return102+ }103+ for a, users := range s.dependents {104+ if f.more {105+ return106+ }107+ if r.Contains(a) {108+ add(users)109+ }110+ }111+}112+113+// namesOverlap reports whether the name with key k, a named range or a114+// region's (nu.sales), stands for cells of r on s.115+func (w *Workbook) namesOverlap(k string, s *Sheet, r Rect) bool {116+ if nm, ok := w.names[k]; ok {117+ _, over := intersectRect(nm.Range, r)118+ return !nm.Gone() && nm.Sheet == s && over119+ }120+ t, table, ok := w.regionName(k)121+ if !ok || t != s || table == (Rect{}) {122+ return false123+ }124+ _, over := intersectRect(table, r)125+ return over126+}127+128+// xrefsOverlap reports whether c reads cells of r on s through a129+// reference that names s.130+func (w *Workbook) xrefsOverlap(c *Cell, s *Sheet, r Rect) bool {131+ for _, x := range c.xrefs {132+ if _, over := intersectRect(x.r, r); over && w.byKey[x.key] == s {133+ return true134+ }135+ }136+ return false137+}138+139+// Reads reports whether the formula at u, on s, reads any cell of r on140+// t directly: through a reference, a range, a named range or a region's141+// name. It costs the formula's references, so asking it of every cell142+// on screen is cheap.143+func (s *Sheet) Reads(u Addr, t *Sheet, r Rect) bool {144+ c, _, _ := s.cells.peek(u)145+ if c == nil || !c.IsFormula() {146+ return false147+ }148+ w := s.wb149+ if t == s {150+ for _, a := range c.refs {151+ if r.Contains(a) {152+ return true153+ }154+ }155+ for _, g := range c.ranges {156+ if _, over := intersectRect(g, r); over {157+ return true158+ }159+ }160+ }161+ if w.xrefsOverlap(c, t, r) {162+ return true163+ }164+ for _, k := range c.names {165+ if w.namesOverlap(k, t, r) {166+ return true167+ }168+ }169+ return false170+}