cat / 012
clone

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

main
012 / internal / functions / streamed.go
streamed.go 6.1K
1package functions23import "src.rest/cat/012/internal/value"45// Functions over aligned ranges computing a StreamCall (stream.go). On6// a sheet they gather the positions of the cells their ranges hold and7// visit those (masked.go), which over a source of ten million rows8// would hold ten million positions. Computing a StreamCall they read9// their ranges instead a window of rows at a time, every cell of the10// window's rows, blank or not, testing and adding as they go, so they11// hold one window whatever the source's size. Blank cells meet12// criteria or add nothing as they would when accounted for at once, so13// the results are the same.1415// streamRows is how many rows of each range a window holds.16const streamRows = 20481718// streamable reports whether same-sized matrices are read a window at19// a time: computing a StreamCall, every one a range of a sheet that20// exists.21func streamable(get lookup, ms ...matrix) bool {22 if !get.stream {23 return false24 }25 for _, m := range ms {26 if !m.ref || m.blank.Kind == value.Error {27 return false28 }29 }30 return true31}3233// windows calls fn with each window of the rows of same-sized ranges34// that may hold data, the values of each range's window row by row (the35// slices are reused), until fn returns false. It returns how many cells36// of each range the windows cover; the rest are blank in all of them.37func windows(get lookup, ms []matrix, fn func(vals [][]Value) bool) int {38 rows, cols := 0, ms[0].cols39 for _, m := range ms {40 rows = max(rows, min(m.dataRows, m.rows))41 }42 bufs := make([][]Value, len(ms))43 for r0 := 0; r0 < rows; r0 += streamRows {44 n := min(streamRows, rows-r0)45 for i, m := range ms {46 bufs[i] = m.window(get, r0, n, bufs[i])47 }48 if !fn(bufs) {49 break50 }51 }52 return rows * cols53}5455// window reads n rows of the range m from its row r0 into buf, every56// cell of them, blanks as blank values.57func (m matrix) window(get lookup, r0, n int, buf []Value) []Value {58 size := n * m.cols59 if cap(buf) < size {60 buf = make([]Value, size)61 }62 buf = buf[:size]63 clear(buf)64 top := m.origin.Row + r065 r := Rect{From: Addr{Col: m.origin.Col, Row: top}, To: Addr{Col: m.origin.Col + m.cols - 1, Row: top + n - 1}}66 get.cells(m.sheet, r, func(a Addr, v Value) bool {67 buf[(a.Row-top)*m.cols+a.Col-m.origin.Col] = v68 return true69 })70 return buf71}7273// passAll reports whether the cell at i of each window meets its74// criterion.75func passAll(vals [][]Value, cs []criterion, i int) bool {76 for j, c := range cs {77 if !c.test(vals[j][i]) {78 return false79 }80 }81 return true82}8384// blanksPass reports whether blank cells meet every criterion.85func blanksPass(cs []criterion) bool {86 for _, c := range cs {87 if !c.test(Value{}) {88 return false89 }90 }91 return true92}9394// countStream is COUNTIFS over windows.95func countStream(get lookup, ms []matrix, cs []criterion) Value {96 n := 097 covered := windows(get, ms, func(vals [][]Value) bool {98 for i := range vals[0] {99 if passAll(vals, cs, i) {100 n++101 }102 }103 return true104 })105 if blanksPass(cs) {106 n += ms[0].size() - covered107 }108 return num(float64(n))109}110111// sumStream calls add with the numbers of val where the criteria on ms112// pass, over windows, or returns the first error there.113func sumStream(get lookup, val matrix, ms []matrix, cs []criterion, add func(float64)) *Value {114 var err *Value115 windows(get, append([]matrix{val}, ms...), func(vals [][]Value) bool {116 for i, v := range vals[0] {117 if !passAll(vals[1:], cs, i) {118 continue119 }120 switch v.Kind {121 case value.Error:122 err = &v123 return false124 case value.Number:125 add(v.Num)126 }127 }128 return true129 })130 return err131}132133// averageStream is AVERAGEIFS over windows.134func averageStream(get lookup, val matrix, ms []matrix, cs []criterion) Value {135 sum, n := 0.0, 0136 if e := sumStream(get, val, ms, cs, func(f float64) { sum += f; n++ }); e != nil {137 return *e138 }139 if n == 0 {140 return value.ErrDiv0141 }142 return num(sum / float64(n))143}144145// blankStream is COUNTBLANK over windows.146func blankStream(get lookup, m matrix) Value {147 n := 0148 covered := windows(get, []matrix{m}, func(vals [][]Value) bool {149 for _, v := range vals[0] {150 if v.Kind == value.Empty || v.Kind == value.Text && v.Str == "" {151 n++152 }153 }154 return true155 })156 return num(float64(n + m.size() - covered))157}158159// productStream calls term with the factors of each entry of160// same-sized ranges, over windows, as sumProductTerms does.161func productStream(get lookup, ms []matrix, term func(fs []float64)) *Value {162 var err *Value163 fs := make([]float64, len(ms))164 windows(get, ms, func(vals [][]Value) bool {165 for i := range vals[0] {166 for j := range ms {167 switch v := vals[j][i]; v.Kind {168 case value.Error:169 err = &v170 return false171 case value.Number, value.Bool:172 fs[j] = v.Num173 default:174 fs[j] = 0175 }176 }177 term(fs)178 }179 return true180 })181 return err182}183184// seekStream is XLOOKUP's search of a sparse line computing a185// StreamCall: one pass over the cells it holds, forwards whichever way186// the search goes, keeping what the search in order would find (the187// first exact match, or the last searching backwards; else the closest188// value, the first or last of equals) without listing the entries. ok189// is false when a blank would match the key, which the search in order190// handles.191func (q *seq) seekStream(key Value, mode int, reverse bool) (int, bool) {192 wild := mode == 2193 if lookupEqual(key, q.blank, wild) {194 return 0, false195 }196 exact, best := -1, -1197 var bestV Value198 c := q.walk()199 defer c.close()200 for a, ok := c.next(); ok; a, ok = c.next() {201 i, v := q.index(a), q.value(a)202 switch {203 case lookupEqual(key, v, wild):204 if !reverse {205 return i, true206 }207 exact = i208 case exact >= 0 || mode == 0 || wild || v.Kind != key.Kind:209 case compare(v, key)*mode > 0 && (best < 0 || closer(v, bestV, mode, reverse)):210 best, bestV = i, v211 }212 }213 if exact >= 0 {214 return exact, true215 }216 return best, true217}218219// closer reports whether v is closer to the key than the best so far,220// in XLOOKUP's next smaller (-1) or next larger (1) mode: equals keep221// the first met, or the last searching backwards.222func closer(v, best Value, mode int, reverse bool) bool {223 d := compare(v, best) * mode224 return d < 0 || reverse && d == 0225}