cat / 012
clone

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

cat/module-path
012 / internal / sheet / stepstext.go
stepstext.go 3.9K
1package sheet23import (4 "slices"5 "strconv"6 "strings"78 "src.rest/cat/012/internal/formula"9 "src.rest/cat/012/internal/functions"10)1112// 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.1819// Segment is a piece of the formula Steps shows.20type 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}2728// Markers in the printed formula: a value, \x01 and its index and \x02,29// and the part computed next, \x03.30const (31 valueOpen = "\x01"32 valueClose = "\x02"33 nextMark = "\x03"34)3536// 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.39func (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}6566// inside reports whether path is within one of paths.67func 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}7576// appendValues splits text at its value markers into segments.77func 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}9394// 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.97func 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}106107func 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}116117func 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}140141// pathKey is a path as a map key.142func 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}