cat / 012
clone

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

Move the function library into internal/functions

Dami committed 46e1e3a
The FuncDef table, formula evaluation, the argument, range and
criteria helpers, decimal arithmetic and the JEV questions move out of
the engine. Functions read cells through a Reader over a small Book
interface the engine implements (Cell, Scan, Bounds, RangeAgg, Ask);
the engine finds functions through the table as before, and names
FuncDef, the remote types and the functions' errors through aliases.

An interface taking a callback would put every range read's callback
on the heap, so Book passes only values: ranges are read in chunks
into buffers the Reader keeps and reuses (Scan), and functions walk
them with callbacks that stay on the stack. A chunk evaluates no cell
past the first error, and lookups scan small chunks first, so no cell
is evaluated or visited that reading one at a time wouldn't have. The
engine fills a chunk from the occupancy index without a call per cell
(colFill), and keeps each sheet's reader between recalculations so the
buffers are made once.

docs/functions.md is generated from the new package with the same
categories and output.
commit
46e1e3a812c763df268fd54fe789fc06d7d5c1f3
parent
cb54b970fafa98bf188a6acebce6bce9e6f091c8
author
Dami <dami.etoile@gmail.com>
36 files changed, +1595 -950
Rinternal/sheet/args.go → internal/functions/args.go61+++---
Rinternal/sheet/functions_date.go → internal/functions/date.go40++--
Ainternal/functions/decimal.go317++++++++++++++++++++++++++++++
Rinternal/sheet/functions_doc_test.go → internal/functions/doc_test.go32++--
Rinternal/sheet/eval.go → internal/functions/eval.go45++--
Rinternal/sheet/functions.go → internal/functions/everyday.go87+++++---
Rinternal/sheet/functions_finance.go → internal/functions/finance.go26+-
Ainternal/functions/format.go100+++++++++
Rinternal/sheet/functions_jev.go → internal/functions/jev.go48++--
Ainternal/functions/link.go25++
Rinternal/sheet/functions_logic.go → internal/functions/logic.go30++-
Rinternal/sheet/functions_lookup.go → internal/functions/lookup.go194++----------------
Rinternal/sheet/masked.go → internal/functions/masked.go49++--
Rinternal/sheet/functions_math.go → internal/functions/math.go21+-
Ainternal/functions/reader.go165++++++++++++++++
Ainternal/functions/reader_test.go188++++++++++++++++++
Ainternal/functions/remote.go46++++
Rinternal/sheet/remotekey.go → internal/functions/remotekey.go2+-
Ainternal/functions/seq.go191++++++++++++++++++
Rinternal/sheet/functions_stats.go → internal/functions/stats.go14+-
Rinternal/sheet/functions_text.go → internal/functions/text.go41++--
Ainternal/functions/values.go41++++
Minternal/sheet/cellformat.go96---------
Minternal/sheet/decimal.go318+------------------------------
Minternal/sheet/evaluate.go9+-
Minternal/sheet/explain.go13+-
Minternal/sheet/formula.go16+-
Minternal/sheet/functions_test.go8-
Rinternal/sheet/functions_link.go → internal/sheet/link.go23+--
Minternal/sheet/occupancy.go30+++
Minternal/sheet/rangememo.go30++-
Minternal/sheet/recalc.go132+++++++++---
Minternal/sheet/remote.go52++---
Minternal/sheet/sheet.go8+-
Minternal/sheet/store.go28+++
Minternal/sheet/values.go19+-
R internal/sheet/args.go → internal/functions/args.go +29 -32
@@ -1,10 +1,11 @@
1-package sheet1+package functions22 33 import (44 "strings"55 "unicode/utf8"66 77 "github.com/FelineStateMachine/012/internal/formula"8+ "github.com/FelineStateMachine/012/internal/value"89 )910 1011 // Argument helpers shared by the function library. Errors come back as a
@@ -13,7 +14,7 @@ import (
1314 1415 func numArg(n Node, get lookup) (float64, *Value) {1516 v := eval(n, get)16- if v.Kind == Error {17+ if v.Kind == value.Error {1718 return 0, errOf(v)1819 }1920 return toNum(v)
@@ -44,14 +45,14 @@ func intArg(args []Node, i int, def float64, get lookup) (int, *Value) {
4445 return 0, err4546 }4647 if f > 1e9 || f < -1e9 {47- return 0, &ErrNum48+ return 0, &value.ErrNum4849 }4950 return int(f), nil5051 }5152 5253 func textArg(n Node, get lookup) (string, *Value) {5354 v := eval(n, get)54- if v.Kind == Error {55+ if v.Kind == value.Error {5556 return "", errOf(v)5657 }5758 return text(v), nil
@@ -113,15 +114,13 @@ func rectMatrix(sheet string, r Rect, get lookup) matrix {
113114 rows: r.To.Row - r.From.Row + 1, cols: r.To.Col - r.From.Col + 1,114115 origin: r.From, sheet: sheet, ref: true,115116 }116- switch t := get.sheet(sheet); {117- case t == nil:118- m.blank = ErrRef119- case denseReads:117+ switch b, any, exists := get.book.Bounds(sheet, r); {118+ case !exists:119+ m.blank = value.ErrRef120+ case get.dense:120121 m.dataRows, m.dataCols = m.rows, m.cols121- default:122- if b, ok := t.cells.bounds(r); ok {123- m.dataRows, m.dataCols = b.To.Row-r.From.Row+1, b.To.Col-r.From.Col+1124- }122+ case any:123+ m.dataRows, m.dataCols = b.To.Row-r.From.Row+1, b.To.Col-r.From.Col+1125124 }126125 rows, cols, blank := m.dataRows, m.dataCols, m.blank127126 m.cell = func(row, col int) Value {
@@ -149,9 +148,9 @@ func nums(args []Node, get lookup) ([]float64, *Value) {
149148 var out []float64150149 e := each(args, get, func(v Value, direct bool) *Value {151150 switch {152- case v.Kind == Error:151+ case v.Kind == value.Error:153152 return errOf(v)154- case v.Kind == Empty, v.Kind != Number && !direct:153+ case v.Kind == value.Empty, v.Kind != value.Number && !direct:155154 return nil156155 }157156 f, err := toNum(v)
@@ -169,7 +168,7 @@ func nums(args []Node, get lookup) ([]float64, *Value) {
169168 func texts(args []Node, get lookup) ([]string, *Value) {170169 var out []string171170 e := each(args, get, func(v Value, _ bool) *Value {172- if v.Kind == Error {171+ if v.Kind == value.Error {173172 return errOf(v)174173 }175174 out = append(out, text(v))
@@ -192,7 +191,7 @@ func textsWithBlanks(args []Node, get lookup, limit int) ([]string, *Value) {
192191 rn, ok := arg.(formula.Range)193192 if !ok {194193 if e := eachOf(arg, get, func(v Value, _ bool) *Value {195- if v.Kind == Error {194+ if v.Kind == value.Error {196195 return errOf(v)197196 }198197 out = append(out, text(v))
@@ -206,7 +205,7 @@ func textsWithBlanks(args []Node, get lookup, limit int) ([]string, *Value) {
206205 width := r.To.Col - r.From.Col + 1207206 var e *Value208207 get.cells(rn.Sheet, r, func(a Addr, v Value) bool {209- if v.Kind == Error {208+ if v.Kind == value.Error {210209 e = errOf(v)211210 return false212211 }
@@ -223,8 +222,6 @@ func textsWithBlanks(args []Node, get lookup, limit int) ([]string, *Value) {
223222 return out, nil224223 }225224 226-func str(s string) Value { return Value{Kind: Text, Str: s} }227-228225 func runeLen(s string) int { return utf8.RuneCountInString(s) }229226 230227 // wildMatch matches s against a Sheets wildcard pattern, case-insensitively:
@@ -264,15 +261,15 @@ func hasWildcards(s string) bool { return strings.ContainsAny(s, "*?~") }
264261 // criterion is a condition of SUMIF, COUNTIFS and friends: 5, ">5",265262 // "<>done", "a*", "" (blank) or TRUE.266263 type criterion struct {267- op string // =, <>, <, >, <=, >=268- kind Kind // Number, Text, Bool or Empty (blank)264+ op string // =, <>, <, >, <=, >=265+ kind value.Kind // Number, Text, Bool or Empty (blank)269266 num float64270267 text string271268 }272269 273270 func newCriterion(v Value) criterion {274271 c := criterion{op: "=", kind: v.Kind, num: v.Num}275- if v.Kind == Text {272+ if v.Kind == value.Text {276273 s := v.Str277274 for _, op := range []string{"<=", ">=", "<>", "<", ">", "="} {278275 if rest, ok := strings.CutPrefix(s, op); ok {
@@ -281,13 +278,13 @@ func newCriterion(v Value) criterion {
281278 }282279 }283280 c.text = s284- switch n, _, isNum := ParseValue(s); {281+ switch n, _, isNum := value.ParseValue(s); {285282 case s == "":286- c.kind = Empty283+ c.kind = value.Empty287284 case isNum:288- c.kind, c.num = Number, n285+ c.kind, c.num = value.Number, n289286 case strings.EqualFold(s, "TRUE") || strings.EqualFold(s, "FALSE"):290- c.kind, c.num = Bool, 0287+ c.kind, c.num = value.Bool, 0291288 if strings.EqualFold(s, "TRUE") {292289 c.num = 1293290 }
@@ -298,8 +295,8 @@ func newCriterion(v Value) criterion {
298295 299296 // test reports whether a cell value satisfies the criterion.300297 func (c criterion) test(v Value) bool {301- blank := v.Kind == Empty || v.Kind == Text && v.Str == ""302- if c.kind == Empty {298+ blank := v.Kind == value.Empty || v.Kind == value.Text && v.Str == ""299+ if c.kind == value.Empty {303300 switch c.op {304301 case "=":305302 return blank
@@ -308,8 +305,8 @@ func (c criterion) test(v Value) bool {
308305 }309306 return false310307 }311- if c.kind == Text {312- if v.Kind != Text {308+ if c.kind == value.Text {309+ if v.Kind != value.Text {313310 return c.op == "<>"314311 }315312 switch c.op {
@@ -358,11 +355,11 @@ func criteriaArgs(args []Node, i int, get lookup) ([]matrix, []criterion, *Value
358355 for ; i+1 < len(args); i += 2 {359356 m := matrixArg(args[i], get)360357 cv := eval(args[i+1], get)361- if cv.Kind == Error {358+ if cv.Kind == value.Error {362359 return nil, nil, &cv363360 }364361 if len(ms) > 0 && (m.rows != ms[0].rows || m.cols != ms[0].cols) {365- return nil, nil, &ErrValue362+ return nil, nil, &value.ErrValue366363 }367364 ms, cs = append(ms, m), append(cs, newCriterion(cv))368365 }
R internal/sheet/functions_date.go → internal/functions/date.go +20 -20
@@ -1,4 +1,4 @@
1-package sheet1+package functions22 33 import (44 "math"
@@ -9,9 +9,9 @@ import (
99 )1010 1111 var (12- dateFormat = returns(Preset(FmtDate))13- timeFormat = returns(Preset(FmtTime))14- dateTimeFormat = returns(Preset(FmtDateTime))12+ dateFormat = returns(value.Preset(value.FmtDate))13+ timeFormat = returns(value.Preset(value.FmtTime))14+ dateTimeFormat = returns(value.Preset(value.FmtDateTime))1515 )1616 1717 func init() {
@@ -63,11 +63,11 @@ func date(x []float64) Value {
6363 y += 1900 // DATE(26, 1, 1) is 1926, as in Sheets6464 }6565 if y < 0 || y >= 10000 || math.Abs(m) > 1e6 || math.Abs(d) > 1e8 {66- return ErrNum66+ return value.ErrNum6767 }6868 v := numfmt.DateSerial(int(y), int(m), int(d))6969 if v < 0 {70- return ErrNum70+ return value.ErrNum7171 }7272 return num(v)7373 }
@@ -75,7 +75,7 @@ func date(x []float64) Value {
7575 func timeOfDay(x []float64) Value {7676 secs := math.Trunc(x[0])*3600 + math.Trunc(x[1])*60 + math.Trunc(x[2])7777 if secs < 0 {78- return ErrNum78+ return value.ErrNum7979 }8080 return num(math.Mod(secs, 86400) / 86400)8181 }
@@ -99,7 +99,7 @@ func weekday(args []Node, get lookup) Value {
9999 case 3:100100 return num(float64((wd + 6) % 7))101101 }102- return ErrNum102+ return value.ErrNum103103 }104104 105105 func days(args []Node, get lookup) Value {
@@ -120,8 +120,8 @@ func dateValue(args []Node, get lookup) Value {
120120 return *ok121121 }122122 d, f, parsed := value.ParseDateTime(v)123- if !parsed || f.Kind == FmtTime || f.Kind == FmtDuration {124- return ErrValue123+ if !parsed || f.Kind == value.FmtTime || f.Kind == value.FmtDuration {124+ return value.ErrValue125125 }126126 return num(math.Floor(d))127127 }
@@ -134,19 +134,19 @@ func timeValue(args []Node, get lookup) Value {
134134 if d, _, parsed := value.ParseDateTime(v); parsed {135135 return num(d - math.Floor(d))136136 }137- return ErrValue137+ return value.ErrValue138138 }139139 140140 // parsedText requires a text argument, as DATEVALUE and TIMEVALUE do.141141 func parsedText(n Node, get lookup) (string, *Value) {142142 v := eval(n, get)143143 switch v.Kind {144- case Error:144+ case value.Error:145145 return "", errOf(v)146- case Text:146+ case value.Text:147147 return v.Str, nil148148 }149- return "", &ErrValue149+ return "", &value.ErrValue150150 }151151 152152 // dateArg is a date argument: a serial number or text such as
@@ -157,7 +157,7 @@ func dateArg(n Node, get lookup) (float64, *Value) {
157157 return 0, err158158 }159159 if d < 0 {160- return 0, &ErrNum160+ return 0, &value.ErrNum161161 }162162 return d, nil163163 }
@@ -200,7 +200,7 @@ func monthShift(endOfMonth bool) func([]Node, lookup) Value {
200200 y, m, day := numfmt.Civil(int64(math.Floor(d)))201201 y, m = numfmt.AddMonths(y, m, months)202202 if y < 1 || y > 9999 {203- return ErrNum203+ return value.ErrNum204204 }205205 last := numfmt.DaysIn(y, m)206206 if endOfMonth || day > last {
@@ -208,7 +208,7 @@ func monthShift(endOfMonth bool) func([]Node, lookup) Value {
208208 }209209 v := numfmt.DateSerial(y, m, day)210210 if v < 0 {211- return ErrNum211+ return value.ErrNum212212 }213213 return num(v)214214 }
@@ -229,7 +229,7 @@ func datedif(args []Node, get lookup) Value {
229229 }230230 s, e := int64(math.Floor(start)), int64(math.Floor(end))231231 if s > e {232- return ErrNum232+ return value.ErrNum233233 }234234 sy, sm, sd := numfmt.Civil(s)235235 ey, em, ed := numfmt.Civil(e)
@@ -265,7 +265,7 @@ func datedif(args []Node, get lookup) Value {
265265 anniv := numfmt.DateSerial(y, sm, min(sd, numfmt.DaysIn(y, sm)))266266 return num(float64(e) - anniv)267267 }268- return ErrNum268+ return value.ErrNum269269 }270270 271271 func networkdays(args []Node, get lookup) Value {
@@ -293,7 +293,7 @@ func networkdays(args []Node, get lookup) Value {
293293 s, e, sign = e, s, -1294294 }295295 if e-s > 1e7 {296- return ErrNum296+ return value.ErrNum297297 }298298 n := 0299299 for d := s; d <= e; d++ {
A internal/functions/decimal.go +317 -0
@@ -0,0 +1,317 @@
1+package functions2+3+import (4+ "sync"5+6+ "github.com/FelineStateMachine/012/internal/formula"7+ "github.com/FelineStateMachine/012/internal/numfmt"8+ "github.com/FelineStateMachine/012/internal/value"9+ "github.com/cockroachdb/apd/v3"10+)11+12+// Decimal arithmetic is an opt-in, per-workbook calculation setting for13+// money: with it, 0.1+0.2 is exactly 0.3, so =0.1+0.2=0.3 is TRUE and a14+// column of cents sums to what an accountant expects. Sheets and Excel use15+// binary floating point, so it is off by default.16+//17+// The boundary is deliberately small. Values stay float64 everywhere (in18+// cells, the file, formats and every other function); only these steps19+// compute in decimal:20+//21+// - the operators + - * / and the postfix %22+// - SUM, AVERAGE, PRODUCT, SUMIF, SUMIFS and SUMPRODUCT23+// - ROUND, ROUNDUP, ROUNDDOWN and TRUNC24+//25+// Each step reads its float64 operands as the shortest decimal that26+// round-trips (what the cell shows and what was typed: 0.1, not27+// 0.1000000000000000055...), computes exactly or to 34 significant digits28+// (IEEE decimal128) for division, and stores the float64 nearest to that29+// decimal result. Comparisons, lookups and everything else then work on30+// those float64s, so they see 0.3 rather than 0.30000000000000004.31+// Exponents (^), statistics, finance and date functions stay binary. A32+// step the decimal context can't represent (overflow past float64 range)33+// falls back to the binary result, so the setting never adds errors.34+//35+// Like any decimal system, it doesn't make 1/3*3 equal 1: the quotient is36+// rounded, and a float64 holds about 17 digits of it.37+38+// decCtx is the context of every decimal step: 34 digits, as decimal128.39+var decCtx = func() *apd.Context {40+ c := apd.BaseContext.WithPrecision(34)41+ c.Rounding = apd.RoundHalfEven42+ return c43+}()44+45+// fromDec returns the value nearest to d, or false if it is out of46+// float64 range.47+func fromDec(d *apd.Decimal) (Value, bool) {48+ f, err := d.Float64()49+ if err != nil {50+ return Value{}, false51+ }52+ return num(f), true53+}54+55+// decArith computes a op b in decimal, or reports false for operators56+// that stay binary and for results it can't represent.57+func decArith(op string, a, b float64) (Value, bool) {58+ var x, y, z apd.Decimal59+ if _, err := x.SetFloat64(a); err != nil {60+ return Value{}, false61+ }62+ if _, err := y.SetFloat64(b); err != nil {63+ return Value{}, false64+ }65+ var err error66+ switch op {67+ case "+":68+ _, err = decCtx.Add(&z, &x, &y)69+ case "-":70+ _, err = decCtx.Sub(&z, &x, &y)71+ case "*":72+ _, err = decCtx.Mul(&z, &x, &y)73+ case "/":74+ if b == 0 {75+ return value.ErrDiv0, true76+ }77+ _, err = decCtx.Quo(&z, &x, &y)78+ default:79+ return Value{}, false80+ }81+ if err != nil {82+ return Value{}, false83+ }84+ return fromDec(&z)85+}86+87+// decRounding maps ROUND's modes to apd's: half away from zero, away88+// from zero, toward zero.89+var decRounding = [...]apd.Rounder{numfmt.HalfUp: apd.RoundHalfUp, numfmt.Up: apd.RoundUp, numfmt.Down: apd.RoundDown}90+91+// decRound rounds x to places decimal places (negative places round to92+// tens, hundreds...) in decimal.93+func decRound(x float64, places int, mode numfmt.Rounding) (Value, bool) {94+ var d, z apd.Decimal95+ if _, err := d.SetFloat64(x); err != nil {96+ return Value{}, false97+ }98+ c := *decCtx99+ c.Rounding = decRounding[mode]100+ if _, err := c.Quantize(&z, &d, int32(-places)); err != nil {101+ return Value{}, false102+ }103+ return fromDec(&z)104+}105+106+// decAcc accumulates a sum or a product in decimal; ok turns false when107+// a step can't be represented, and the caller falls back to binary.108+type decAcc struct {109+ d, x apd.Decimal110+ ok bool111+}112+113+func newDecAcc(start int64) *decAcc {114+ a := &decAcc{ok: true}115+ a.d.SetInt64(start)116+ return a117+}118+119+func (a *decAcc) add(f float64) { a.step(f, decCtx.Add) }120+func (a *decAcc) mul(f float64) { a.step(f, decCtx.Mul) }121+122+func (a *decAcc) step(f float64, op func(d, x, y *apd.Decimal) (apd.Condition, error)) {123+ if !a.ok {124+ return125+ }126+ if _, err := a.x.SetFloat64(f); err != nil {127+ a.ok = false128+ } else if _, err := op(&a.d, &a.d, &a.x); err != nil {129+ a.ok = false130+ }131+}132+133+// addDec adds a decimal to the sum.134+func (a *decAcc) addDec(d *apd.Decimal) {135+ if !a.ok {136+ return137+ }138+ if _, err := decCtx.Add(&a.d, &a.d, d); err != nil {139+ a.ok = false140+ }141+}142+143+// result is the accumulated value, or false to fall back.144+func (a *decAcc) result() (Value, bool) {145+ if !a.ok {146+ return Value{}, false147+ }148+ return fromDec(&a.d)149+}150+151+// decFold feeds the numbers SUM and PRODUCT take to step and counts152+// them: in ranges only numbers, direct arguments coerced.153+func decFold(args []Node, get lookup, step func(float64)) (n int, e *Value) {154+ e = each(args, get, func(v Value, direct bool) *Value {155+ switch v.Kind {156+ case value.Error:157+ return &v158+ case value.Empty:159+ return nil160+ }161+ if v.Kind != value.Number && !direct {162+ return nil163+ }164+ f, err := toNum(v)165+ if err != nil {166+ return err167+ }168+ n++169+ step(f)170+ return nil171+ })172+ return n, e173+}174+175+// decTerms builds the decimal twin of a function that visits the terms176+// it adds (SUMIF, SUMIFS).177+func decTerms(terms func([]Node, lookup, func(float64)) *Value) func([]Node, lookup) (Value, bool) {178+ return func(args []Node, get lookup) (Value, bool) {179+ acc := newDecAcc(0)180+ if e := terms(args, get, acc.add); e != nil {181+ return *e, true182+ }183+ return acc.result()184+ }185+}186+187+// decSumProduct is SUMPRODUCT with each entry's product and the sum in188+// decimal.189+func decSumProduct(args []Node, get lookup) (Value, bool) {190+ acc := newDecAcc(0)191+ e := sumProductTerms(args, get, func(fs []float64) {192+ p := newDecAcc(1)193+ for _, f := range fs {194+ p.mul(f)195+ }196+ if !p.ok {197+ acc.ok = false198+ }199+ acc.addDec(&p.d)200+ })201+ if e != nil {202+ return *e, true203+ }204+ return acc.result()205+}206+207+// decEvals are the decimal versions of functions, by name. Each reports208+// false to fall back to the binary version.209+var decEvals = map[string]func(args []Node, get lookup) (Value, bool){210+ "SUM": func(args []Node, get lookup) (Value, bool) {211+ acc := newDecAcc(0)212+ if _, e := decFold(args, get, acc.add); e != nil {213+ return *e, true214+ }215+ return acc.result()216+ },217+ "AVERAGE": func(args []Node, get lookup) (Value, bool) {218+ acc := newDecAcc(0)219+ n, e := decFold(args, get, acc.add)220+ switch {221+ case e != nil:222+ return *e, true223+ case n == 0:224+ return value.ErrDiv0, true225+ case !acc.ok:226+ return Value{}, false227+ }228+ var q apd.Decimal229+ if _, err := decCtx.Quo(&q, &acc.d, apd.New(int64(n), 0)); err != nil {230+ return Value{}, false231+ }232+ return fromDec(&q)233+ },234+ "PRODUCT": func(args []Node, get lookup) (Value, bool) {235+ acc := newDecAcc(1)236+ n, e := decFold(args, get, acc.mul)237+ switch {238+ case e != nil:239+ return *e, true240+ case n == 0:241+ return num(0), true242+ }243+ return acc.result()244+ },245+ "SUMIF": decTerms(sumIfTerms),246+ "SUMIFS": decTerms(sumIfsTerms),247+ "SUMPRODUCT": decSumProduct,248+ "ROUND": decRounder(numfmt.HalfUp),249+ "ROUNDUP": decRounder(numfmt.Up),250+ "ROUNDDOWN": decRounder(numfmt.Down),251+ "TRUNC": decRounder(numfmt.Down),252+}253+254+func decRounder(mode numfmt.Rounding) func([]Node, lookup) (Value, bool) {255+ return func(args []Node, get lookup) (Value, bool) {256+ x, err := numArg(args[0], get)257+ if err != nil {258+ return *err, true259+ }260+ places, err := intArg(args, 1, 0, get)261+ if err != nil {262+ return *err, true263+ }264+ return decRound(x, clampInt(places, -308, 308), mode)265+ }266+}267+268+// decFuncs returns the decimal twins of the functions in decEvals: the269+// same definitions (name, help, format inference) with a decimal eval270+// that falls back to the binary one. Built on first use, after every271+// function is defined.272+var decFuncs = sync.OnceValue(func() map[string]*FuncDef {273+ out := map[string]*FuncDef{}274+ for name, dec := range decEvals {275+ bin := funcs[name]276+ twin := *bin277+ twin.eval = func(args []Node, get lookup) Value {278+ if v, ok := dec(args, get); ok {279+ return v280+ }281+ return bin.eval(args, get)282+ }283+ out[name] = &twin284+ }285+ return out286+})287+288+// Operators computed in decimal are marked by these types, which eval289+// knows; parsed formulas never contain them.290+type (291+ decUnary formula.Unary292+ decBinary formula.Binary293+)294+295+// Decimalize returns a copy of n that evaluates in decimal: operators are296+// marked, and functions with a decimal twin call it.297+func Decimalize(n Node) Node {298+ switch n := n.(type) {299+ case formula.Unary:300+ n.X = Decimalize(n.X)301+ return decUnary(n)302+ case formula.Binary:303+ n.L, n.R = Decimalize(n.L), Decimalize(n.R)304+ return decBinary(n)305+ case formula.Call:306+ args := make([]Node, len(n.Args))307+ for i, a := range n.Args {308+ args[i] = Decimalize(a)309+ }310+ n.Args = args311+ if twin, ok := decFuncs()[funcOf(n).Name]; ok {312+ n.Fn = twin313+ }314+ return n315+ }316+ return n317+}
R internal/sheet/functions_doc_test.go → internal/functions/doc_test.go +16 -16
@@ -1,4 +1,4 @@
1-package sheet1+package functions22 33 import (44 "flag"
@@ -19,23 +19,23 @@ var updateDocs = flag.Bool("update-docs", false, "rewrite docs/functions.md from
1919 // docsPath is the generated function reference.2020 const docsPath = "../../docs/functions.md"2121 22-// categories names each functions*.go file's group, in the order the23-// reference lists them.22+// categories names the group of the functions each file defines, in the23+// order the reference lists them.2424 var categories = []struct{ file, title string }{25- {"functions.go", "Everyday"},26- {"functions_math.go", "Math"},27- {"functions_stats.go", "Statistics"},28- {"functions_logic.go", "Logic and information"},29- {"functions_text.go", "Text"},30- {"functions_lookup.go", "Lookup"},31- {"functions_date.go", "Date and time"},32- {"functions_finance.go", "Finance"},33- {"functions_link.go", "Links"},34- {"functions_jev.go", "JEV (hosted model)"},25+ {"everyday.go", "Everyday"},26+ {"math.go", "Math"},27+ {"stats.go", "Statistics"},28+ {"logic.go", "Logic and information"},29+ {"text.go", "Text"},30+ {"lookup.go", "Lookup"},31+ {"date.go", "Date and time"},32+ {"finance.go", "Finance"},33+ {"link.go", "Links"},34+ {"jev.go", "JEV (hosted model)"},3535 }3636 3737 // TestFunctionsDoc keeps docs/functions.md in step with the function38-// table, as help is: run `go test ./internal/sheet -run FunctionsDoc38+// table, as help is: run `go test ./internal/functions -run FunctionsDoc3939 // -update-docs` after adding a function.4040 func TestFunctionsDoc(t *testing.T) {4141 byFile, err := definingFiles()
@@ -51,7 +51,7 @@ func TestFunctionsDoc(t *testing.T) {
5151 }5252 want, err := os.ReadFile(docsPath)5353 if err != nil || string(want) != got {54- t.Errorf("docs/functions.md is stale; run go test ./internal/sheet -run FunctionsDoc -update-docs")54+ t.Errorf("docs/functions.md is stale; run go test ./internal/functions -run FunctionsDoc -update-docs")5555 }5656 }5757
@@ -60,7 +60,7 @@ func TestFunctionsDoc(t *testing.T) {
6060 func definingFiles() (map[string]string, error) {6161 fset := gotoken.NewFileSet()6262 out := map[string]string{}63- files, err := filepath.Glob("functions*.go")63+ files, err := filepath.Glob("*.go")6464 if err != nil {6565 return nil, err6666 }
R internal/sheet/eval.go → internal/functions/eval.go +21 -24
@@ -1,32 +1,29 @@
1-package sheet1+package functions22 33 import (44 "math"55 66 "github.com/FelineStateMachine/012/internal/formula"7+ "github.com/FelineStateMachine/012/internal/value"78 )89 9-// lookup is how a formula reads other cells (see reader): one at a time,10-// or a whole range at once, so reading a range needn't cost a lookup per11-// cell and can later be served from column blocks or cached aggregates.12-// It is a concrete type rather than an interface so the callbacks given13-// to cells stay on the stack.14-type lookup = *reader10+// Eval computes a formula, reading the cells it refers to through get.11+func Eval(n Node, get *Reader) Value { return eval(n, get) }1512 1613 func eval(n Node, get lookup) Value {1714 switch n := n.(type) {1815 case formula.Num:1916 return num(n.V)2017 case formula.Str:21- return Value{Kind: Text, Str: n.V}18+ return Value{Kind: value.Text, Str: n.V}2219 case formula.Bool:2320 return boolean(n.V)2421 case formula.Ref:2522 return get.cell(n.Sheet, n.Addr)2623 case formula.Name:27- return ErrName24+ return value.ErrName2825 case formula.RefErr:29- return ErrRef26+ return value.ErrRef3027 case formula.Empty:3128 return Value{}3229 case formula.Range:
@@ -35,7 +32,7 @@ func eval(n Node, get lookup) Value {
3532 if n.Rect.From == n.Rect.To {3633 return get.cell(n.Sheet, n.Rect.From)3734 }38- return ErrValue35+ return value.ErrValue3936 case formula.Unary:4037 return evalUnary(n, get, false)4138 case decUnary:
@@ -45,21 +42,21 @@ func eval(n Node, get lookup) Value {
4542 case decBinary:4643 return evalBinary(formula.Binary(n), get, true)4744 case formula.Call:48- get.w.depth++ // see evaluate.go; operators count in theirs45+ *get.depth++ // see the engine's evaluate.go; operators count in theirs4946 v := funcOf(n).call(n.Args, get)50- get.w.depth--47+ *get.depth--5148 return v5249 }53- return ErrValue50+ return value.ErrValue5451 }5552 5653 // evalUnary computes a prefix operator or the postfix %; with dec, % is5754 // decimal (decimal.go).5855 func evalUnary(n formula.Unary, get lookup, dec bool) Value {59- get.w.depth++56+ *get.depth++6057 x := eval(n.X, get)61- get.w.depth--62- if x.Kind == Error {58+ *get.depth--59+ if x.Kind == value.Error {6360 return x6461 }6562 switch n.Op {
@@ -93,18 +90,18 @@ func evalUnary(n formula.Unary, get lookup, dec bool) Value {
9390 // evalBinary computes a binary operator; with dec, arithmetic is decimal9491 // (decimal.go).9592 func evalBinary(n formula.Binary, get lookup, dec bool) Value {96- get.w.depth++93+ *get.depth++9794 l, r := eval(n.L, get), eval(n.R, get)98- get.w.depth--99- if l.Kind == Error {95+ *get.depth--96+ if l.Kind == value.Error {10097 return l10198 }102- if r.Kind == Error {99+ if r.Kind == value.Error {103100 return r104101 }105102 switch n.Op {106103 case "&":107- return Value{Kind: Text, Str: text(l) + text(r)}104+ return Value{Kind: value.Text, Str: text(l) + text(r)}108105 case "=", "<>", "<", ">", "<=", ">=":109106 c := compare(l, r)110107 switch n.Op {
@@ -143,7 +140,7 @@ func evalBinary(n formula.Binary, get lookup, dec bool) Value {
143140 return num(a * b)144141 case "/":145142 if b == 0 {146- return ErrDiv0143+ return value.ErrDiv0147144 }148145 return num(a / b)149146 case "^":
@@ -153,5 +150,5 @@ func evalBinary(n formula.Binary, get lookup, dec bool) Value {
153150 case "#OR#":154151 return boolean(a != 0 || b != 0)155152 }156- return ErrValue153+ return value.ErrValue157154 }
R internal/sheet/functions.go → internal/functions/everyday.go +57 -30
@@ -1,4 +1,4 @@
1-package sheet1+package functions22 33 import (44 "math"
@@ -6,6 +6,7 @@ import (
66 77 "github.com/FelineStateMachine/012/internal/formula"88 "github.com/FelineStateMachine/012/internal/numfmt"9+ "github.com/FelineStateMachine/012/internal/value"910 )1011 1112 // FuncDef describes a spreadsheet function. The table drives parsing
@@ -32,6 +33,26 @@ type FuncDef struct {
3233 3334 func (f *FuncDef) call(args []Node, get lookup) Value { return f.eval(args, get) }3435 36+// Signature is how the parser checks calls to f.37+func (f *FuncDef) Signature() formula.Signature {38+ return formula.Signature{Name: f.Name, Args: f.Args, Min: f.Min, Max: f.Max, Step: f.step}39+}40+41+// Remote reports whether f asks a remote question (JEV functions).42+func (f *FuncDef) Remote() bool { return f.remote != nil }43+44+// Question is the remote question a call of f asks with its current45+// arguments, or an error when they don't make one.46+func (f *FuncDef) Question(args []Node, get *Reader) (RemoteCall, error) {47+ return f.remote(args, get)48+}49+50+// Of is the function a parsed call calls: always one of the table's, as51+// long as the parser found functions through LookupFunc.52+func Of(c formula.Call) *FuncDef { return c.Fn.(*FuncDef) }53+54+func funcOf(c formula.Call) *FuncDef { return Of(c) }55+3556 var funcs = map[string]*FuncDef{}3657 3758 // aliases maps alternative names (including 1-2-3's) to canonical ones.
@@ -71,27 +92,27 @@ func Funcs() []*FuncDef {
7192 func init() {7293 define(7394 &FuncDef{Name: "SUM", Args: "value1, [value2, ...]", Desc: "Sum of numbers", Min: 1, Max: -1,74- eval: aggregate(func(s agg) Value { return num(s.sum) }), format: inherit},95+ eval: aggregate(func(s Agg) Value { return num(s.sum) }), format: inherit},7596 &FuncDef{Name: "AVERAGE", Args: "value1, [value2, ...]", Desc: "Average of numbers, ignoring text", Min: 1, Max: -1,76- eval: aggregate(func(s agg) Value {97+ eval: aggregate(func(s Agg) Value {7798 if s.nums == 0 {78- return ErrDiv099+ return value.ErrDiv079100 }80101 return num(s.sum / float64(s.nums))81102 }), format: inherit},82103 &FuncDef{Name: "COUNT", Args: "value1, [value2, ...]", Desc: "Count of numeric values", Min: 1, Max: -1,83- eval: aggregate(func(s agg) Value { return num(float64(s.nums)) })},104+ eval: aggregate(func(s Agg) Value { return num(float64(s.nums)) })},84105 &FuncDef{Name: "COUNTA", Args: "value1, [value2, ...]", Desc: "Count of non-empty values", Min: 1, Max: -1,85- eval: aggregate(func(s agg) Value { return num(float64(s.count)) })},106+ eval: aggregate(func(s Agg) Value { return num(float64(s.count)) })},86107 &FuncDef{Name: "MIN", Args: "value1, [value2, ...]", Desc: "Smallest number", Min: 1, Max: -1,87- eval: aggregate(func(s agg) Value {108+ eval: aggregate(func(s Agg) Value {88109 if s.nums == 0 {89110 return num(0)90111 }91112 return num(s.min)92113 }), format: inherit},93114 &FuncDef{Name: "MAX", Args: "value1, [value2, ...]", Desc: "Largest number", Min: 1, Max: -1,94- eval: aggregate(func(s agg) Value {115+ eval: aggregate(func(s Agg) Value {95116 if s.nums == 0 {96117 return num(0)97118 }
@@ -105,18 +126,18 @@ func init() {
105126 &FuncDef{Name: "MOD", Args: "dividend, divisor", Desc: "Remainder, with the sign of the divisor", Min: 2, Max: 2,106127 eval: numeric(func(x []float64) Value {107128 if x[1] == 0 {108- return ErrDiv0129+ return value.ErrDiv0109130 }110131 return num(x[0] - x[1]*math.Floor(x[0]/x[1]))111132 })},112133 &FuncDef{Name: "PI", Desc: "The number pi", Max: 0, eval: constant(num(math.Pi))},113134 &FuncDef{Name: "TRUE", Desc: "The logical value TRUE", Max: 0, eval: constant(boolean(true))},114135 &FuncDef{Name: "FALSE", Desc: "The logical value FALSE", Max: 0, eval: constant(boolean(false))},115- &FuncDef{Name: "NA", Desc: "The #N/A error", Max: 0, eval: constant(ErrNA)},136+ &FuncDef{Name: "NA", Desc: "The #N/A error", Max: 0, eval: constant(value.ErrNA)},116137 &FuncDef{Name: "IF", Args: "condition, value_if_true, [value_if_false]", Desc: "Choose a value by a condition", Min: 2, Max: 3,117138 eval: func(args []Node, get lookup) Value {118139 c := eval(args[0], get)119- if c.Kind == Error {140+ if c.Kind == value.Error {120141 return c121142 }122143 f, err := toNum(c)
@@ -134,11 +155,11 @@ func init() {
134155 &FuncDef{Name: "IFERROR", Args: "value, [value_if_error]", Desc: "A fallback when a value is an error", Min: 1, Max: 2,135156 eval: func(args []Node, get lookup) Value {136157 v := eval(args[0], get)137- if v.Kind != Error {158+ if v.Kind != value.Error {138159 return v139160 }140161 if len(args) < 2 {141- return Value{Kind: Text}162+ return Value{Kind: value.Text}142163 }143164 return eval(args[1], get)144165 }},
@@ -151,7 +172,11 @@ func init() {
151172 )152173 }153174 154-type agg struct {175+// Agg is the running aggregate of SUM-like functions (SUM, AVERAGE,176+// COUNT, COUNTA, MIN, MAX, PRODUCT): what they read of a range, added177+// in order. The engine keeps one per range shared by a recalculation178+// (Book.RangeAgg).179+type Agg struct {155180 sum float64156181 prod float64157182 count int // non-empty values
@@ -190,17 +215,17 @@ func eachOf(arg Node, get lookup, fn func(v Value, direct bool) *Value) *Value {
190215 return fn(eval(arg, get), true)191216 }192217 193-// add counts v into the aggregate, with SUM's rules: blanks are skipped,218+// Add counts v into the aggregate, with SUM's rules: blanks are skipped,194219 // errors returned, and text counts only when given directly.195-func (s *agg) add(v Value, direct bool) *Value {220+func (s *Agg) Add(v Value, direct bool) *Value {196221 switch v.Kind {197- case Error:222+ case value.Error:198223 return errOf(v)199- case Empty:224+ case value.Empty:200225 return nil201226 }202227 s.count++203- if v.Kind != Number && !direct {228+ if v.Kind != value.Number && !direct {204229 return nil205230 }206231 f, err := toNum(v)
@@ -214,19 +239,21 @@ func (s *agg) add(v Value, direct bool) *Value {
214239 return nil215240 }216241 217-func newAgg() agg { return agg{prod: 1, min: math.Inf(1), max: math.Inf(-1)} }242+// NewAgg is the aggregate of nothing.243+func NewAgg() Agg { return Agg{prod: 1, min: math.Inf(1), max: math.Inf(-1)} }218244 219245 // aggregate builds SUM-like functions with Sheets semantics: in ranges only220246 // numbers count and text is ignored; direct arguments are coerced, so221247 // SUM("a") is #VALUE!. A range read first is served from the running222-// aggregates shared by the recalculation (rangememo.go), so a thousand223-// SUM(A:A) or a column of running totals read each cell once.224-func aggregate(done func(agg) Value) func([]Node, lookup) Value {248+// aggregates shared by the recalculation (Book.RangeAgg; the engine's249+// rangememo.go), so a thousand SUM(A:A) or a column of running totals250+// read each cell once.251+func aggregate(done func(Agg) Value) func([]Node, lookup) Value {225252 return func(args []Node, get lookup) Value {226- s := newAgg()253+ s := NewAgg()227254 for _, arg := range args {228255 if rn, ok := arg.(formula.Range); ok && s.count == 0 {229- if a, e, ok := get.rangeAgg(rn.Sheet, rn.Rect); ok {256+ if a, e, ok := get.book.RangeAgg(rn.Sheet, rn.Rect); ok {230257 if e != nil {231258 return *e232259 }
@@ -234,7 +261,7 @@ func aggregate(done func(agg) Value) func([]Node, lookup) Value {
234261 continue235262 }236263 }237- if e := eachOf(arg, get, s.add); e != nil {264+ if e := eachOf(arg, get, s.Add); e != nil {238265 return *e239266 }240267 }
@@ -247,9 +274,9 @@ func logical(test func(trues, n int) bool) func([]Node, lookup) Value {
247274 trues, n := 0, 0248275 e := each(args, get, func(v Value, direct bool) *Value {249276 switch {250- case v.Kind == Error:277+ case v.Kind == value.Error:251278 return errOf(v)252- case v.Kind == Empty, v.Kind == Text && !direct:279+ case v.Kind == value.Empty, v.Kind == value.Text && !direct:253280 return nil254281 }255282 f, err := toNum(v)
@@ -266,7 +293,7 @@ func logical(test func(trues, n int) bool) func([]Node, lookup) Value {
266293 case e != nil:267294 return *e268295 case n == 0:269- return ErrValue296+ return value.ErrValue270297 }271298 return boolean(test(trues, n))272299 }
@@ -278,7 +305,7 @@ func numeric(f func([]float64) Value) func([]Node, lookup) Value {
278305 xs := make([]float64, len(args))279306 for i, a := range args {280307 v := eval(a, get)281- if v.Kind == Error {308+ if v.Kind == value.Error {282309 return v283310 }284311 x, err := toNum(v)
R internal/sheet/functions_finance.go → internal/functions/finance.go +14 -12
@@ -1,7 +1,9 @@
1-package sheet1+package functions22 33 import (44 "math"5+6+ "github.com/FelineStateMachine/012/internal/value"57 )68 79 // Finance functions follow the usual cash-flow sign convention: money
@@ -14,7 +16,7 @@ func init() {
1416 eval: finance(func(x []float64) Value {1517 r, n, pv, fv, t := x[0], x[1], x[2], x[3], x[4]1618 if n == 0 {17- return ErrNum19+ return value.ErrNum1820 }1921 if r == 0 {2022 return num(-(pv + fv) / n)
@@ -45,19 +47,19 @@ func init() {
4547 r, pmt, pv, fv, t := x[0], x[1], x[2], x[3], x[4]4648 if r == 0 {4749 if pmt == 0 {48- return ErrNum50+ return value.ErrNum4951 }5052 return num(-(pv + fv) / pmt)5153 }5254 a := pmt * (1 + r*t)5355 ratio := (a - fv*r) / (a + pv*r)5456 if ratio <= 0 || r <= -1 {55- return ErrNum57+ return value.ErrNum5658 }5759 return num(math.Log(ratio) / math.Log(1+r))5860 })},5961 &FuncDef{Name: "RATE", Args: "number_of_periods, payment_per_period, present_value, [future_value], [end_or_beginning], [rate_guess]", Desc: "Interest rate per period of an annuity", Min: 3, Max: 6,60- eval: rate, format: returns(Preset(FmtPercent))},62+ eval: rate, format: returns(value.Preset(value.FmtPercent))},6163 &FuncDef{Name: "NPV", Args: "discount, cashflow1, [cashflow2, ...]", Desc: "Net present value of periodic cash flows", Min: 2, Max: -1,6264 eval: func(args []Node, get lookup) Value {6365 r, err := numArg(args[0], get)
@@ -69,12 +71,12 @@ func init() {
6971 return *err7072 }7173 if r == -1 {72- return ErrDiv074+ return value.ErrDiv07375 }7476 return num(npv(r, flows, 1))7577 }},7678 &FuncDef{Name: "IRR", Args: "cashflow_amounts, [rate_guess]", Desc: "Internal rate of return of periodic cash flows", Min: 1, Max: 2,77- eval: irr, format: returns(Preset(FmtPercent))},79+ eval: irr, format: returns(value.Preset(value.FmtPercent))},7880 )7981 }8082
@@ -126,7 +128,7 @@ func rate(args []Node, get lookup) Value {
126128 t = 1127129 }128130 if n <= 0 {129- return ErrNum131+ return value.ErrNum130132 }131133 f := func(r float64) float64 {132134 if math.Abs(r) < 1e-12 {
@@ -140,7 +142,7 @@ func rate(args []Node, get lookup) Value {
140142 h := 1e-7 * math.Max(1, math.Abs(r))141143 d := (f(r+h) - f(r-h)) / (2 * h)142144 if d == 0 || math.IsNaN(d) {143- return ErrNum145+ return value.ErrNum144146 }145147 next := r - y/d146148 if next <= -1 {
@@ -151,7 +153,7 @@ func rate(args []Node, get lookup) Value {
151153 }152154 r = next153155 }154- return ErrNum156+ return value.ErrNum155157 }156158 157159 // irr finds the rate where the cash flows' present value is zero: Newton
@@ -171,7 +173,7 @@ func irr(args []Node, get lookup) Value {
171173 neg = neg || v < 0172174 }173175 if !pos || !neg {174- return ErrNum176+ return value.ErrNum175177 }176178 f := func(r float64) float64 { return npv(r, flows, 0) }177179 df := func(r float64) float64 {
@@ -202,7 +204,7 @@ func irr(args []Node, get lookup) Value {
202204 hi *= 2203205 }204206 if f(hi)*f(lo) > 0 {205- return ErrNum207+ return value.ErrNum206208 }207209 for range 200 {208210 mid := (lo + hi) / 2
A internal/functions/format.go +100 -0
@@ -0,0 +1,100 @@
1+package functions2+3+import (4+ "github.com/FelineStateMachine/012/internal/formula"5+ "github.com/FelineStateMachine/012/internal/value"6+)7+8+// IsVolatile reports whether n calls a function whose result changes9+// without its inputs changing (TODAY, NOW, RAND).10+func IsVolatile(n Node) bool {11+ switch n := n.(type) {12+ case formula.Unary:13+ return IsVolatile(n.X)14+ case formula.Binary:15+ return IsVolatile(n.L) || IsVolatile(n.R)16+ case formula.Call:17+ if funcOf(n).Volatile {18+ return true19+ }20+ for _, a := range n.Args {21+ if IsVolatile(a) {22+ return true23+ }24+ }25+ }26+ return false27+}28+29+// InferFormat picks the format Sheets shows a formula's result in when30+// the cell is Automatic: dates from date functions, and otherwise the31+// format of the first formatted input, so =B2+B3 of currency shows32+// currency and a date plus days shows a date.33+func InferFormat(n Node, at func(string, Addr) Format) Format {34+ switch n := n.(type) {35+ case formula.Ref:36+ return at(n.Sheet, n.Addr)37+ case formula.Range:38+ return at(n.Sheet, n.Rect.From)39+ case formula.Unary:40+ if n.Op == "-" || n.Op == "+" {41+ return InferFormat(n.X, at)42+ }43+ case formula.Binary:44+ l, r := InferFormat(n.L, at), InferFormat(n.R, at)45+ switch n.Op {46+ case "+", "-":47+ if n.Op == "-" && l.Kind.IsTime() && r.Kind.IsTime() {48+ return Format{} // days between two dates49+ }50+ return firstFormat(l, r)51+ case "*", "/":52+ if l.Kind.IsTime() || r.Kind.IsTime() {53+ return Format{}54+ }55+ return firstFormat(l, r)56+ }57+ case formula.Call:58+ if funcOf(n).format != nil {59+ return funcOf(n).format(n.Args, func(n Node) Format { return InferFormat(n, at) })60+ }61+ }62+ return Format{}63+}64+65+func firstFormat(fs ...Format) Format {66+ for _, f := range fs {67+ if !f.IsZero() && f.Kind != value.FmtText {68+ return f69+ }70+ }71+ return Format{}72+}73+74+// Result formats for the function table.75+76+// returns makes a function's result show in format f.77+func returns(f Format) func([]Node, func(Node) Format) Format {78+ return func([]Node, func(Node) Format) Format { return f }79+}80+81+// inherit makes a function's result take the format of its first82+// formatted argument (SUM, MIN, ROUND...).83+func inherit(args []Node, infer func(Node) Format) Format {84+ for _, a := range args {85+ if f := infer(a); !f.IsZero() && f.Kind != value.FmtText {86+ return f87+ }88+ }89+ return Format{}90+}91+92+// inheritFrom takes the format of argument i only.93+func inheritFrom(i int) func([]Node, func(Node) Format) Format {94+ return func(args []Node, infer func(Node) Format) Format {95+ if i < len(args) {96+ return infer(args[i])97+ }98+ return Format{}99+ }100+}
R internal/sheet/functions_jev.go → internal/functions/jev.go +22 -26
@@ -1,9 +1,10 @@
1-package sheet1+package functions22 33 import (44 "strings"55 66 "github.com/FelineStateMachine/012/internal/formula"7+ "github.com/FelineStateMachine/012/internal/value"78 )89 910 // JEV functions ask TypeSafe's hosted model (jev) typed questions about a
@@ -25,10 +26,10 @@ func init() {
2526 &FuncDef{Name: "JEV.PROB", Args: "value, question, [yes_means], [no_means]",2627 Desc: "The probability the answer is yes, from the JEV model", Min: 2, Max: 4, Volatile: true,2728 remote: yesNoCall, eval: remoteEval(yesNoCall, func(a RemoteAnswer) Value { return num(a.Noul) }),28- format: func([]Node, func(Node) Format) Format { return Format{Kind: FmtPercent} }},29+ format: func([]Node, func(Node) Format) Format { return Format{Kind: value.FmtPercent} }},2930 &FuncDef{Name: "JEV.CLASSIFY", Args: "value, question, labels, [descriptions]",3031 Desc: "The label that fits best, chosen by the JEV model", Min: 3, Max: 4, Volatile: true,31- remote: choiceCall, eval: remoteEval(choiceCall, func(a RemoteAnswer) Value { return Value{Kind: Text, Str: a.Choice} })},32+ remote: choiceCall, eval: remoteEval(choiceCall, func(a RemoteAnswer) Value { return Value{Kind: value.Text, Str: a.Choice} })},3233 &FuncDef{Name: "JEV.SCORE", Args: "value, question, levels",3334 Desc: "A score on an ordered rubric, from the JEV model", Min: 3, Max: 3, Volatile: true,3435 remote: scoreCall, eval: remoteEval(scoreCall, func(a RemoteAnswer) Value { return num(a.Score) })},
@@ -45,11 +46,11 @@ const (
4546 4647 // ErrRemote is a question the model couldn't answer, e.g. a network4748 // failure; the context line says why.48-var ErrRemote = Value{Kind: Error, Str: "#ERROR!"}49+var ErrRemote = Value{Kind: value.Error, Str: "#ERROR!"}4950 50-// remoteEval evaluates a JEV function through the workbook's51-// RemoteSource, converting the52-// answer with result. Errors in the inputs come back as-is so they can be51+// remoteEval evaluates a JEV function by asking the Book, which looks52+// the answer up in the workbook's RemoteSource, converting the answer53+// with result. Errors in the inputs come back as-is so they can be5354 // fixed; they're never sent.5455 func remoteEval(build func([]Node, lookup) (RemoteCall, error), result func(RemoteAnswer) Value) func([]Node, lookup) Value {5556 return func(args []Node, get lookup) Value {
@@ -57,15 +58,10 @@ func remoteEval(build func([]Node, lookup) (RemoteCall, error), result func(Remo
5758 if err != nil {5859 return err.(inputError).v5960 }60- remote := get.w.remote61- if remote == nil {62- return ErrNoRemote63- }64- ans, ok := remote.Lookup(call)61+ ans, v := get.book.Ask(call)6562 switch {66- case !ok:67- get.w.wait(call)68- return Pending63+ case v.Kind == value.Error:64+ return v6965 case ans.Failed != "":7066 return ErrRemote7167 }
@@ -88,7 +84,7 @@ func (e inputError) Error() string { return e.v.Str }
8884 func jevState(n Node, get lookup) (any, error) {8985 if rn, ok := n.(formula.Range); ok {9086 m := rectMatrix(rn.Sheet, rn.Rect, get)91- if m.blank.Kind == Error {87+ if m.blank.Kind == value.Error {9288 return nil, inputError{m.blank}9389 }9490 rows, cols := m.rows, m.cols
@@ -100,7 +96,7 @@ func jevState(n Node, get lookup) (any, error) {
10096 var row []any10197 for c := range cols {10298 v := m.cell(r, c)103- if v.Kind == Error {99+ if v.Kind == value.Error {104100 return nil, inputError{v}105101 }106102 row = append(row, plain(v))
@@ -110,7 +106,7 @@ func jevState(n Node, get lookup) (any, error) {
110106 return out, nil111107 }112108 v := eval(n, get)113- if v.Kind == Error {109+ if v.Kind == value.Error {114110 return nil, inputError{v}115111 }116112 return plain(v), nil
@@ -119,11 +115,11 @@ func jevState(n Node, get lookup) (any, error) {
119115 // plain converts a value to a JSON-friendly Go value.120116 func plain(v Value) any {121117 switch v.Kind {122- case Number:118+ case value.Number:123119 return v.Num124- case Bool:120+ case value.Bool:125121 return v.Num != 0126- case Text:122+ case value.Text:127123 return v.Str128124 }129125 return ""
@@ -132,7 +128,7 @@ func plain(v Value) any {
132128 // jevText evaluates an argument that must be text, such as the question.133129 func jevText(n Node, get lookup) (string, error) {134130 v := eval(n, get)135- if v.Kind == Error {131+ if v.Kind == value.Error {136132 return "", inputError{v}137133 }138134 return strings.TrimSpace(text(v)), nil
@@ -142,7 +138,7 @@ func jevText(n Node, get lookup) (string, error) {
142138 func jevQuestion(n Node, get lookup) (string, error) {143139 q, err := jevText(n, get)144140 if err == nil && q == "" {145- err = inputError{ErrValue}141+ err = inputError{value.ErrValue}146142 }147143 return q, err148144 }
@@ -155,7 +151,7 @@ func jevList(n Node, get lookup) ([]string, error) {
155151 if rn, ok := n.(formula.Range); ok {156152 var err error157153 get.cells(rn.Sheet, rn.Rect, func(_ Addr, v Value) bool {158- if v.Kind == Error {154+ if v.Kind == value.Error {159155 err = inputError{v}160156 return false161157 }
@@ -223,7 +219,7 @@ func choiceCall(args []Node, get lookup) (RemoteCall, error) {
223219 }224220 }225221 if len(labels) < 2 || len(labels) > maxChoiceLabels {226- return RemoteCall{}, inputError{ErrValue} // nothing to choose between, or too many222+ return RemoteCall{}, inputError{value.ErrValue} // nothing to choose between, or too many227223 }228224 criteria := make(map[string]string, len(labels))229225 for i, l := range labels {
@@ -249,7 +245,7 @@ func scoreCall(args []Node, get lookup) (RemoteCall, error) {
249245 return RemoteCall{}, err250246 }251247 if len(levels) < 2 || len(levels) > maxScoreLevels {252- return RemoteCall{}, inputError{ErrValue}248+ return RemoteCall{}, inputError{value.ErrValue}253249 }254250 return RemoteCall{Kind: "score", State: state, Instructions: q, Criteria: levels}, nil255251 }
A internal/functions/link.go +25 -0
R internal/sheet/functions_logic.go → internal/functions/logic.go +16 -14
@@ -1,4 +1,6 @@
1-package sheet1+package functions2+3+import "github.com/FelineStateMachine/012/internal/value"24 35 func init() {46 define(
@@ -11,19 +13,19 @@ func init() {
1113 &FuncDef{Name: "IFNA", Args: "value, value_if_na", Desc: "A fallback when a value is #N/A", Min: 2, Max: 2,1214 eval: ifNA, format: inherit},1315 &FuncDef{Name: "ISBLANK", Args: "value", Desc: "TRUE if a cell is empty", Min: 1, Max: 1,14- eval: is(func(v Value) bool { return v.Kind == Empty })},16+ eval: is(func(v Value) bool { return v.Kind == value.Empty })},1517 &FuncDef{Name: "ISNUMBER", Args: "value", Desc: "TRUE if a value is a number", Min: 1, Max: 1,16- eval: is(func(v Value) bool { return v.Kind == Number })},18+ eval: is(func(v Value) bool { return v.Kind == value.Number })},1719 &FuncDef{Name: "ISTEXT", Args: "value", Desc: "TRUE if a value is text", Min: 1, Max: 1,18- eval: is(func(v Value) bool { return v.Kind == Text })},20+ eval: is(func(v Value) bool { return v.Kind == value.Text })},1921 &FuncDef{Name: "ISLOGICAL", Args: "value", Desc: "TRUE if a value is TRUE or FALSE", Min: 1, Max: 1,20- eval: is(func(v Value) bool { return v.Kind == Bool })},22+ eval: is(func(v Value) bool { return v.Kind == value.Bool })},2123 &FuncDef{Name: "ISERROR", Args: "value", Desc: "TRUE if a value is any error", Min: 1, Max: 1,22- eval: is(func(v Value) bool { return v.Kind == Error })},24+ eval: is(func(v Value) bool { return v.Kind == value.Error })},2325 &FuncDef{Name: "ISERR", Args: "value", Desc: "TRUE if a value is an error other than #N/A", Min: 1, Max: 1,24- eval: is(func(v Value) bool { return v.Kind == Error && v != ErrNA })},26+ eval: is(func(v Value) bool { return v.Kind == value.Error && v != value.ErrNA })},2527 &FuncDef{Name: "ISNA", Args: "value", Desc: "TRUE if a value is #N/A", Min: 1, Max: 1,26- eval: is(func(v Value) bool { return v == ErrNA })},28+ eval: is(func(v Value) bool { return v == value.ErrNA })},2729 )2830 }2931
@@ -39,7 +41,7 @@ func ifs(args []Node, get lookup) Value {
3941 return eval(args[i+1], get)4042 }4143 }42- return ErrNA44+ return value.ErrNA4345 }4446 4547 func ifsFormat(args []Node, infer func(Node) Format) Format {
@@ -52,13 +54,13 @@ func ifsFormat(args []Node, infer func(Node) Format) Format {
5254 5355 func switchCase(args []Node, get lookup) Value {5456 x := eval(args[0], get)55- if x.Kind == Error {57+ if x.Kind == value.Error {5658 return x5759 }5860 i := 15961 for ; i+1 < len(args); i += 2 {6062 c := eval(args[i], get)61- if c.Kind == Error {63+ if c.Kind == value.Error {6264 return c6365 }6466 if sameKind(x, c) && compare(x, c) == 0 {
@@ -68,12 +70,12 @@ func switchCase(args []Node, get lookup) Value {
6870 if i < len(args) {6971 return eval(args[i], get)7072 }71- return ErrNA73+ return value.ErrNA7274 }7375 7476 func ifNA(args []Node, get lookup) Value {7577 v := eval(args[0], get)76- if v == ErrNA {78+ if v == value.ErrNA {7779 return eval(args[1], get)7880 }7981 return v
@@ -90,7 +92,7 @@ func is(test func(Value) bool) func([]Node, lookup) Value {
9092 // lookups: numbers only equal numbers, text only text. A blank is 0 or "".9193 func sameKind(a, b Value) bool {9294 ka, kb := a.Kind, b.Kind93- if ka == Empty || kb == Empty {95+ if ka == value.Empty || kb == value.Empty {9496 return true9597 }9698 return ka == kb
R internal/sheet/functions_lookup.go → internal/functions/lookup.go +22 -172
@@ -1,9 +1,9 @@
1-package sheet1+package functions22 33 import (4- "iter"5- "slices"64 "strings"5+6+ "github.com/FelineStateMachine/012/internal/value"77 )88 99 func init() {
@@ -31,7 +31,7 @@ func init() {
3131 3232 func match(args []Node, get lookup) Value {3333 key := eval(args[0], get)34- if key.Kind == Error {34+ if key.Kind == value.Error {3535 return key3636 }3737 m := matrixArg(args[1], get)
@@ -40,7 +40,7 @@ func match(args []Node, get lookup) Value {
4040 case err != nil:4141 return *err4242 case !m.vector():43- return ErrNA43+ return value.ErrNA4444 }4545 q := lineSeq(m, get, m.cols == 1)4646 var i int
@@ -53,7 +53,7 @@ func match(args []Node, get lookup) Value {
5353 i = q.findSorted(key, -1)5454 }5555 if i < 0 {56- return ErrNA56+ return value.ErrNA5757 }5858 return num(float64(i + 1))5959 }
@@ -73,7 +73,7 @@ func index(args []Node, get lookup) Value {
7373 row, col = 1, row7474 }7575 if row < 0 || col < 0 {76- return ErrValue76+ return value.ErrValue7777 }7878 if row == 0 && m.rows == 1 {7979 row = 1
@@ -83,9 +83,9 @@ func index(args []Node, get lookup) Value {
8383 }8484 switch {8585 case row == 0 || col == 0:86- return ErrValue // a whole row or column: an array we can't show86+ return value.ErrValue // a whole row or column: an array we can't show8787 case row > m.rows || col > m.cols:88- return ErrRef88+ return value.ErrRef8989 }9090 return m.cell(row-1, col-1)9191 }
@@ -96,7 +96,7 @@ func choose(args []Node, get lookup) Value {
9696 return *err9797 }9898 if i < 1 || i >= len(args) {99- return ErrValue99+ return value.ErrValue100100 }101101 return eval(args[i], get)102102 }
@@ -105,7 +105,7 @@ func choose(args []Node, get lookup) Value {
105105 // (or row), then return the index-th entry across.106106 func tableLookup(args []Node, get lookup, vertical bool) Value {107107 key := eval(args[0], get)108- if key.Kind == Error {108+ if key.Kind == value.Error {109109 return key110110 }111111 m := matrixArg(args[1], get)
@@ -124,9 +124,9 @@ func tableLookup(args []Node, get lookup, vertical bool) Value {
124124 }125125 switch {126126 case idx < 1:127- return ErrValue127+ return value.ErrValue128128 case idx > across:129- return ErrRef129+ return value.ErrRef130130 }131131 var i int132132 if sorted {
@@ -135,7 +135,7 @@ func tableLookup(args []Node, get lookup, vertical bool) Value {
135135 i = q.findExact(key, true)136136 }137137 if i < 0 {138- return ErrNA138+ return value.ErrNA139139 }140140 if vertical {141141 return m.cell(i, idx-1)
@@ -149,7 +149,7 @@ func lookupEqual(key, v Value, wild bool) bool {
149149 if key.Kind != v.Kind {150150 return false151151 }152- if key.Kind == Text {152+ if key.Kind == value.Text {153153 if wild && hasWildcards(key.Str) {154154 return wildMatch(key.Str, v.Str)155155 }
@@ -158,160 +158,9 @@ func lookupEqual(key, v Value, wild bool) bool {
158158 return key.Num == v.Num159159 }160160 161-// seq is the entries of a row or column searched by a lookup: n of them,162-// of which only the first data may hold data. With sparse, only those in163-// stored hold anything; every other entry is blank.164-type seq struct {165- n, data int166- at func(int) Value167- blank Value168- stored iter.Seq[int] // the entries holding cells, in order, when sparse169- sparse bool170-}171-172-// lineSeq is the first column (vertical) or row of m, as a lookup173-// searches it: through the cells it holds, when it is a range.174-func lineSeq(m matrix, get lookup, vertical bool) seq {175- line := m176- if m.ref {177- to := Addr{Col: m.origin.Col, Row: m.origin.Row + m.rows - 1}178- if !vertical {179- to = Addr{Col: m.origin.Col + m.cols - 1, Row: m.origin.Row}180- }181- line = rectMatrix(m.sheet, Rect{From: m.origin, To: to}, get)182- }183- q := seq{n: line.size(), data: min(line.dataLen(), line.size()), at: line.at, blank: line.blank}184- t := get.sheet(line.sheet)185- switch {186- case !line.ref:187- q.sparse, q.stored = true, func(yield func(int) bool) { yield(0) }188- case denseReads:189- case t == nil:190- q.sparse, q.stored = true, func(func(int) bool) {}191- default:192- q.sparse = true193- r := Rect{From: line.origin, To: Addr{Col: line.origin.Col + line.cols - 1, Row: line.origin.Row + line.rows - 1}}194- q.stored = func(yield func(int) bool) {195- if vertical {196- t.cells.colScan(r.From.Col, r.From.Row, r.To.Row, func(row int) bool { return yield(row - r.From.Row) })197- return198- }199- for a := range t.cells.inRange(r) {200- if !yield(a.Col - r.From.Col) {201- return202- }203- }204- }205- }206- return q207-}208-209-// findExact returns the first index whose value equals key, or -1.210-func (q seq) findExact(key Value, wild bool) int {211- if q.sparse {212- blank := lookupEqual(key, q.blank, wild)213- next := 0 // the first entry not looked at214- for i := range q.stored {215- if i > next && blank {216- return next // a blank before this cell217- }218- if lookupEqual(key, q.at(i), wild) {219- return i220- }221- next = i + 1222- }223- if blank && next < q.n {224- return next225- }226- return -1227- }228- for i := range q.data {229- if lookupEqual(key, q.at(i), wild) {230- return i231- }232- }233- if q.data < q.n && lookupEqual(key, q.blank, wild) {234- return q.data235- }236- return -1237-}238-239-// findSorted is the approximate match of sorted lookups: with dir 1 the240-// last value <= key in ascending data, with dir -1 the last value >= key241-// in descending data. Values of another kind are skipped.242-func (q seq) findSorted(key Value, dir int) int {243- found := -1244- if q.sparse && key.Kind != q.blank.Kind { // blanks are skipped: only the cells count245- for i := range q.stored {246- v := q.at(i)247- if v.Kind != key.Kind {248- continue249- }250- if compare(v, key)*dir > 0 {251- return found252- }253- found = i254- }255- return found256- }257- for i := range q.data {258- v := q.at(i)259- if v.Kind != key.Kind {260- continue261- }262- if c := compare(v, key) * dir; c > 0 {263- return found264- }265- found = i266- }267- if v := q.blank; q.data < q.n && v.Kind == key.Kind && compare(v, key)*dir <= 0 {268- found = q.n - 1 // the blanks past the data all match; the last wins269- }270- return found271-}272-273-// order is the entries a search visits, first to last (last to first274-// with reverse): those that hold cells, and of the blanks, which are all275-// the same, only the first met.276-func (q seq) order(reverse bool) []int {277- var out []int278- if !q.sparse {279- for i := range q.data {280- out = append(out, i)281- }282- if q.data < q.n {283- out = append(out, q.data)284- }285- if reverse {286- slices.Reverse(out)287- if q.data < q.n {288- out[0] = q.n - 1289- }290- }291- return out292- }293- out = slices.Collect(q.stored)294- gap := -1 // the first blank in search order295- if !reverse {296- for gap = 0; gap < len(out) && out[gap] == gap; gap++ {297- }298- } else {299- for gap = q.n - 1; len(out) > 0 && q.n-1-gap < len(out) && out[len(out)-1-(q.n-1-gap)] == gap; gap-- {300- }301- }302- if gap >= 0 && gap < q.n {303- i, _ := slices.BinarySearch(out, gap)304- out = slices.Insert(out, i, gap)305- }306- if reverse {307- slices.Reverse(out)308- }309- return out310-}311-312161 func xlookup(args []Node, get lookup) Value {313162 key := eval(args[0], get)314- if key.Kind == Error {163+ if key.Kind == value.Error {315164 return key316165 }317166 look := matrixArg(args[1], get)
@@ -326,9 +175,9 @@ func xlookup(args []Node, get lookup) Value {
326175 }327176 switch {328177 case !look.vector():329- return ErrValue178+ return value.ErrValue330179 case mode < -1 || mode > 2 || search == 0 || search < -2 || search > 2:331- return ErrValue180+ return value.ErrValue332181 }333182 // The result range lines up with the lookup range along its length.334183 n := look.size()
@@ -339,9 +188,10 @@ func xlookup(args []Node, get lookup) Value {
339188 case look.rows == 1 && res.cols == n:340189 pick = func(i int) Value { return res.cell(0, i) }341190 default:342- return ErrValue191+ return value.ErrValue343192 }344- order := lineSeq(look, get, look.cols == 1).order(search < 0)193+ q := lineSeq(look, get, look.cols == 1)194+ order := q.order(search < 0)345195 best := -1346196 for _, i := range order {347197 v := look.at(i)
@@ -362,7 +212,7 @@ func xlookup(args []Node, get lookup) Value {
362212 if given(args, 3) {363213 return eval(args[3], get)364214 }365- return ErrNA215+ return value.ErrNA366216 }367217 return pick(best)368218 }
R internal/sheet/masked.go → internal/functions/masked.go +24 -25
@@ -1,8 +1,10 @@
1-package sheet1+package functions22 33 import (44 "cmp"55 "slices"6+7+ "github.com/FelineStateMachine/012/internal/value"68 )79 810 // Functions over aligned ranges (SUMIF, COUNTIFS, SUMPRODUCT...) visit
@@ -22,18 +24,15 @@ func (m matrix) storedPos(get lookup) ([]pos, bool) {
2224 if !m.ref {2325 return []pos{{}}, true2426 }25- t := get.sheet(m.sheet)26- switch {27- case denseReads:27+ if get.dense {2828 return nil, false29- case t == nil:30- return nil, true3129 }3230 r := Rect{From: m.origin, To: Addr{Col: m.origin.Col + m.cols - 1, Row: m.origin.Row + m.rows - 1}}3331 var out []pos34- for a := range t.cells.inRange(r) {32+ get.stored(m.sheet, r, func(a Addr) bool {3533 out = append(out, pos{a.Row - r.From.Row, a.Col - r.From.Col})36- }34+ return true35+ })3736 return out, true3837 }3938
@@ -111,13 +110,13 @@ func passes(ms []matrix, cs []criterion, p pos) bool {
111110 func sumMasked(m matrix, k masked, add func(float64)) *Value {112111 for _, p := range k.pass {113112 switch v := m.cell(p.r, p.c); v.Kind {114- case Error:113+ case value.Error:115114 return &v116- case Number:115+ case value.Number:117116 add(v.Num)118117 }119118 }120- if k.tail > 0 && k.tailPass && m.blank.Kind == Error {119+ if k.tail > 0 && k.tailPass && m.blank.Kind == value.Error {121120 return &m.blank122121 }123122 return nil
@@ -128,18 +127,18 @@ func averageMasked(m matrix, k masked) Value {
128127 sum, n := 0.0, 0129128 for _, p := range k.pass {130129 switch v := m.cell(p.r, p.c); v.Kind {131- case Error:130+ case value.Error:132131 return v133- case Number:132+ case value.Number:134133 sum += v.Num135134 n++136135 }137136 }138137 switch {139- case k.tail > 0 && k.tailPass && m.blank.Kind == Error:138+ case k.tail > 0 && k.tailPass && m.blank.Kind == value.Error:140139 return m.blank141140 case n == 0:142- return ErrDiv0141+ return value.ErrDiv0143142 }144143 return num(sum / float64(n))145144 }
@@ -182,14 +181,14 @@ func sumProduct(args []Node, get lookup) Value {
182181 func sumIfTerms(args []Node, get lookup, add func(float64)) *Value {183182 rng := matrixArg(args[0], get)184183 cv := eval(args[1], get)185- if cv.Kind == Error {184+ if cv.Kind == value.Error {186185 return &cv187186 }188187 sum := rng189188 if len(args) > 2 {190189 sum = matrixArg(args[2], get).resized(rng.rows, rng.cols, get)191190 if sum.rows != rng.rows || sum.cols != rng.cols {192- return &ErrValue191+ return &value.ErrValue193192 }194193 }195194 return sumMasked(sum, applyCriteria(get, []matrix{rng}, []criterion{newCriterion(cv)}, sum), add)
@@ -204,7 +203,7 @@ func sumIfsTerms(args []Node, get lookup, add func(float64)) *Value {
204203 case err != nil:205204 return err206205 case len(ms) == 0 || ms[0].rows != sum.rows || ms[0].cols != sum.cols:207- return &ErrValue206+ return &value.ErrValue208207 }209208 return sumMasked(sum, applyCriteria(get, ms, cs, sum), add)210209 }
@@ -212,7 +211,7 @@ func sumIfsTerms(args []Node, get lookup, add func(float64)) *Value {
212211 func averageIf(args []Node, get lookup) Value {213212 rng := matrixArg(args[0], get)214213 cv := eval(args[1], get)215- if cv.Kind == Error {214+ if cv.Kind == value.Error {216215 return cv217216 }218217 avg := rng
@@ -229,7 +228,7 @@ func averageIfs(args []Node, get lookup) Value {
229228 case err != nil:230229 return *err231230 case len(ms) == 0 || ms[0].rows != avg.rows || ms[0].cols != avg.cols:232- return ErrValue231+ return value.ErrValue233232 }234233 return averageMasked(avg, applyCriteria(get, ms, cs, avg))235234 }
@@ -249,7 +248,7 @@ func countIfs(args []Node, get lookup) Value {
249248 250249 func countBlank(args []Node, get lookup) Value {251250 m := matrixArg(args[0], get)252- isBlank := func(v Value) bool { return v.Kind == Empty || v.Kind == Text && v.Str == "" }251+ isBlank := func(v Value) bool { return v.Kind == value.Empty || v.Kind == value.Text && v.Str == "" }253252 cells := cellsOf(get, m)254253 n := 0255254 for _, p := range cells {
@@ -272,7 +271,7 @@ func sumProductTerms(args []Node, get lookup, term func(fs []float64)) *Value {
272271 for i, a := range args {273272 ms[i] = matrixArg(a, get)274273 if ms[i].rows != ms[0].rows || ms[i].cols != ms[0].cols {275- return &ErrValue274+ return &value.ErrValue276275 }277276 }278277 cells := cellsOf(get, ms...)
@@ -280,9 +279,9 @@ func sumProductTerms(args []Node, get lookup, term func(fs []float64)) *Value {
280279 for _, pc := range cells {281280 for i, m := range ms {282281 switch v := m.cell(pc.r, pc.c); v.Kind {283- case Error:282+ case value.Error:284283 return &v285- case Number:284+ case value.Number:286285 fs[i] = v.Num287286 default:288287 fs[i] = 0
@@ -292,7 +291,7 @@ func sumProductTerms(args []Node, get lookup, term func(fs []float64)) *Value {
292291 }293292 if ms[0].size() > len(cells) {294293 for _, m := range ms {295- if m.blank.Kind == Error {294+ if m.blank.Kind == value.Error {296295 return &m.blank297296 }298297 }
R internal/sheet/functions_math.go → internal/functions/math.go +11 -10
@@ -1,10 +1,11 @@
1-package sheet1+package functions22 33 import (44 "math"55 "math/rand/v2"66 77 "github.com/FelineStateMachine/012/internal/numfmt"8+ "github.com/FelineStateMachine/012/internal/value"89 )910 1011 func init() {
@@ -16,7 +17,7 @@ func init() {
1617 &FuncDef{Name: "SUMPRODUCT", Args: "array1, [array2, ...]", Desc: "Sum of the products of matching entries", Min: 1, Max: -1,1718 eval: sumProduct},1819 &FuncDef{Name: "PRODUCT", Args: "factor1, [factor2, ...]", Desc: "Product of numbers", Min: 1, Max: -1,19- eval: aggregate(func(s agg) Value {20+ eval: aggregate(func(s Agg) Value {2021 if s.nums == 0 {2122 return num(0)2223 }
@@ -48,7 +49,7 @@ func init() {
4849 &FuncDef{Name: "LN", Args: "value", Desc: "Natural logarithm", Min: 1, Max: 1,4950 eval: numeric(func(x []float64) Value {5051 if x[0] <= 0 {51- return ErrNum52+ return value.ErrNum5253 }5354 return num(math.Log(x[0]))5455 })},
@@ -64,23 +65,23 @@ func init() {
6465 }6566 switch {6667 case x <= 0 || base <= 0:67- return ErrNum68+ return value.ErrNum6869 case base == 1:69- return ErrDiv070+ return value.ErrDiv07071 }7172 return num(math.Log(x) / math.Log(base))7273 }},7374 &FuncDef{Name: "LOG10", Args: "value", Desc: "Base-10 logarithm", Min: 1, Max: 1,7475 eval: numeric(func(x []float64) Value {7576 if x[0] <= 0 {76- return ErrNum77+ return value.ErrNum7778 }7879 return num(math.Log10(x[0]))7980 })},8081 &FuncDef{Name: "QUOTIENT", Args: "dividend, divisor", Desc: "Integer part of a division", Min: 2, Max: 2,8182 eval: numeric(func(x []float64) Value {8283 if x[1] == 0 {83- return ErrDiv084+ return value.ErrDiv08485 }8586 return num(math.Trunc(x[0] / x[1]))8687 })},
@@ -90,7 +91,7 @@ func init() {
9091 eval: numeric(func(x []float64) Value {9192 lo, hi := math.Ceil(x[0]), math.Floor(x[1])9293 if lo > hi {93- return ErrNum94+ return value.ErrNum9495 }9596 return num(lo + math.Floor(rand.Float64()*(hi-lo+1)))9697 })},
@@ -99,7 +100,7 @@ func init() {
99100 100101 func powerOf(b, e float64) Value {101102 if b == 0 && e < 0 {102- return ErrDiv0103+ return value.ErrDiv0103104 }104105 return num(math.Pow(b, e))105106 }
@@ -121,7 +122,7 @@ func toMultiple(round func(float64) float64) func([]Node, lookup) Value {
121122 case f == 0 || x == 0:122123 return num(0)123124 case x > 0 && f < 0:124- return ErrNum125+ return value.ErrNum125126 }126127 q := x / f127128 // Snap quotients like 2.0000000000000004 to the integer they mean.
A internal/functions/reader.go +165 -0
@@ -0,0 +1,165 @@
1+package functions2+3+import "github.com/FelineStateMachine/012/internal/value"4+5+// Book is how formulas reach the workbook: the engine implements it, once6+// for each sheet whose formulas it evaluates. Sheets are named as7+// references write them; "" is the formula's own sheet.8+//9+// Every method takes and returns plain values, never a callback: a10+// function value or pointer passed through an interface escapes to the11+// heap, so a callback per range read would cost an allocation where the12+// engine used to cost none. Ranges are read instead in chunks, into13+// buffers the Reader keeps and reuses (Scan), and each function walks14+// them with callbacks of its own that stay on the stack.15+type Book interface {16+ // Cell is the current value of a cell, evaluated first if it's17+ // dirty; #REF! on a sheet that doesn't exist.18+ Cell(sheet string, a Addr) Value19+ // Scan reads the cells of r that hold something, row by row, from20+ // the cell from on (those before it in that order are skipped): their21+ // addresses into addrs and, unless vals is nil, their values into22+ // vals, evaluating them as Cell does. It stops when addrs is full or23+ // just after a value that is an error, so no cell past the first24+ // error is evaluated, and returns how many it read: -1 when the sheet25+ // doesn't exist.26+ Scan(sheet string, r Rect, from Addr, addrs []Addr, vals []Value) int27+ // Bounds is the smallest range holding every stored cell of r, with28+ // false if r holds none; exists is false when the sheet doesn't.29+ Bounds(sheet string, r Rect) (b Rect, any, exists bool)30+ // RangeAgg is the aggregate of r that SUM-like functions share within31+ // a recalculation (see Agg), with the first error in it, or false32+ // when r should be read directly.33+ RangeAgg(sheet string, r Rect) (Agg, *Value, bool)34+ // Ask looks up the answer to a remote question (JEV functions). The35+ // value is an error when there is none to give: Pending while it is36+ // on its way, ErrNoRemote when nothing answers them.37+ Ask(call RemoteCall) (RemoteAnswer, Value)38+}39+40+// Reader is what a function is given to read the cells its arguments41+// refer to: a Book, and the buffers ranges are read through. It is a42+// concrete type so the callbacks functions pass to it stay on the stack.43+type Reader struct {44+ book Book45+ depth *int // how deeply evaluation is nested; see the engine's evaluate.go46+ dense bool // read every address of a range; see NewReader47+ bufs []*scanBuf48+ level int // how many of bufs are in use by range reads in progress49+}50+51+// lookup is the parameter every evaluator takes.52+type lookup = *Reader53+54+// NewReader reads cells through book. depth counts how deeply evaluation55+// nests (cells, operators and calls), for the engine to put off cells56+// past its limit. With dense, functions read every address of their57+// ranges, as they did before ranges were clipped to what they hold, so58+// tests can check that the two agree.59+func NewReader(book Book, depth *int, dense bool) *Reader {60+ return &Reader{book: book, depth: depth, dense: dense}61+}62+63+// A range is read in chunks that start small, for the many short ranges,64+// and double up to maxChunk as a range proves long. Nested range reads (a65+// SUM whose cells hold SUMs) each use a buffer of their own.66+const (67+ firstChunk = 868+ maxChunk = 102469+)70+71+type scanBuf struct {72+ addrs []Addr73+ vals []Value74+}75+76+// take returns the buffer for a range read starting now; give returns it.77+func (rd *Reader) take() *scanBuf {78+ if rd.level == len(rd.bufs) {79+ rd.bufs = append(rd.bufs, &scanBuf{addrs: make([]Addr, firstChunk), vals: make([]Value, firstChunk)})80+ }81+ b := rd.bufs[rd.level]82+ rd.level++83+ return b84+}85+86+func (rd *Reader) give() { rd.level-- }87+88+// fit makes the buffer hold at least n cells.89+func (b *scanBuf) fit(n int) {90+ if len(b.addrs) < n {91+ b.addrs, b.vals = make([]Addr, n), make([]Value, n)92+ }93+}94+95+// next is the cell after a in r, row by row.96+func next(r Rect, a Addr) Addr {97+ if a.Col < r.To.Col {98+ return Addr{Col: a.Col + 1, Row: a.Row}99+ }100+ return Addr{Col: r.From.Col, Row: a.Row + 1}101+}102+103+// cell returns the current value of a cell.104+func (rd *Reader) cell(sheet string, a Addr) Value { return rd.book.Cell(sheet, a) }105+106+// cells calls fn with the address and value of every cell of r that107+// holds something, row by row, until fn returns false: blank cells are108+// skipped without being visited, so a whole column costs what it holds.109+// A range on a sheet that doesn't exist reads as one #REF!. Cells are110+// evaluated a chunk ahead of fn, but never past an error: every caller111+// stops at the first error, so none evaluates a cell it wouldn't have112+// read one at a time.113+func (rd *Reader) cells(sheet string, r Rect, fn func(Addr, Value) bool) {114+ b := rd.take()115+ defer rd.give()116+ for from := r.From; from.Row <= r.To.Row; {117+ n := rd.book.Scan(sheet, r, from, b.addrs, b.vals)118+ if n < 0 {119+ fn(r.From, value.ErrRef)120+ return121+ }122+ for i := range n {123+ if !fn(b.addrs[i], b.vals[i]) {124+ return125+ }126+ }127+ switch {128+ case n == len(b.addrs):129+ from = next(r, b.addrs[n-1])130+ b.fit(min(2*n, maxChunk))131+ case n > 0 && b.vals[n-1].Kind == value.Error:132+ from = next(r, b.addrs[n-1])133+ default:134+ return135+ }136+ }137+}138+139+// stored calls fn with every cell of r that holds something, row by row,140+// until fn returns false, without evaluating any. A sheet that doesn't141+// exist holds none. Lookups stop at the first match, so each call starts142+// with a small chunk, however long earlier ranges were, and doubles it143+// as the range goes on: finding a key near the top of a whole column144+// costs the cells before it.145+func (rd *Reader) stored(sheet string, r Rect, fn func(Addr) bool) {146+ b := rd.take()147+ defer rd.give()148+ size := firstChunk149+ for from := r.From; from.Row <= r.To.Row; {150+ n := rd.book.Scan(sheet, r, from, b.addrs[:size], nil)151+ for i := range n {152+ if !fn(b.addrs[i]) {153+ return154+ }155+ }156+ if n < size {157+ return158+ }159+ from = next(r, b.addrs[n-1])160+ if size < maxChunk {161+ size *= 2162+ b.fit(size)163+ }164+ }165+}
A internal/functions/reader_test.go +188 -0
@@ -0,0 +1,188 @@
1+package functions2+3+import (4+ "slices"5+ "testing"6+7+ "github.com/FelineStateMachine/012/internal/value"8+)9+10+// fakeBook is one sheet of values, "", for testing the Reader alone. It11+// counts the values it reads and the cells it scans.12+type fakeBook struct {13+ cells map[Addr]Value14+ read int // values read, as evaluating them would15+ scanned int // cells found by Scan16+}17+18+func (b *fakeBook) Cell(sheet string, a Addr) Value {19+ if sheet != "" {20+ return value.ErrRef21+ }22+ b.read++23+ return b.cells[a]24+}25+26+func (b *fakeBook) Scan(sheet string, r Rect, from Addr, addrs []Addr, vals []Value) int {27+ if sheet != "" {28+ return -129+ }30+ var in []Addr31+ for a := range b.cells {32+ if r.Contains(a) && (a.Row > from.Row || a.Row == from.Row && a.Col >= from.Col) {33+ in = append(in, a)34+ }35+ }36+ slices.SortFunc(in, func(x, y Addr) int {37+ if x.Row != y.Row {38+ return x.Row - y.Row39+ }40+ return x.Col - y.Col41+ })42+ n := 043+ for _, a := range in {44+ if n == len(addrs) {45+ break46+ }47+ addrs[n] = a48+ n++49+ b.scanned++50+ if vals != nil {51+ vals[n-1] = b.Cell(sheet, a)52+ if vals[n-1].Kind == value.Error {53+ break54+ }55+ }56+ }57+ return n58+}59+60+func (b *fakeBook) Bounds(string, Rect) (Rect, bool, bool) { return Rect{}, false, true }61+func (b *fakeBook) RangeAgg(string, Rect) (Agg, *Value, bool) { return Agg{}, nil, false }62+func (b *fakeBook) Ask(RemoteCall) (RemoteAnswer, Value) { return RemoteAnswer{}, ErrNoRemote }63+func newFakeReader(cells map[Addr]Value) (*Reader, *fakeBook) {64+ b := &fakeBook{cells: cells}65+ depth := 066+ return NewReader(b, &depth, false), b67+}68+69+// column is n numbers 1..n down column A, every other row, and a 3-wide70+// block of the same beside them in C:E.71+func column(n int) map[Addr]Value {72+ cells := map[Addr]Value{}73+ for i := range n {74+ cells[Addr{Row: 2 * i}] = num(float64(i + 1))75+ for c := 2; c <= 4; c++ {76+ cells[Addr{Col: c, Row: 2 * i}] = num(float64(i + 1))77+ }78+ }79+ return cells80+}81+82+func TestReaderCells(t *testing.T) {83+ const n = 300084+ rd, _ := newFakeReader(column(n))85+ for _, r := range []Rect{86+ {To: Addr{Row: 2*n - 1}}, // one column, in chunks87+ {From: Addr{Col: 2}, To: Addr{Col: 4, Row: 2*n - 1}}, // three, resuming mid-row88+ } {89+ width := r.To.Col - r.From.Col + 190+ var got []float6491+ rd.cells("", r, func(a Addr, v Value) bool {92+ got = append(got, v.Num)93+ return true94+ })95+ if len(got) != n*width {96+ t.Fatalf("%v: read %d cells, want %d", r, len(got), n*width)97+ }98+ for i, f := range got {99+ if f != float64(i/width+1) {100+ t.Fatalf("%v: cell %d is %v, want %v", r, i, f, i/width+1)101+ }102+ }103+ }104+ if rd.level != 0 {105+ t.Errorf("level %d after reads, want 0", rd.level)106+ }107+}108+109+// Ranges are read ahead in chunks, but no cell past the first error is110+// evaluated, as when cells were read one at a time.111+func TestReaderStopsAtError(t *testing.T) {112+ cells := column(100)113+ cells[Addr{Row: 2 * 40}] = value.ErrDiv0114+ rd, b := newFakeReader(cells)115+ seen := 0116+ var e Value117+ rd.cells("", Rect{To: Addr{Row: 199}}, func(_ Addr, v Value) bool {118+ seen++119+ e = v120+ return v.Kind != value.Error121+ })122+ if seen != 41 || e != value.ErrDiv0 || b.read != 41 {123+ t.Errorf("saw %d cells, evaluated %d, last %v; want 41, 41 and #DIV/0!", seen, b.read, e)124+ }125+}126+127+// A lookup that stops early scans about as many cells as it looked at,128+// however long the range and however long the last one read was.129+func TestReaderStoredStopsEarly(t *testing.T) {130+ rd, b := newFakeReader(column(5000))131+ whole := Rect{To: Addr{Row: 9999}}132+ rd.stored("", whole, func(Addr) bool { return true }) // grows the buffer133+ b.scanned = 0134+ seen := 0135+ rd.stored("", whole, func(Addr) bool { seen++; return seen < 20 })136+ if seen != 20 || b.scanned > 2*seen+firstChunk {137+ t.Errorf("looked at %d cells and scanned %d; want 20 and at most %d", seen, b.scanned, 2*seen+firstChunk)138+ }139+ if b.read != 0 {140+ t.Errorf("evaluated %d cells, want none", b.read)141+ }142+}143+144+// A read nested in another (a cell of the range evaluating a range of its145+// own) uses a buffer of its own.146+func TestReaderNested(t *testing.T) {147+ rd, _ := newFakeReader(column(50))148+ outer := 0.0149+ rd.cells("", Rect{To: Addr{Row: 99}}, func(_ Addr, v Value) bool {150+ inner := 0.0151+ rd.cells("", Rect{From: Addr{Col: 2}, To: Addr{Col: 4, Row: 99}}, func(_ Addr, w Value) bool {152+ inner += w.Num153+ return true154+ })155+ if inner != 3*50*51/2 {156+ t.Fatalf("inner sum %v", inner)157+ }158+ outer += v.Num159+ return true160+ })161+ if outer != 50*51/2 || rd.level != 0 {162+ t.Errorf("outer sum %v, level %d", outer, rd.level)163+ }164+}165+166+func TestReaderMissingSheet(t *testing.T) {167+ rd, _ := newFakeReader(column(3))168+ var got []Value169+ rd.cells("Nowhere", Rect{To: Addr{Row: 9}}, func(_ Addr, v Value) bool {170+ got = append(got, v)171+ return true172+ })173+ if len(got) != 1 || got[0] != value.ErrRef {174+ t.Errorf("got %v, want one #REF!", got)175+ }176+ rd.stored("Nowhere", Rect{To: Addr{Row: 9}}, func(Addr) bool {177+ t.Error("a missing sheet holds a cell")178+ return true179+ })180+}181+182+func TestFunctionTable(t *testing.T) {183+ for _, f := range Funcs() {184+ if f.Desc == "" || (f.Max != 0 && f.Args == "") || f.eval == nil {185+ t.Errorf("%s is missing its description, signature or implementation", f.Name)186+ }187+ }188+}
A internal/functions/remote.go +46 -0
@@ -0,0 +1,46 @@
1+package functions2+3+import "github.com/FelineStateMachine/012/internal/value"4+5+// Some functions (JEV.*) are answered by a hosted model. The library6+// never talks to the network: it describes each question as a RemoteCall7+// and asks the Book for the answer, which the engine looks up in the8+// workbook's RemoteSource. Until the answer arrives the cell shows9+// Loading…, and the source queues the call.10+11+// RemoteCall is one question for the model, built from a function's12+// arguments. It isn't comparable: Key identifies it by its content.13+type RemoteCall struct {14+ Kind string `json:"kind"` // "noul", "choice" or "score"15+ State any `json:"state"`16+ Instructions string `json:"instructions"`17+ // Criteria depends on Kind: noul is [2]string{yes, no} (either may be18+ // empty), choice is map[label]description, score is []string levels.19+ Criteria any `json:"criteria"`20+}21+22+// RemoteAnswer is the model's answer. Which fields are set depends on the23+// call's Kind; Failed explains an answer that couldn't be had.24+type RemoteAnswer struct {25+ Noul float64 // probability of yes26+ Choice string27+ Score float6428+ Confidence float64 // in Choice or Score, not a probability of truth29+ Failed string30+}31+32+// RemoteSource answers RemoteCalls. Lookup returns false while an answer33+// isn't known, and queues the call.34+type RemoteSource interface {35+ Lookup(RemoteCall) (RemoteAnswer, bool)36+}37+38+var (39+ // Pending is shown while an answer is on its way.40+ Pending = Value{Kind: value.Error, Str: "Loading…"}41+ // ErrNoRemote means JEV functions can't run: there is no API key.42+ ErrNoRemote = Value{Kind: value.Error, Str: "#N/A"}43+)44+45+// IsPending reports whether v is waiting for a remote answer.46+func IsPending(v Value) bool { return v == Pending }
R internal/sheet/remotekey.go → internal/functions/remotekey.go +1 -1
A internal/functions/seq.go +191 -0
@@ -0,0 +1,191 @@
1+package functions2+3+import "slices"4+5+// seq is the entries of a row or column searched by a lookup: n of them,6+// of which only the first data may hold data. With sparse, only the7+// entries holding cells (entries, positions) hold anything; every other8+// entry is blank.9+type seq struct {10+ n, data int11+ line matrix // the row or column12+ blank Value13+ sparse bool14+15+ get lookup16+ r Rect // the line's cells, when it is a range17+ vertical bool18+}19+20+// lineSeq is the first column (vertical) or row of m, as a lookup21+// searches it: through the cells it holds, when it is a range.22+func lineSeq(m matrix, get lookup, vertical bool) seq {23+ line := m24+ if m.ref {25+ to := Addr{Col: m.origin.Col, Row: m.origin.Row + m.rows - 1}26+ if !vertical {27+ to = Addr{Col: m.origin.Col + m.cols - 1, Row: m.origin.Row}28+ }29+ line = rectMatrix(m.sheet, Rect{From: m.origin, To: to}, get)30+ }31+ return seq{32+ n: line.size(), data: min(line.dataLen(), line.size()), line: line, blank: line.blank,33+ sparse: !line.ref || !get.dense,34+ get: get, vertical: vertical,35+ r: Rect{From: line.origin, To: Addr{Col: line.origin.Col + line.cols - 1, Row: line.origin.Row + line.rows - 1}},36+ }37+}38+39+// at is entry i's value.40+func (q *seq) at(i int) Value { return q.line.at(i) }41+42+// value is the value of entry i, whose cell is at a when the line is a43+// range.44+func (q *seq) value(i int, a Addr) Value {45+ if !q.line.ref {46+ return q.at(i)47+ }48+ return q.get.cell(q.line.sheet, a)49+}50+51+// index is the entry of the line's cell at a.52+func (q *seq) index(a Addr) int {53+ if q.vertical {54+ return a.Row - q.r.From.Row55+ }56+ return a.Col - q.r.From.Col57+}58+59+// entries calls fn with the entries holding cells, in order, and where60+// their cells are, until fn returns false. It evaluates nothing: a search61+// reads the value of each entry it reaches (q.value), and no more.62+func (q *seq) entries(fn func(i int, a Addr) bool) {63+ if !q.line.ref {64+ fn(0, Addr{})65+ return66+ }67+ q.get.stored(q.line.sheet, q.r, func(a Addr) bool { return fn(q.index(a), a) })68+}69+70+// positions is the entries holding cells, in order.71+func (q *seq) positions() []int {72+ if !q.line.ref {73+ return []int{0}74+ }75+ var out []int76+ q.get.stored(q.line.sheet, q.r, func(a Addr) bool {77+ out = append(out, q.index(a))78+ return true79+ })80+ return out81+}82+83+// findExact returns the first index whose value equals key, or -1.84+func (q *seq) findExact(key Value, wild bool) int {85+ if q.sparse {86+ blank := lookupEqual(key, q.blank, wild)87+ next, found := 0, -1 // next is the first entry not looked at88+ q.entries(func(i int, a Addr) bool {89+ switch {90+ case i > next && blank:91+ found = next // a blank before this cell92+ case lookupEqual(key, q.value(i, a), wild):93+ found = i94+ default:95+ next = i + 196+ return true97+ }98+ return false99+ })100+ switch {101+ case found >= 0:102+ return found103+ case blank && next < q.n:104+ return next105+ }106+ return -1107+ }108+ for i := range q.data {109+ if lookupEqual(key, q.at(i), wild) {110+ return i111+ }112+ }113+ if q.data < q.n && lookupEqual(key, q.blank, wild) {114+ return q.data115+ }116+ return -1117+}118+119+// findSorted is the approximate match of sorted lookups: with dir 1 the120+// last value <= key in ascending data, with dir -1 the last value >= key121+// in descending data. Values of another kind are skipped.122+func (q *seq) findSorted(key Value, dir int) int {123+ found := -1124+ if q.sparse && key.Kind != q.blank.Kind { // blanks are skipped: only the cells count125+ q.entries(func(i int, a Addr) bool {126+ v := q.value(i, a)127+ if v.Kind != key.Kind {128+ return true129+ }130+ if compare(v, key)*dir > 0 {131+ return false132+ }133+ found = i134+ return true135+ })136+ return found137+ }138+ for i := range q.data {139+ v := q.at(i)140+ if v.Kind != key.Kind {141+ continue142+ }143+ if c := compare(v, key) * dir; c > 0 {144+ return found145+ }146+ found = i147+ }148+ if v := q.blank; q.data < q.n && v.Kind == key.Kind && compare(v, key)*dir <= 0 {149+ found = q.n - 1 // the blanks past the data all match; the last wins150+ }151+ return found152+}153+154+// order is the entries a search visits, first to last (last to first155+// with reverse): those that hold cells, and of the blanks, which are all156+// the same, only the first met.157+func (q *seq) order(reverse bool) []int {158+ var out []int159+ if !q.sparse {160+ for i := range q.data {161+ out = append(out, i)162+ }163+ if q.data < q.n {164+ out = append(out, q.data)165+ }166+ if reverse {167+ slices.Reverse(out)168+ if q.data < q.n {169+ out[0] = q.n - 1170+ }171+ }172+ return out173+ }174+ out = q.positions()175+ gap := -1 // the first blank in search order176+ if !reverse {177+ for gap = 0; gap < len(out) && out[gap] == gap; gap++ {178+ }179+ } else {180+ for gap = q.n - 1; len(out) > 0 && q.n-1-gap < len(out) && out[len(out)-1-(q.n-1-gap)] == gap; gap-- {181+ }182+ }183+ if gap >= 0 && gap < q.n {184+ i, _ := slices.BinarySearch(out, gap)185+ out = slices.Insert(out, i, gap)186+ }187+ if reverse {188+ slices.Reverse(out)189+ }190+ return out191+}
R internal/sheet/functions_stats.go → internal/functions/stats.go +8 -6
R internal/sheet/functions_text.go → internal/functions/text.go +22 -19
@@ -1,8 +1,11 @@
1-package sheet1+package functions22 33 import (44 "strings"55 "unicode"6+7+ "github.com/FelineStateMachine/012/internal/numfmt"8+ "github.com/FelineStateMachine/012/internal/value"69 )710 811 // maxText is the longest text a function may build, as in Sheets.
@@ -111,7 +114,7 @@ func mid(args []Node, get lookup) Value {
111114 return *err112115 }113116 if start < 1 || n < 0 {114- return ErrValue117+ return value.ErrValue115118 }116119 r := []rune(s)117120 if start > len(r) {
@@ -142,7 +145,7 @@ func replaceAt(args []Node, get lookup) Value {
142145 return *err143146 }144147 if pos < 1 || n < 0 {145- return ErrValue148+ return value.ErrValue146149 }147150 r := []rune(s)148151 from := min(pos-1, len(r))
@@ -152,40 +155,40 @@ func replaceAt(args []Node, get lookup) Value {
152155 153156 func textFormat(args []Node, get lookup) Value {154157 v := eval(args[0], get)155- if v.Kind == Error {158+ if v.Kind == value.Error {156159 return v157160 }158161 pat, err := textArg(args[1], get)159162 if err != nil {160163 return *err161164 }162- if v.Kind == Text {163- n, _, ok := ParseValue(v.Str)165+ if v.Kind == value.Text {166+ n, _, ok := value.ParseValue(v.Str)164167 if !ok {165168 return v166169 }167170 v = num(n)168171 }169- return str(FormatPattern(v.Num, pat))172+ return str(numfmt.Format(v.Num, pat))170173 }171174 172175 func valueOf(args []Node, get lookup) Value {173176 v := eval(args[0], get)174177 switch v.Kind {175- case Error, Number:178+ case value.Error, value.Number:176179 return v177- case Empty:180+ case value.Empty:178181 return num(0)179- case Bool:180- return ErrValue182+ case value.Bool:183+ return value.ErrValue181184 }182- if n, _, ok := ParseValue(strings.TrimSpace(v.Str)); ok {185+ if n, _, ok := value.ParseValue(strings.TrimSpace(v.Str)); ok {183186 return num(n)184187 }185188 if strings.TrimSpace(v.Str) == "" {186189 return num(0)187190 }188- return ErrValue191+ return value.ErrValue189192 }190193 191194 func rept(args []Node, get lookup) Value {
@@ -198,7 +201,7 @@ func rept(args []Node, get lookup) Value {
198201 return *err199202 }200203 if n < 0 || n*len(s) > maxText {201- return ErrValue204+ return value.ErrValue202205 }203206 return str(strings.Repeat(s, n))204207 }
@@ -217,7 +220,7 @@ func exact(args []Node, get lookup) Value {
217220 218221 func capText(s string) Value {219222 if len(s) > maxText {220- return ErrValue223+ return value.ErrValue221224 }222225 return str(s)223226 }
@@ -254,7 +257,7 @@ func textSlice(f func(r []rune, n int) string) func([]Node, lookup) Value {
254257 return *err255258 }256259 if n < 0 {257- return ErrValue260+ return value.ErrValue258261 }259262 return str(f([]rune(s), n))260263 }
@@ -300,7 +303,7 @@ func substitute(args []Node, get lookup) Value {
300303 case err != nil:301304 return *err302305 case nth < 1:303- return ErrValue306+ return value.ErrValue304307 case old == "":305308 return str(s)306309 }
@@ -336,7 +339,7 @@ func textFinder(search bool) func([]Node, lookup) Value {
336339 }337340 r := []rune(hay)338341 if start < 1 || start > len(r)+1 {339- return ErrValue342+ return value.ErrValue340343 }341344 if needle == "" {342345 return num(float64(start))
@@ -352,7 +355,7 @@ func textFinder(search bool) func([]Node, lookup) Value {
352355 return num(float64(i + 1))353356 }354357 }355- return ErrValue358+ return value.ErrValue356359 }357360 }358361
A internal/functions/values.go +41 -0
@@ -0,0 +1,41 @@
1+// Package functions is 012's function library: the FuncDef table, the2+// evaluation of formulas (operators and calls), the argument, range and3+// criteria helpers functions share, decimal arithmetic, and the4+// questions JEV functions ask.5+//6+// It knows nothing of sheets or storage. A formula reads cells through a7+// Reader over a Book, which the engine (internal/sheet) implements, and8+// the engine finds functions through the table (LookupFunc) when it9+// parses formulas. Dependencies point one way: formula and value below,10+// the engine above.11+package functions12+13+import (14+ "github.com/FelineStateMachine/012/internal/formula"15+ "github.com/FelineStateMachine/012/internal/value"16+)17+18+// The library works in the formula and value packages' terms; these19+// names keep its tables short, as the engine's do.20+type (21+ // Value is the computed contents of a cell.22+ Value = value.Value23+ // Format is a cell's number format.24+ Format = value.Format25+ // Node is a parsed formula expression.26+ Node = formula.Node27+ // Addr identifies a cell by zero-based column and row.28+ Addr = formula.Addr29+ // Rect is an inclusive rectangular range of cells.30+ Rect = formula.Rect31+)32+33+func num(v float64) Value { return value.Num(v) }34+func boolean(b bool) Value { return value.Boolean(b) }35+func str(s string) Value { return value.Str(s) }36+func errOf(v Value) *Value { return value.ErrOf(v) }37+func toNum(v Value) (float64, *Value) { return value.ToNum(v) }38+func text(v Value) string { return value.AsText(v) }39+func compare(l, r Value) int { return value.Compare(l, r) }40+41+func clampInt(v, lo, hi int) int { return max(lo, min(v, hi)) }
M internal/sheet/cellformat.go +0 -96
@@ -1,7 +1,5 @@
11 package sheet22 3-import "github.com/FelineStateMachine/012/internal/formula"4-53 // Formatting changes go through these methods rather than by editing64 // cells directly, so history (undo) can hook a single place.75
@@ -105,97 +103,3 @@ func (s *Sheet) eachCell(r Rect, create bool, fn func(Addr, *lineFmt)) {
105103 s.placeFormat(a, want)106104 }107105 }108-109-// isVolatile reports whether n calls a function whose result changes110-// without its inputs changing (TODAY, NOW, RAND).111-func isVolatile(n Node) bool {112- switch n := n.(type) {113- case formula.Unary:114- return isVolatile(n.X)115- case formula.Binary:116- return isVolatile(n.L) || isVolatile(n.R)117- case formula.Call:118- if funcOf(n).Volatile {119- return true120- }121- for _, a := range n.Args {122- if isVolatile(a) {123- return true124- }125- }126- }127- return false128-}129-130-// inferFormat picks the format Sheets shows a formula's result in when131-// the cell is Automatic: dates from date functions, and otherwise the132-// format of the first formatted input, so =B2+B3 of currency shows133-// currency and a date plus days shows a date.134-func inferFormat(n Node, at func(string, Addr) Format) Format {135- switch n := n.(type) {136- case formula.Ref:137- return at(n.Sheet, n.Addr)138- case formula.Range:139- return at(n.Sheet, n.Rect.From)140- case formula.Unary:141- if n.Op == "-" || n.Op == "+" {142- return inferFormat(n.X, at)143- }144- case formula.Binary:145- l, r := inferFormat(n.L, at), inferFormat(n.R, at)146- switch n.Op {147- case "+", "-":148- if n.Op == "-" && l.Kind.IsTime() && r.Kind.IsTime() {149- return Format{} // days between two dates150- }151- return firstFormat(l, r)152- case "*", "/":153- if l.Kind.IsTime() || r.Kind.IsTime() {154- return Format{}155- }156- return firstFormat(l, r)157- }158- case formula.Call:159- if funcOf(n).format != nil {160- return funcOf(n).format(n.Args, func(n Node) Format { return inferFormat(n, at) })161- }162- }163- return Format{}164-}165-166-func firstFormat(fs ...Format) Format {167- for _, f := range fs {168- if !f.IsZero() && f.Kind != FmtText {169- return f170- }171- }172- return Format{}173-}174-175-// Result formats for the function table.176-177-// returns makes a function's result show in format f.178-func returns(f Format) func([]Node, func(Node) Format) Format {179- return func([]Node, func(Node) Format) Format { return f }180-}181-182-// inherit makes a function's result take the format of its first183-// formatted argument (SUM, MIN, ROUND...).184-func inherit(args []Node, infer func(Node) Format) Format {185- for _, a := range args {186- if f := infer(a); !f.IsZero() && f.Kind != FmtText {187- return f188- }189- }190- return Format{}191-}192-193-// inheritFrom takes the format of argument i only.194-func inheritFrom(i int) func([]Node, func(Node) Format) Format {195- return func(args []Node, infer func(Node) Format) Format {196- if i < len(args) {197- return infer(args[i])198- }199- return Format{}200- }201-}
M internal/sheet/decimal.go +4 -314
@@ -1,325 +1,15 @@
11 package sheet22 3-import (4- "sync"3+import "github.com/FelineStateMachine/012/internal/functions"54 6- "github.com/FelineStateMachine/012/internal/formula"7- "github.com/FelineStateMachine/012/internal/numfmt"8- "github.com/cockroachdb/apd/v3"9-)10-11-// Decimal arithmetic is an opt-in, per-workbook calculation setting for12-// money: with it, 0.1+0.2 is exactly 0.3, so =0.1+0.2=0.3 is TRUE and a13-// column of cents sums to what an accountant expects. Sheets and Excel use14-// binary floating point, so it is off by default.15-//16-// The boundary is deliberately small. Values stay float64 everywhere (in17-// cells, the file, formats and every other function); only these steps18-// compute in decimal:19-//20-// - the operators + - * / and the postfix %21-// - SUM, AVERAGE, PRODUCT, SUMIF, SUMIFS and SUMPRODUCT22-// - ROUND, ROUNDUP, ROUNDDOWN and TRUNC23-//24-// Each step reads its float64 operands as the shortest decimal that25-// round-trips (what the cell shows and what was typed: 0.1, not26-// 0.1000000000000000055...), computes exactly or to 34 significant digits27-// (IEEE decimal128) for division, and stores the float64 nearest to that28-// decimal result. Comparisons, lookups and everything else then work on29-// those float64s, so they see 0.3 rather than 0.30000000000000004.30-// Exponents (^), statistics, finance and date functions stay binary. A31-// step the decimal context can't represent (overflow past float64 range)32-// falls back to the binary result, so the setting never adds errors.33-//34-// Like any decimal system, it doesn't make 1/3*3 equal 1: the quotient is35-// rounded, and a float64 holds about 17 digits of it.36-37-// decCtx is the context of every decimal step: 34 digits, as decimal128.38-var decCtx = func() *apd.Context {39- c := apd.BaseContext.WithPrecision(34)40- c.Rounding = apd.RoundHalfEven41- return c42-}()43-44-// fromDec returns the value nearest to d, or false if it is out of45-// float64 range.46-func fromDec(d *apd.Decimal) (Value, bool) {47- f, err := d.Float64()48- if err != nil {49- return Value{}, false50- }51- return num(f), true52-}53-54-// decArith computes a op b in decimal, or reports false for operators55-// that stay binary and for results it can't represent.56-func decArith(op string, a, b float64) (Value, bool) {57- var x, y, z apd.Decimal58- if _, err := x.SetFloat64(a); err != nil {59- return Value{}, false60- }61- if _, err := y.SetFloat64(b); err != nil {62- return Value{}, false63- }64- var err error65- switch op {66- case "+":67- _, err = decCtx.Add(&z, &x, &y)68- case "-":69- _, err = decCtx.Sub(&z, &x, &y)70- case "*":71- _, err = decCtx.Mul(&z, &x, &y)72- case "/":73- if b == 0 {74- return ErrDiv0, true75- }76- _, err = decCtx.Quo(&z, &x, &y)77- default:78- return Value{}, false79- }80- if err != nil {81- return Value{}, false82- }83- return fromDec(&z)84-}85-86-// decRounding maps ROUND's modes to apd's: half away from zero, away87-// from zero, toward zero.88-var decRounding = [...]apd.Rounder{numfmt.HalfUp: apd.RoundHalfUp, numfmt.Up: apd.RoundUp, numfmt.Down: apd.RoundDown}89-90-// decRound rounds x to places decimal places (negative places round to91-// tens, hundreds...) in decimal.92-func decRound(x float64, places int, mode numfmt.Rounding) (Value, bool) {93- var d, z apd.Decimal94- if _, err := d.SetFloat64(x); err != nil {95- return Value{}, false96- }97- c := *decCtx98- c.Rounding = decRounding[mode]99- if _, err := c.Quantize(&z, &d, int32(-places)); err != nil {100- return Value{}, false101- }102- return fromDec(&z)103-}104-105-// decAcc accumulates a sum or a product in decimal; ok turns false when106-// a step can't be represented, and the caller falls back to binary.107-type decAcc struct {108- d, x apd.Decimal109- ok bool110-}111-112-func newDecAcc(start int64) *decAcc {113- a := &decAcc{ok: true}114- a.d.SetInt64(start)115- return a116-}117-118-func (a *decAcc) add(f float64) { a.step(f, decCtx.Add) }119-func (a *decAcc) mul(f float64) { a.step(f, decCtx.Mul) }120-121-func (a *decAcc) step(f float64, op func(d, x, y *apd.Decimal) (apd.Condition, error)) {122- if !a.ok {123- return124- }125- if _, err := a.x.SetFloat64(f); err != nil {126- a.ok = false127- } else if _, err := op(&a.d, &a.d, &a.x); err != nil {128- a.ok = false129- }130-}131-132-// addDec adds a decimal to the sum.133-func (a *decAcc) addDec(d *apd.Decimal) {134- if !a.ok {135- return136- }137- if _, err := decCtx.Add(&a.d, &a.d, d); err != nil {138- a.ok = false139- }140-}141-142-// result is the accumulated value, or false to fall back.143-func (a *decAcc) result() (Value, bool) {144- if !a.ok {145- return Value{}, false146- }147- return fromDec(&a.d)148-}149-150-// decFold feeds the numbers SUM and PRODUCT take to step and counts151-// them: in ranges only numbers, direct arguments coerced.152-func decFold(args []Node, get lookup, step func(float64)) (n int, e *Value) {153- e = each(args, get, func(v Value, direct bool) *Value {154- switch v.Kind {155- case Error:156- return &v157- case Empty:158- return nil159- }160- if v.Kind != Number && !direct {161- return nil162- }163- f, err := toNum(v)164- if err != nil {165- return err166- }167- n++168- step(f)169- return nil170- })171- return n, e172-}173-174-// decTerms builds the decimal twin of a function that visits the terms175-// it adds (SUMIF, SUMIFS).176-func decTerms(terms func([]Node, lookup, func(float64)) *Value) func([]Node, lookup) (Value, bool) {177- return func(args []Node, get lookup) (Value, bool) {178- acc := newDecAcc(0)179- if e := terms(args, get, acc.add); e != nil {180- return *e, true181- }182- return acc.result()183- }184-}185-186-// decSumProduct is SUMPRODUCT with each entry's product and the sum in187-// decimal.188-func decSumProduct(args []Node, get lookup) (Value, bool) {189- acc := newDecAcc(0)190- e := sumProductTerms(args, get, func(fs []float64) {191- p := newDecAcc(1)192- for _, f := range fs {193- p.mul(f)194- }195- if !p.ok {196- acc.ok = false197- }198- acc.addDec(&p.d)199- })200- if e != nil {201- return *e, true202- }203- return acc.result()204-}205-206-// decEvals are the decimal versions of functions, by name. Each reports207-// false to fall back to the binary version.208-var decEvals = map[string]func(args []Node, get lookup) (Value, bool){209- "SUM": func(args []Node, get lookup) (Value, bool) {210- acc := newDecAcc(0)211- if _, e := decFold(args, get, acc.add); e != nil {212- return *e, true213- }214- return acc.result()215- },216- "AVERAGE": func(args []Node, get lookup) (Value, bool) {217- acc := newDecAcc(0)218- n, e := decFold(args, get, acc.add)219- switch {220- case e != nil:221- return *e, true222- case n == 0:223- return ErrDiv0, true224- case !acc.ok:225- return Value{}, false226- }227- var q apd.Decimal228- if _, err := decCtx.Quo(&q, &acc.d, apd.New(int64(n), 0)); err != nil {229- return Value{}, false230- }231- return fromDec(&q)232- },233- "PRODUCT": func(args []Node, get lookup) (Value, bool) {234- acc := newDecAcc(1)235- n, e := decFold(args, get, acc.mul)236- switch {237- case e != nil:238- return *e, true239- case n == 0:240- return num(0), true241- }242- return acc.result()243- },244- "SUMIF": decTerms(sumIfTerms),245- "SUMIFS": decTerms(sumIfsTerms),246- "SUMPRODUCT": decSumProduct,247- "ROUND": decRounder(numfmt.HalfUp),248- "ROUNDUP": decRounder(numfmt.Up),249- "ROUNDDOWN": decRounder(numfmt.Down),250- "TRUNC": decRounder(numfmt.Down),251-}252-253-func decRounder(mode numfmt.Rounding) func([]Node, lookup) (Value, bool) {254- return func(args []Node, get lookup) (Value, bool) {255- x, err := numArg(args[0], get)256- if err != nil {257- return *err, true258- }259- places, err := intArg(args, 1, 0, get)260- if err != nil {261- return *err, true262- }263- return decRound(x, clampInt(places, -308, 308), mode)264- }265-}266-267-// decFuncs returns the decimal twins of the functions in decEvals: the268-// same definitions (name, help, format inference) with a decimal eval269-// that falls back to the binary one. Built on first use, after every270-// function is defined.271-var decFuncs = sync.OnceValue(func() map[string]*FuncDef {272- out := map[string]*FuncDef{}273- for name, dec := range decEvals {274- bin := funcs[name]275- twin := *bin276- twin.eval = func(args []Node, get lookup) Value {277- if v, ok := dec(args, get); ok {278- return v279- }280- return bin.eval(args, get)281- }282- out[name] = &twin283- }284- return out285-})286-287-// Operators computed in decimal are marked by these types, which eval288-// knows; parsed formulas never contain them.289-type (290- decUnary formula.Unary291- decBinary formula.Binary292-)293-294-// decimalize returns a copy of n that evaluates in decimal: operators are295-// marked, and functions with a decimal twin call it.296-func decimalize(n Node) Node {297- switch n := n.(type) {298- case formula.Unary:299- n.X = decimalize(n.X)300- return decUnary(n)301- case formula.Binary:302- n.L, n.R = decimalize(n.L), decimalize(n.R)303- return decBinary(n)304- case formula.Call:305- args := make([]Node, len(n.Args))306- for i, a := range n.Args {307- args[i] = decimalize(a)308- }309- n.Args = args310- if twin, ok := decFuncs()[funcOf(n).Name]; ok {311- n.Fn = twin312- }313- return n314- }315- return n316-}5+// Decimal arithmetic is a per-workbook calculation setting; what it6+// computes in decimal is the function library's (functions/decimal.go).3177 3188 // arith returns the formula to evaluate under the workbook's arithmetic3199 // setting.32010 func (w *Workbook) arith(n Node) Node {32111 if w.decimal {322- return decimalize(n)12+ return functions.Decimalize(n)32313 }32414 return n32515 }
M internal/sheet/evaluate.go +5 -4
M internal/sheet/explain.go +7 -6
@@ -5,6 +5,7 @@ import (
55 "strings"66 77 "github.com/FelineStateMachine/012/internal/formula"8+ "github.com/FelineStateMachine/012/internal/functions"89 )910 1011 // ExplainError says why the cell at a shows an error, for the context
@@ -82,7 +83,7 @@ func (s *Sheet) errorOrigin(n Node, want Value) (Node, *loc) {
8283 type errorSearch struct {8384 s *Sheet8485 want Value85- get lookup86+ get *reader8687 }8788 8889 func (e errorSearch) same(v Value) bool { return v.Kind == Error && v.Str == e.want.Str }
@@ -99,11 +100,11 @@ func (e errorSearch) at(sheet string, a Addr) *loc {
99100 func (e errorSearch) find(n Node) (Node, *loc) {100101 switch n := n.(type) {101102 case formula.Ref:102- if e.same(e.get.cell(n.Sheet, n.Addr)) {103+ if e.same(e.get.Cell(n.Sheet, n.Addr)) {103104 return n, e.at(n.Sheet, n.Addr)104105 }105106 case formula.Range:106- if n.Rect.From == n.Rect.To && e.same(e.get.cell(n.Sheet, n.Rect.From)) {107+ if n.Rect.From == n.Rect.To && e.same(e.get.Cell(n.Sheet, n.Rect.From)) {107108 return n, e.at(n.Sheet, n.Rect.From)108109 }109110 case formula.Unary:
@@ -120,7 +121,7 @@ func (e errorSearch) find(n Node) (Node, *loc) {
120121 // at n when none has.121122 func (e errorSearch) operand(n Node, operands ...Node) (Node, *loc) {122123 for _, x := range operands {123- if e.same(eval(x, e.get)) {124+ if e.same(functions.Eval(x, e.get.lib)) {124125 return e.find(x)125126 }126127 }
@@ -130,7 +131,7 @@ func (e errorSearch) operand(n Node, operands ...Node) (Node, *loc) {
130131 // inCall follows the first argument of a call that has the error: for a131132 // range, its first cell with it.132133 func (e errorSearch) inCall(n formula.Call) (Node, *loc) {133- if funcOf(n).remote != nil {134+ if funcOf(n).Remote() {134135 return n, nil // JEV explains its own answers135136 }136137 for _, arg := range n.Args {
@@ -140,7 +141,7 @@ func (e errorSearch) inCall(n formula.Call) (Node, *loc) {
140141 }141142 continue142143 }143- if e.same(eval(arg, e.get)) {144+ if e.same(functions.Eval(arg, e.get.lib)) {144145 return e.find(arg)145146 }146147 }
M internal/sheet/formula.go +7 -9
M internal/sheet/functions_test.go +0 -8
R internal/sheet/functions_link.go → internal/sheet/link.go +2 -21
M internal/sheet/occupancy.go +30 -0
M internal/sheet/rangememo.go +17 -13
@@ -1,6 +1,10 @@
11 package sheet22 3-import "slices"3+import (4+ "slices"5+6+ "github.com/FelineStateMachine/012/internal/functions"7+)48 59 // Running aggregates: within one recalculation, the SUM-like functions610 // (SUM, AVERAGE, COUNT, COUNTA, MIN, MAX, PRODUCT) share what they read
@@ -25,7 +29,7 @@ type aggKey struct {
2529 // through each.2630 type runAgg struct {2731 rows []int28- cum []agg32+ cum []functions.Agg2933 next int // every row before next has been read3034 3135 errRow int
@@ -39,18 +43,18 @@ type aggMemo struct {
3943 m map[aggKey]*runAgg4044 }4145 42-// rangeAgg returns the aggregate of the range r on the named sheet, or46+// RangeAgg returns the aggregate of the range r on the named sheet, or4347 // false when it should be read directly: no recalculation is running, the4448 // range is short, its sheet doesn't exist, or a circular reference makes4549 // the order of evaluation matter.46-func (rd *reader) rangeAgg(sheet string, r Rect) (agg, *Value, bool) {50+func (rd *reader) RangeAgg(sheet string, r Rect) (functions.Agg, *Value, bool) {4751 memo := rd.memo4852 if memo == nil || denseReads || r.To.Row-r.From.Row+1 < memoRows || memo.w.Circular {49- return agg{}, nil, false53+ return functions.Agg{}, nil, false5054 }5155 t := rd.sheet(sheet)5256 if t == nil {53- return agg{}, nil, false57+ return functions.Agg{}, nil, false5458 }5559 key := aggKey{t, r.From.Col, r.To.Col, r.From.Row}5660 run := memo.m[key]
@@ -58,11 +62,11 @@ func (rd *reader) rangeAgg(sheet string, r Rect) (agg, *Value, bool) {
5862 // A range read once (a SUM of each named region) is read directly;5963 // only the second read of a run starts sharing it.6064 memo.m[key] = &runAgg{next: r.From.Row}61- return agg{}, nil, false65+ return functions.Agg{}, nil, false6266 }6367 if run.err == nil && r.To.Row >= run.next {6468 if run.busy {65- return agg{}, nil, false69+ return functions.Agg{}, nil, false6670 }6771 circular := memo.w.Circular6872 run.extend(rd, t, key, r.To.Row)
@@ -77,7 +81,7 @@ func (rd *reader) rangeAgg(sheet string, r Rect) (agg, *Value, bool) {
7781 func (run *runAgg) extend(rd *reader, t *Sheet, key aggKey, to int) {7882 run.busy = true7983 defer func() { run.busy = false }()80- cur, row := newAgg(), -184+ cur, row := functions.NewAgg(), -18185 if n := len(run.cum); n > 0 {8286 cur = run.cum[n-1]8387 }
@@ -88,7 +92,7 @@ func (run *runAgg) extend(rd *reader, t *Sheet, key aggKey, to int) {
8892 }8993 row, run.next = a.Row, a.Row9094 }91- if e := cur.add(rd.read(t, a), false); e != nil {95+ if e := cur.Add(rd.read(t, a), false); e != nil {9296 run.errRow, run.err = a.Row, e9397 return false9498 }
@@ -113,16 +117,16 @@ func (run *runAgg) extend(rd *reader, t *Sheet, key aggKey, to int) {
113117 }114118 115119 // through is the aggregate from the run's first row through row to.116-func (run *runAgg) through(to int) (agg, *Value, bool) {120+func (run *runAgg) through(to int) (functions.Agg, *Value, bool) {117121 if run.err != nil && to >= run.errRow {118- return agg{}, run.err, true122+ return functions.Agg{}, run.err, true119123 }120124 i, found := slices.BinarySearch(run.rows, to)121125 if found {122126 return run.cum[i], nil, true123127 }124128 if i == 0 {125- return newAgg(), nil, true129+ return functions.NewAgg(), nil, true126130 }127131 return run.cum[i-1], nil, true128132 }
M internal/sheet/recalc.go +99 -33
@@ -1,6 +1,9 @@
11 package sheet22 3-import "github.com/FelineStateMachine/012/internal/formula"3+import (4+ "github.com/FelineStateMachine/012/internal/formula"5+ "github.com/FelineStateMachine/012/internal/functions"6+)47 58 // Recalculation: a change marks the changed cells and everything that69 // transitively reads them dirty, on any sheet, then evaluates the dirty
@@ -165,10 +168,12 @@ func (w *Workbook) crossReads(u, l loc) bool {
165168 return false166169 }167170 168-// reader resolves the references of formulas on s with read: references169-// without a sheet ("") read s, others the sheet they name, with cells on170-// a sheet no sheet has the name of reading as #REF!. The last sheet name171-// is remembered, so a range on another sheet resolves its name once.171+// reader is the engine's side of how formulas on s read cells: the Book172+// the function library reads through (functions.Book). It resolves173+// references with read: references without a sheet ("") read s, others174+// the sheet they name, with cells on a sheet no sheet has the name of175+// reading as #REF!. The last sheet name is remembered, so a range on176+// another sheet resolves its name once.172177 type reader struct {173178 w *Workbook174179 s *Sheet
@@ -176,10 +181,27 @@ type reader struct {
176181 memo *aggMemo // running aggregates shared by a recalculation; nil otherwise177182 lastName string178183 last *Sheet184+ lib *functions.Reader // what the function library is given, reading through rd185+ dense bool // denseReads when lib was made186+}187+188+func (w *Workbook) lookupOn(s *Sheet, read func(*Sheet, Addr) Value) *reader {189+ rd := &reader{w: w, s: s, read: read, dense: denseReads}190+ rd.lib = functions.NewReader(rd, &w.depth, denseReads)191+ return rd179192 }180193 181-func (w *Workbook) lookupOn(s *Sheet, read func(*Sheet, Addr) Value) lookup {182- return &reader{w: w, s: s, read: read}194+// recalcReader is the reader for formulas on s in a recalculation, which195+// reads through e and shares running aggregates in memo: the one kept196+// from the last recalculation, when there is one.197+func (w *Workbook) recalcReader(s *Sheet, e *evaluator, memo *aggMemo) *reader {198+ rd := s.recalcs199+ if rd == nil || rd.w != w || rd.dense != denseReads {200+ rd = w.lookupOn(s, nil)201+ s.recalcs = rd202+ }203+ rd.read, rd.memo, rd.lastName, rd.last = e.compute, memo, "", nil204+ return rd183205 }184206 185207 // sheet is the sheet a reference written with the name sheet points at.
@@ -193,8 +215,8 @@ func (rd *reader) sheet(sheet string) *Sheet {
193215 return rd.last194216 }195217 196-// cell returns the current value of a cell.197-func (rd *reader) cell(sheet string, a Addr) Value {218+// Cell returns the current value of a cell.219+func (rd *reader) Cell(sheet string, a Addr) Value {198220 t := rd.sheet(sheet)199221 if t == nil {200222 return ErrRef
@@ -202,35 +224,79 @@ func (rd *reader) cell(sheet string, a Addr) Value {
202224 return rd.read(t, a)203225 }204226 205-// cells calls fn with the address and value of every cell of r that206-// holds something, row by row, until fn returns false: blank cells are207-// skipped without being visited, so a whole column costs what it holds.208-// A range on a sheet that doesn't exist reads as one #REF!.209-func (rd *reader) cells(sheet string, r Rect, fn func(Addr, Value) bool) {227+// Scan reads the stored cells of r from the cell from on, row by row, as228+// functions.Book describes: blank cells are skipped without being229+// visited, so a whole column costs what it holds. It finds the cells230+// first, then reads their values, so the chunk is filled without a call231+// per cell.232+func (rd *reader) Scan(sheet string, r Rect, from Addr, addrs []Addr, vals []Value) int {210233 t := rd.sheet(sheet)234+ if t == nil {235+ return -1236+ }237+ n := fill(t, r, from, addrs)238+ if vals == nil {239+ return n240+ }241+ for i, a := range addrs[:n] {242+ v := rd.read(t, a)243+ vals[i] = v244+ if v.Kind == Error {245+ return i + 1246+ }247+ }248+ return n249+}250+251+// fill writes the stored cells of r on t from the cell from on into dst,252+// row by row, until it is full, and returns how many.253+func fill(t *Sheet, r Rect, from Addr, dst []Addr) int {211254 switch {212- case t == nil:213- fn(r.From, ErrRef)214255 case denseReads:215- for row := r.From.Row; row <= r.To.Row; row++ {216- for col := r.From.Col; col <= r.To.Col; col++ {217- if a := (Addr{Col: col, Row: row}); !fn(a, rd.read(t, a)) {218- return219- }220- }221- }256+ return fillDense(r, from, dst)222257 case r.From.Col == r.To.Col:223- t.cells.colScan(r.From.Col, r.From.Row, r.To.Row, func(row int) bool {224- a := Addr{Col: r.From.Col, Row: row}225- return fn(a, rd.read(t, a))226- })227- default:228- for a := range t.cells.inRange(r) {229- if !fn(a, rd.read(t, a)) {230- return231- }258+ return t.cells.colFill(r.From.Col, from.Row, r.To.Row, dst)259+ }260+ return t.cells.rangeFill(r, from, dst)261+}262+263+// fillDense writes every address of r from the cell from on into dst,264+// row by row, until it is full, as tests ask with denseReads.265+func fillDense(r Rect, from Addr, dst []Addr) int {266+ n := 0267+ for a := from; a.Row <= r.To.Row && n < len(dst); n++ {268+ dst[n] = a269+ if a.Col++; a.Col > r.To.Col {270+ a = Addr{Col: r.From.Col, Row: a.Row + 1}232271 }233272 }273+ return n274+}275+276+// Bounds is the smallest range holding every stored cell of r.277+func (rd *reader) Bounds(sheet string, r Rect) (b Rect, any, exists bool) {278+ t := rd.sheet(sheet)279+ if t == nil {280+ return Rect{}, false, false281+ }282+ b, any = t.cells.bounds(r)283+ return b, any, true284+}285+286+// Ask looks up the answer to a JEV function's question in the workbook's287+// RemoteSource, noting that the formula being evaluated waits for it288+// when it isn't known yet.289+func (rd *reader) Ask(call RemoteCall) (RemoteAnswer, Value) {290+ w := rd.w291+ if w.remote == nil {292+ return RemoteAnswer{}, ErrNoRemote293+ }294+ ans, ok := w.remote.Lookup(call)295+ if !ok {296+ w.wait(call)297+ return RemoteAnswer{}, Pending298+ }299+ return ans, Value{}234300 }235301 236302 // denseReads makes formulas read every address of their ranges, as they
@@ -239,7 +305,7 @@ func (rd *reader) cells(sheet string, r Rect, fn func(Addr, Value) bool) {
239305 var denseReads bool240306 241307 // values reads current values for formulas on s, across sheets.242-func (w *Workbook) values(s *Sheet) lookup {308+func (w *Workbook) values(s *Sheet) *reader {243309 return w.lookupOn(s, func(t *Sheet, a Addr) Value { return t.Value(a) })244310 }245311
M internal/sheet/remote.go +19 -33
@@ -1,6 +1,9 @@
11 package sheet22 3-import "github.com/FelineStateMachine/012/internal/formula"3+import (4+ "github.com/FelineStateMachine/012/internal/formula"5+ "github.com/FelineStateMachine/012/internal/functions"6+)47 58 // Some functions (JEV.*) are answered by a hosted model. The engine never69 // talks to the network: it describes each question as a RemoteCall and
@@ -9,32 +12,13 @@ import "github.com/FelineStateMachine/012/internal/formula"
912 // calls RecalcAnswered, which recomputes the formulas waiting for them.1013 // The functions are volatile, so any other change looks again too.1114 12-// RemoteCall is one question for the model, built from a function's13-// arguments. It isn't comparable: Key identifies it by its content.14-type RemoteCall struct {15- Kind string `json:"kind"` // "noul", "choice" or "score"16- State any `json:"state"`17- Instructions string `json:"instructions"`18- // Criteria depends on Kind: noul is [2]string{yes, no} (either may be19- // empty), choice is map[label]description, score is []string levels.20- Criteria any `json:"criteria"`21-}22-23-// RemoteAnswer is the model's answer. Which fields are set depends on the24-// call's Kind; Failed explains an answer that couldn't be had.25-type RemoteAnswer struct {26- Noul float64 // probability of yes27- Choice string28- Score float6429- Confidence float64 // in Choice or Score, not a probability of truth30- Failed string31-}32-33-// RemoteSource answers RemoteCalls. Lookup returns false while an answer34-// isn't known, and queues the call.35-type RemoteSource interface {36- Lookup(RemoteCall) (RemoteAnswer, bool)37-}15+// The questions and answers are the function library's16+// (functions/remote.go); the engine's API names them too.17+type (18+ RemoteCall = functions.RemoteCall19+ RemoteAnswer = functions.RemoteAnswer20+ RemoteSource = functions.RemoteSource21+)3822 3923 // SetRemote sets what answers the workbook's JEV functions: nil when no4024 // API key is configured, and the functions then evaluate to ErrNoRemote.
@@ -49,13 +33,15 @@ func (s *Sheet) SetRemote(r RemoteSource) { s.wb.SetRemote(r) }
4933 5034 var (5135 // Pending is shown while an answer is on its way.52- Pending = Value{Kind: Error, Str: "Loading…"}36+ Pending = functions.Pending5337 // ErrNoRemote means JEV functions can't run: there is no API key.54- ErrNoRemote = Value{Kind: Error, Str: "#N/A"}38+ ErrNoRemote = functions.ErrNoRemote39+ // ErrRemote is a question the model couldn't answer.40+ ErrRemote = functions.ErrRemote5541 )5642 5743 // IsPending reports whether v is waiting for a remote answer.58-func IsPending(v Value) bool { return v == Pending }44+func IsPending(v Value) bool { return functions.IsPending(v) }5945 6046 // RecalcVolatile recomputes volatile formulas and their dependents on6147 // every sheet, e.g. after remote answers arrive. It doesn't touch the
@@ -69,7 +55,7 @@ func (s *Sheet) RemoteCalls(a Addr) []RemoteCall {
6955 if c == nil || c.expr == nil {7056 return nil7157 }72- get := s.wb.values(s)58+ get := s.wb.values(s).lib7359 var calls []RemoteCall7460 var walk func(Node)7561 walk = func(n Node) {
@@ -80,8 +66,8 @@ func (s *Sheet) RemoteCalls(a Addr) []RemoteCall {
8066 walk(n.L)8167 walk(n.R)8268 case formula.Call:83- if funcOf(n).remote != nil {84- if call, err := funcOf(n).remote(n.Args, get); err == nil {69+ if f := funcOf(n); f.Remote() {70+ if call, err := f.Question(n.Args, get); err == nil {8571 calls = append(calls, call)8672 }8773 }
M internal/sheet/sheet.go +6 -2
M internal/sheet/store.go +28 -0
@@ -78,6 +78,12 @@ func (st *cellStore) colScan(c, r0, r1 int, fn func(row int) bool) bool {
7878 return st.stored.colScan(c, r0, r1, fn)7979 }8080 81+// colFill writes the rows from r0 to r1 of column c that hold a cell82+// into dst, until it is full; see occupancy.colFill.83+func (st *cellStore) colFill(c, r0, r1 int, dst []Addr) int {84+ return st.stored.colFill(c, r0, r1, dst)85+}86+8187 // inRange yields the stored cells in r in row-major order, visiting only8288 // the rows of each column that hold cells.8389 func (st *cellStore) inRange(r Rect) iter.Seq2[Addr, *Cell] {
@@ -101,6 +107,28 @@ func (st *cellStore) inRange(r Rect) iter.Seq2[Addr, *Cell] {
101107 }102108 }103109 110+// rangeFill writes the stored cells of r from the cell from on, in111+// row-major order, into dst until it is full, and returns how many.112+func (st *cellStore) rangeFill(r Rect, from Addr, dst []Addr) int {113+ n := 0114+ if from.Col > r.From.Col { // the rest of a row first115+ for a := range st.inRange(Rect{From: from, To: Addr{Col: r.To.Col, Row: from.Row}}) {116+ dst[n] = a117+ if n++; n == len(dst) {118+ return n119+ }120+ }121+ from = Addr{Col: r.From.Col, Row: from.Row + 1}122+ }123+ for a := range st.inRange(Rect{From: from, To: r.To}) {124+ dst[n] = a125+ if n++; n == len(dst) {126+ break127+ }128+ }129+ return n130+}131+104132 // anyInRange yields the stored cells in r in no particular order,105133 // visiting whichever is cheaper: the index, row by row, or every stored106134 // cell (iterating the map costs a quarter of a lookup per cell) when r
M internal/sheet/values.go +15 -4
@@ -1,10 +1,14 @@
11 package sheet22 3-import "github.com/FelineStateMachine/012/internal/value"3+import (4+ "github.com/FelineStateMachine/012/internal/functions"5+ "github.com/FelineStateMachine/012/internal/value"6+)47 58 // Values, formats and the parsing of typed entries live in6-// internal/value, below the engine. The engine's API keeps their names,7-// as it does the formula package's, so callers see one package.9+// internal/value, below the function library and the engine; functions10+// in internal/functions. The engine's API keeps their names, as it does11+// the formula package's, so callers see one package.812 913 type (1014 // Kind is the type of a computed cell value.
@@ -15,6 +19,8 @@ type (
1519 FormatKind = value.FormatKind1620 // Format is a cell's number format.1721 Format = value.Format22+ // FuncDef describes a spreadsheet function.23+ FuncDef = functions.FuncDef1824 )1925 2026 // Kinds of values.
@@ -71,9 +77,14 @@ func ParseNumber(s string) (float64, bool) { return value.ParseNumber(s) }
7177 // Sheets applies.7278 func ParseValue(s string) (float64, Format, bool) { return value.ParseValue(s) }7379 80+// LookupFunc finds a function by name or alias; callers pass upper case.81+func LookupFunc(name string) (*FuncDef, bool) { return functions.LookupFunc(name) }82+83+// Funcs returns every function, sorted by name.84+func Funcs() []*FuncDef { return functions.Funcs() }85+7486 func num(v float64) Value { return value.Num(v) }7587 func boolean(b bool) Value { return value.Boolean(b) }76-func errOf(v Value) *Value { return value.ErrOf(v) }7788 func toNum(v Value) (float64, *Value) { return value.ToNum(v) }7889 func text(v Value) string { return value.AsText(v) }7990 func compare(l, r Value) int { return value.Compare(l, r) }