cat / 012
clone

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

Compute PRODUCT, SUMIF, SUMIFS and SUMPRODUCT in decimal arithmetic

Dami committed cb58592
commit
cb58592dcc47269d9ee3e5416fa8b3aa695a8dd1
parent
bdc52596b78b33bf8de50f034204c3732688d78f
author
Dami <dami.etoile@gmail.com>
4 files changed, +186 -50
Mdocs/formulas.md2+-
Minternal/sheet/decimal.go132+++++++++++++++++++++++++-----
Minternal/sheet/decimal_test.go17++++
Minternal/sheet/functions_math.go85++++++++++++++------
M docs/formulas.md +1 -1
M internal/sheet/decimal.go +108 -24
@@ -18,7 +18,7 @@ import (
1818 // compute in decimal:1919 //2020 // - the operators + - * / and the postfix %21-// - SUM and AVERAGE21+// - SUM, AVERAGE, PRODUCT, SUMIF, SUMIFS and SUMPRODUCT2222 // - ROUND, ROUNDUP, ROUNDDOWN and TRUNC2323 //2424 // Each step reads its float64 operands as the shortest decimal that
@@ -102,10 +102,54 @@ func decRound(x float64, places int, mode numfmt.Rounding) (Value, bool) {
102102 return fromDec(&z)103103 }104104 105-// decSum adds the numbers SUM would, in decimal, and counts them.106-func decSum(args []Node, get lookup) (sum apd.Decimal, n int, e *Value, ok bool) {107- ok = true108- var x apd.Decimal105+// 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) {109153 e = each(args, get, func(v Value, direct bool) *Value {110154 switch v.Kind {111155 case Error:
@@ -121,49 +165,89 @@ func decSum(args []Node, get lookup) (sum apd.Decimal, n int, e *Value, ok bool)
121165 return err122166 }123167 n++124- if _, err := x.SetFloat64(f); err != nil {125- ok = false126- } else if _, err := decCtx.Add(&sum, &sum, &x); err != nil {127- ok = false128- }168+ step(f)129169 return nil130170 })131- return sum, n, e, ok171+ 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()132204 }133205 134206 // decEvals are the decimal versions of functions, by name. Each reports135207 // false to fall back to the binary version.136208 var decEvals = map[string]func(args []Node, get lookup) (Value, bool){137209 "SUM": func(args []Node, get lookup) (Value, bool) {138- sum, _, e, ok := decSum(args, get)139- if e != nil {210+ acc := newDecAcc(0)211+ if _, e := decFold(args, get, acc.add); e != nil {140212 return *e, true141213 }142- if !ok {143- return Value{}, false144- }145- return fromDec(&sum)214+ return acc.result()146215 },147216 "AVERAGE": func(args []Node, get lookup) (Value, bool) {148- sum, n, e, ok := decSum(args, get)217+ acc := newDecAcc(0)218+ n, e := decFold(args, get, acc.add)149219 switch {150220 case e != nil:151221 return *e, true152222 case n == 0:153223 return ErrDiv0, true154- case !ok:224+ case !acc.ok:155225 return Value{}, false156226 }157227 var q apd.Decimal158- if _, err := decCtx.Quo(&q, &sum, apd.New(int64(n), 0)); err != nil {228+ if _, err := decCtx.Quo(&q, &acc.d, apd.New(int64(n), 0)); err != nil {159229 return Value{}, false160230 }161231 return fromDec(&q)162232 },163- "ROUND": decRounder(numfmt.HalfUp),164- "ROUNDUP": decRounder(numfmt.Up),165- "ROUNDDOWN": decRounder(numfmt.Down),166- "TRUNC": decRounder(numfmt.Down),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),167251 }168252 169253 func decRounder(mode numfmt.Rounding) func([]Node, lookup) (Value, bool) {
M internal/sheet/decimal_test.go +17 -0
@@ -69,6 +69,23 @@ func TestDecimalArithmetic(t *testing.T) {
6969 {"average of nothing", "=AVERAGE(A1:A2)", nil, "#DIV/0!", "#DIV/0!"},7070 {"sum ignores text in ranges", "=SUM(A1:A3)", []string{"0.1", "x", "0.2"}, "0.30000000000000004", "0.3"},7171 {"huge sum falls back", "=SUM(1E308, 1E308)", nil, "#NUM!", "#NUM!"},72+ // PRODUCT, SUMIF, SUMIFS and SUMPRODUCT have decimal twins too.73+ {"product", "=PRODUCT(A1:A2)", []string{"1.1", "1.1"}, "1.2100000000000002", "1.21"},74+ {"product of direct", "=PRODUCT(0.07, 3)", nil, "0.21000000000000002", "0.21"},75+ {"product skips text in ranges", "=PRODUCT(A1:A3)", []string{"1.1", "x", "1.1"}, "1.2100000000000002", "1.21"},76+ {"product of nothing", "=PRODUCT(A1:A2)", nil, "0", "0"},77+ {"product of text", `=PRODUCT("a")`, nil, "#VALUE!", "#VALUE!"},78+ {"huge product falls back", "=PRODUCT(1E308, 10)", nil, "#NUM!", "#NUM!"},79+ {"sumif", `=SUMIF(A1:A10, ">0")`, tenCents, "0.9999999999999999", "1"},80+ {"sumif equals", `=SUMIF(A1:A10, ">0")=1`, tenCents, "FALSE", "TRUE"},81+ {"sumif with sum range", `=SUMIF(A1:A3, "<>x", A1:A3)`, []string{"0.1", "x", "0.2"}, "0.30000000000000004", "0.3"},82+ {"sumif error", `=SUMIF(A1:A2, ">0")`, []string{"1", "=1/0"}, "1", "1"},83+ {"sumifs", `=SUMIFS(A1:A10, A1:A10, "0.1")`, tenCents, "0.9999999999999999", "1"},84+ {"sumifs mismatched", `=SUMIFS(A1:A10, A1:A9, ">0")`, tenCents, "#VALUE!", "#VALUE!"},85+ {"sumproduct", "=SUMPRODUCT(A1:A3, A1:A3)", []string{"0.1", "0.2", "0.3"}, "0.14", "0.14"},86+ {"sumproduct of cents", "=SUMPRODUCT(A1:A2, A3:A4)", []string{"0.1", "0.2", "3", "3"}, "0.9000000000000001", "0.9"},87+ {"sumproduct mismatched", "=SUMPRODUCT(A1:A2, A1:A3)", []string{"1", "2", "3"}, "#VALUE!", "#VALUE!"},88+ {"sumproduct error", "=SUMPRODUCT(A1:A2)", []string{"1", "=1/0"}, "#DIV/0!", "#DIV/0!"},7289 }7390 show := func(v Value) string {7491 if v.Kind == Number {
M internal/sheet/functions_math.go +60 -25
@@ -134,21 +134,55 @@ func toMultiple(round func(float64) float64) func([]Node, lookup) Value {
134134 }135135 }136136 137+// sumIf, sumIfs and sumProduct add in binary; their decimal twins138+// (decimal.go) visit the same terms and add them in decimal.137139 func sumIf(args []Node, get lookup) Value {140+ total := 0.0141+ if e := sumIfTerms(args, get, func(f float64) { total += f }); e != nil {142+ return *e143+ }144+ return num(total)145+}146+147+func sumIfs(args []Node, get lookup) Value {148+ total := 0.0149+ if e := sumIfsTerms(args, get, func(f float64) { total += f }); e != nil {150+ return *e151+ }152+ return num(total)153+}154+155+func sumProduct(args []Node, get lookup) Value {156+ total := 0.0157+ e := sumProductTerms(args, get, func(fs []float64) {158+ p := 1.0159+ for _, f := range fs {160+ p *= f161+ }162+ total += p163+ })164+ if e != nil {165+ return *e166+ }167+ return num(total)168+}169+170+// sumIfTerms calls add with each number SUMIF sums, or returns the error171+// it gives.172+func sumIfTerms(args []Node, get lookup, add func(float64)) *Value {138173 rng := matrixArg(args[0], get)139174 cv := eval(args[1], get)140175 if cv.Kind == Error {141- return cv176+ return &cv142177 }143178 c := newCriterion(cv)144179 sum := rng145180 if len(args) > 2 {146181 sum = matrixArg(args[2], get).resized(rng.rows, rng.cols, get)147182 if sum.rows != rng.rows || sum.cols != rng.cols {148- return ErrValue183+ return &ErrValue149184 }150185 }151- total := 0.0152186 for i := range rng.size() {153187 if !c.test(rng.at(i)) {154188 continue
@@ -156,24 +190,25 @@ func sumIf(args []Node, get lookup) Value {
156190 v := sum.at(i)157191 switch v.Kind {158192 case Error:159- return v193+ return &v160194 case Number:161- total += v.Num195+ add(v.Num)162196 }163197 }164- return num(total)198+ return nil165199 }166200 167-func sumIfs(args []Node, get lookup) Value {201+// sumIfsTerms calls add with each number SUMIFS sums, or returns the202+// error it gives.203+func sumIfsTerms(args []Node, get lookup, add func(float64)) *Value {168204 sum := matrixArg(args[0], get)169205 rows, cols, mask, err := criteriaMask(args, 1, get)170206 switch {171207 case err != nil:172- return *err208+ return err173209 case rows != sum.rows || cols != sum.cols:174- return ErrValue210+ return &ErrValue175211 }176- total := 0.0177212 for i, ok := range mask {178213 if !ok {179214 continue
@@ -181,39 +216,39 @@ func sumIfs(args []Node, get lookup) Value {
181216 v := sum.at(i)182217 switch v.Kind {183218 case Error:184- return v219+ return &v185220 case Number:186- total += v.Num221+ add(v.Num)187222 }188223 }189- return num(total)224+ return nil190225 }191226 192-// sumProduct multiplies same-sized arrays entry by entry and sums; text193-// and blanks count as 0.194-func sumProduct(args []Node, get lookup) Value {227+// sumProductTerms calls term with the factors of each entry of228+// same-sized arrays (text and blanks count as 0), or returns the error229+// SUMPRODUCT gives. The slice is reused between calls.230+func sumProductTerms(args []Node, get lookup, term func(fs []float64)) *Value {195231 ms := make([]matrix, len(args))196232 for i, a := range args {197233 ms[i] = matrixArg(a, get)198234 if ms[i].rows != ms[0].rows || ms[i].cols != ms[0].cols {199- return ErrValue235+ return &ErrValue200236 }201237 }202- total := 0.0238+ fs := make([]float64, len(ms))203239 for k := range ms[0].size() {204- p := 1.0205- for _, m := range ms {240+ for i, m := range ms {206241 v := m.at(k)207242 switch v.Kind {208243 case Error:209- return v244+ return &v210245 case Number:211- p *= v.Num246+ fs[i] = v.Num212247 default:213- p = 0248+ fs[i] = 0214249 }215250 }216- total += p251+ term(fs)217252 }218- return num(total)253+ return nil219254 }