cat / 012
clone

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

main
012 / internal / sheet / sourceask.go
sourceask.go 6.9K
1package sheet23import (4 "fmt"5 "strings"67 "src.rest/cat/012/internal/functions"8)910// What formulas and pivot tables ask of linked sources (source.go). The11// engine never reads a file: it describes each question and asks the12// workbook's SourceHost, which answers from what it keeps and queues13// the rest, worked out in the background by streaming the source (the14// UI's; headless answers at once). Until an answer arrives the cell15// shows Loading…, as a JEV question does, and when answers arrive the16// UI calls RecalcSourced, which recomputes what waited for them. A17// source's answers are kept until its file changes (SourceChanged).1819// SourceHost answers the questions of a workbook's formulas and pivot20// tables about its linked sources.21type SourceHost interface {22 // Answer returns the answer to q, or false while it isn't known,23 // queuing q to be worked out.24 Answer(q SourceQuestion) (SourceAnswer, bool)25}2627// AskKind is what a SourceQuestion asks.28type AskKind uint82930const (31 // AskCell asks a cell's value: At, on the source's tab.32 AskCell AskKind = iota33 // AskRange asks the values of a range, R, as an array: read whole,34 // so no bigger than the host's budget.35 AskRange36 // AskCall asks what a function computes, Call, its ranges named by37 // their sources' names.38 AskCall39 // AskPivot asks a pivot table's groups over the source, Pivot.40 AskPivot41)4243// SourceQuestion is one question about a linked source. Source is the44// source's name, the one whose file changing makes the answer stale; a45// call may read others too, named in its arguments.46type SourceQuestion struct {47 Kind AskKind48 Source string49 At Addr50 R Rect51 Call functions.StreamCall52 Pivot Pivot53}5455// Key identifies the question by its content.56func (q SourceQuestion) Key() string {57 switch q.Kind {58 case AskCell:59 return fmt.Sprintf("cell\x1f%s\x1f%d:%d", nameKey(q.Source), q.At.Col, q.At.Row)60 case AskRange:61 return fmt.Sprintf("range\x1f%s\x1f%d:%d:%d:%d", nameKey(q.Source), q.R.From.Col, q.R.From.Row, q.R.To.Col, q.R.To.Row)62 case AskPivot:63 return "pivot\x1f" + nameKey(q.Source) + "\x1f" + pivotKey(q.Pivot)64 }65 return "call\x1f" + q.Call.Key()66}6768// Sources names every source the question reads.69func (q SourceQuestion) Sources() []string {70 if q.Kind != AskCall {71 return []string{q.Source}72 }73 var out []string74 for _, a := range q.Call.Args {75 if a.Sheet != "" && !containsFold(out, a.Sheet) {76 out = append(out, a.Sheet)77 }78 }79 return out80}8182func containsFold(list []string, s string) bool {83 for _, x := range list {84 if strings.EqualFold(x, s) {85 return true86 }87 }88 return false89}9091// SourceAnswer is what a SourceQuestion is answered: a value, an array92// of them, or a pivot table's groups; for an error that needs one, why.93type SourceAnswer struct {94 V Value95 A *functions.Array96 Why string97 Pivot *PivotGroups98}99100// SetSources sets what answers the workbook's questions about linked101// sources, nil for nothing (they then say so), and recomputes what102// reads them.103func (w *Workbook) SetSources(h SourceHost) {104 w.sources = h105 w.recalc(w.src.all())106}107108// sourceAsks keeps who asked what of linked sources: the formulas that109// asked each source anything, so its file changing recalculates them,110// the formulas and pivot sheets waiting for each question's answer, and111// why each formula whose answer was an error shows it.112type sourceAsks struct {113 users map[string]map[loc]struct{} // by source key114 waiting map[string]map[loc]struct{} // by question key115 pivots map[string][]*Sheet // by question key116 why map[loc]string117}118119// all is every formula that asked a source something.120func (a *sourceAsks) all() []loc {121 var out []loc122 for _, set := range a.users {123 for l := range set {124 out = append(out, l)125 }126 }127 return out128}129130// errNoSources is what a formula reading a source shows when nothing131// answers them.132var errNoSources = ErrNA133134// ask looks up the answer to q for the formula being evaluated, noting135// that it read the sources q reads and, when the answer isn't known,136// that it waits for it.137func (w *Workbook) ask(q SourceQuestion) (SourceAnswer, bool) {138 l := w.evaluating139 if l.s != nil {140 for _, src := range q.Sources() {141 addLoc(&w.src.users, nameKey(src), l)142 }143 delete(w.src.why, l)144 }145 if w.sources == nil {146 return SourceAnswer{V: errNoSources, Why: "Nothing reads linked sources here"}, true147 }148 ans, ok := w.sources.Answer(q)149 switch {150 case !ok && l.s != nil:151 addLoc(&w.src.waiting, q.Key(), l)152 case ok && ans.Why != "" && l.s != nil:153 if w.src.why == nil {154 w.src.why = map[loc]string{}155 }156 w.src.why[l] = ans.Why157 }158 return ans, ok159}160161func addLoc(m *map[string]map[loc]struct{}, k string, l loc) {162 if *m == nil {163 *m = map[string]map[loc]struct{}{}164 }165 if (*m)[k] == nil {166 (*m)[k] = map[loc]struct{}{}167 }168 (*m)[k][l] = struct{}{}169}170171// RecalcSourced recomputes the formulas and pivot tables that were172// waiting for the answers to the questions with keys, and what depends173// on them. It doesn't touch the undo history.174func (w *Workbook) RecalcSourced(keys []string) {175 var changed []loc176 for _, k := range keys {177 for l := range w.src.waiting[k] {178 if l.s.live && l.s.cells.has(l.a) {179 changed = append(changed, l)180 }181 }182 delete(w.src.waiting, k)183 for _, p := range w.src.pivots[k] {184 if p.live {185 p.pivot.stale = true186 }187 }188 delete(w.src.pivots, k)189 }190 w.recalcFrom(changed, false)191}192193// sourceFailure says why the formula c at l shows an error that a194// linked source gave it: the answer's reason, or why a source it names195// can't be read.196func (s *Sheet) sourceFailure(l loc, c *Cell) string {197 if why := s.wb.src.why[l]; why != "" {198 return why199 }200 for _, k := range c.names {201 k = strings.TrimPrefix(k, nameKey(regionPrefix))202 if info, ok := s.wb.LookupSource(k); ok && info.Err != "" {203 return info.Name + ": " + info.Err204 }205 }206 return ""207}208209// paged is the paged sheet a reference names from the formula's sheet,210// with its source, or nil.211func (rd *reader) paged(sheet string) (*Sheet, Region) {212 if sheet == "" {213 return nil, Region{}214 }215 t := rd.sheet(sheet)216 if t == nil {217 return nil, Region{}218 }219 if r, ok := t.pagedRegion(); ok {220 return t, r221 }222 return nil, Region{}223}224225// Paged reports whether a sheet is a linked source's tab.226func (rd *reader) Paged(sheet string) bool {227 t, _ := rd.paged(sheet)228 return t != nil229}230231// Stream answers a call over linked sources, its ranges renamed by232// their sources' names so the answer stays the same whatever the tabs233// are called.234func (rd *reader) Stream(call functions.StreamCall) (Value, *functions.Array) {235 q := SourceQuestion{Kind: AskCall, Call: call}236 q.Call.Args = append([]functions.StreamArg(nil), call.Args...)237 for i, a := range q.Call.Args {238 if a.Sheet == "" {239 continue240 }241 t, r := rd.paged(a.Sheet)242 if t == nil {243 return ErrRef, nil244 }245 q.Call.Args[i].Sheet = r.Name246 if q.Source == "" {247 q.Source = r.Name248 }249 }250 ans, ok := rd.w.ask(q)251 if !ok {252 return Pending, nil253 }254 return ans.V, ans.A255}