AVERAGEIF, AVERAGEIFS, COUNTIF, COUNTIFS, COUNTBLANK, MEDIAN, MODE, STDEV, STDEVP, VAR, VARP, LARGE, SMALL and RANK.
- commit
- 5bb28c9c7cddb2237b3a42aa3ee96275058a1968
- parent
- d5598f06ccd24dae15c784a00f6b92006cf43c20
- author
- Dami <dami.etoile@gmail.com>
| A | internal/ | 228 | ++++++++++++++++++++++++++++++ |
| A | internal/ | 46 | ++++++ |
@@ -0,0 +1,228 @@
1+package sheet2+3+import (4+ "math"5+ "slices"6+)7+8+func init() {9+ define(10+ &FuncDef{Name: "AVERAGEIF", Args: "criteria_range, criterion, [average_range]", Desc: "Average of the cells that meet a condition", Min: 2, Max: 3,11+ eval: func(args []Node, get lookup) Value {12+ rng := matrixArg(args[0], get)13+ cv := eval(args[1], get)14+ if cv.Kind == Error {15+ return cv16+ }17+ c := newCriterion(cv)18+ avg := rng19+ if len(args) > 2 {20+ avg = matrixArg(args[2], get).resized(rng.rows, rng.cols, get)21+ }22+ mask := make([]bool, rng.size())23+ for i := range mask {24+ mask[i] = c.test(rng.at(i))25+ }26+ return averageMasked(avg, mask)27+ }, format: inheritFrom(2)},28+ &FuncDef{Name: "AVERAGEIFS", Args: "average_range, criteria_range1, criterion1, [criteria_range2, criterion2, ...]", Desc: "Average of the cells that meet every condition", Min: 3, Max: -1, step: 2,29+ eval: func(args []Node, get lookup) Value {30+ avg := matrixArg(args[0], get)31+ rows, cols, mask, err := criteriaMask(args, 1, get)32+ switch {33+ case err != nil:34+ return *err35+ case rows != avg.rows || cols != avg.cols:36+ return ErrValue37+ }38+ return averageMasked(avg, mask)39+ }, format: inheritFrom(0)},40+ &FuncDef{Name: "COUNTIF", Args: "range, criterion", Desc: "Count of the cells that meet a condition", Min: 2, Max: 2,41+ eval: countIfs},42+ &FuncDef{Name: "COUNTIFS", Args: "criteria_range1, criterion1, [criteria_range2, criterion2, ...]", Desc: "Count of the cells that meet every condition", Min: 2, Max: -1, step: 2,43+ eval: countIfs},44+ &FuncDef{Name: "COUNTBLANK", Args: "range", Desc: "Count of empty cells, including empty text", Min: 1, Max: 1,45+ eval: func(args []Node, get lookup) Value {46+ m := matrixArg(args[0], get)47+ n := 048+ for i := range m.size() {49+ if v := m.at(i); v.Kind == Empty || v.Kind == Text && v.Str == "" {50+ n++51+ }52+ }53+ return num(float64(n))54+ }},55+ &FuncDef{Name: "MEDIAN", Args: "value1, [value2, ...]", Desc: "Middle value of numbers", Min: 1, Max: -1,56+ eval: withNums(func(x []float64) Value {57+ if len(x) == 0 {58+ return ErrNum59+ }60+ slices.Sort(x)61+ n := len(x)62+ if n%2 == 1 {63+ return num(x[n/2])64+ }65+ return num((x[n/2-1] + x[n/2]) / 2)66+ }), format: inherit},67+ &FuncDef{Name: "MODE", Args: "value1, [value2, ...]", Desc: "Most common number (the first, on a tie)", Min: 1, Max: -1,68+ eval: withNums(func(x []float64) Value {69+ best, bestN := 0.0, 170+ for i, v := range x {71+ n := 072+ for _, w := range x[i:] {73+ if w == v {74+ n++75+ }76+ }77+ if n > bestN {78+ best, bestN = v, n79+ }80+ }81+ if bestN < 2 {82+ return ErrNA83+ }84+ return num(best)85+ }), format: inherit},86+ &FuncDef{Name: "STDEV", Args: "value1, [value2, ...]", Desc: "Standard deviation of a sample", Min: 1, Max: -1,87+ eval: withNums(func(x []float64) Value { return spread(x, 1, true) })},88+ &FuncDef{Name: "STDEVP", Args: "value1, [value2, ...]", Desc: "Standard deviation of a whole population", Min: 1, Max: -1,89+ eval: withNums(func(x []float64) Value { return spread(x, 0, true) })},90+ &FuncDef{Name: "VAR", Args: "value1, [value2, ...]", Desc: "Variance of a sample", Min: 1, Max: -1,91+ eval: withNums(func(x []float64) Value { return spread(x, 1, false) })},92+ &FuncDef{Name: "VARP", Args: "value1, [value2, ...]", Desc: "Variance of a whole population", Min: 1, Max: -1,93+ eval: withNums(func(x []float64) Value { return spread(x, 0, false) })},94+ &FuncDef{Name: "LARGE", Args: "data, n", Desc: "The nth largest number", Min: 2, Max: 2,95+ eval: nth(func(a, b float64) int { return cmpFloat(b, a) }), format: inheritFrom(0)},96+ &FuncDef{Name: "SMALL", Args: "data, n", Desc: "The nth smallest number", Min: 2, Max: 2,97+ eval: nth(cmpFloat), format: inheritFrom(0)},98+ &FuncDef{Name: "RANK", Args: "value, data, [is_ascending]", Desc: "Rank of a number among others, largest first by default", Min: 2, Max: 3,99+ eval: func(args []Node, get lookup) Value {100+ v, err := numArg(args[0], get)101+ if err != nil {102+ return *err103+ }104+ data, err := nums(args[1:2], get)105+ if err != nil {106+ return *err107+ }108+ asc, err := boolArg(args, 2, false, get)109+ if err != nil {110+ return *err111+ }112+ rank, found := 1, false113+ for _, d := range data {114+ switch {115+ case d == v:116+ found = true117+ case asc && d < v, !asc && d > v:118+ rank++119+ }120+ }121+ if !found {122+ return ErrNA123+ }124+ return num(float64(rank))125+ }},126+ )127+}128+129+func countIfs(args []Node, get lookup) Value {130+ _, _, mask, err := criteriaMask(args, 0, get)131+ if err != nil {132+ return *err133+ }134+ n := 0135+ for _, ok := range mask {136+ if ok {137+ n++138+ }139+ }140+ return num(float64(n))141+}142+143+// averageMasked averages the numbers of m where mask is set.144+func averageMasked(m matrix, mask []bool) Value {145+ sum, n := 0.0, 0146+ for i, ok := range mask {147+ if !ok {148+ continue149+ }150+ v := m.at(i)151+ switch v.Kind {152+ case Error:153+ return v154+ case Number:155+ sum += v.Num156+ n++157+ }158+ }159+ if n == 0 {160+ return ErrDiv0161+ }162+ return num(sum / float64(n))163+}164+165+// withNums passes the numbers of all arguments, with aggregate166+// semantics, to f.167+func withNums(f func([]float64) Value) func([]Node, lookup) Value {168+ return func(args []Node, get lookup) Value {169+ x, err := nums(args, get)170+ if err != nil {171+ return *err172+ }173+ return f(x)174+ }175+}176+177+// spread computes variance (or its root) with ddof 1 for samples and 0178+// for populations.179+func spread(x []float64, ddof int, root bool) Value {180+ n := len(x)181+ if n-ddof <= 0 {182+ return ErrDiv0183+ }184+ mean := 0.0185+ for _, v := range x {186+ mean += v187+ }188+ mean /= float64(n)189+ ss := 0.0190+ for _, v := range x {191+ ss += (v - mean) * (v - mean)192+ }193+ v := ss / float64(n-ddof)194+ if root {195+ v = math.Sqrt(v)196+ }197+ return num(v)198+}199+200+// nth builds LARGE and SMALL: the nth number of data in cmp order.201+func nth(cmp func(a, b float64) int) func([]Node, lookup) Value {202+ return func(args []Node, get lookup) Value {203+ x, err := nums(args[:1], get)204+ if err != nil {205+ return *err206+ }207+ n, err := numArg(args[1], get)208+ if err != nil {209+ return *err210+ }211+ k := int(math.Ceil(n))212+ if k < 1 || k > len(x) {213+ return ErrNum214+ }215+ slices.SortFunc(x, cmp)216+ return num(x[k-1])217+ }218+}219+220+func cmpFloat(a, b float64) int {221+ switch {222+ case a < b:223+ return -1224+ case a > b:225+ return 1226+ }227+ return 0228+}@@ -0,0 +1,46 @@
1+package sheet2+3+import "testing"4+5+func TestStatsFunctions(t *testing.T) {6+ checkFormulas(t, fixture(t), []fnCase{7+ {`=AVERAGEIF(B1:B5, "north", C1:C5)`, num(3)},8+ {`=AVERAGEIF(A1:A7, ">0")`, num(20)},9+ {`=AVERAGEIF(A1:A7, ">100")`, ErrDiv0},10+ {`=AVERAGEIFS(C1:C5, B1:B5, "north", C1:C5, ">1")`, num(4)},11+ {`=COUNTIF(B1:B7, "north")`, num(3)},12+ {`=COUNTIF(B1:B7, "NORTH")`, num(3)}, // case-insensitive13+ {`=COUNTIF(B1:B7, "?o*")`, num(4)},14+ {`=COUNTIF(B1:B7, "Ab~*d")`, num(1)},15+ {`=COUNTIF(A1:A7, "<>10")`, num(6)}, // blanks and text count as not equal16+ {`=COUNTIF(A1:A7, "")`, num(1)},17+ {`=COUNTIF(B1:B7, "")`, num(1)}, // "" from a formula counts as blank18+ {`=COUNTIF(A1:A7, "<>")`, num(6)},19+ {`=COUNTIF(A1:A7, TRUE)`, num(1)},20+ {`=COUNTIF(A1:A7, ">=10")`, num(3)},21+ {`=COUNTIF(F1:F5, ">1/1/2026")`, num(2)}, // date criteria compare as dates22+ {`=COUNTIF(B1:B7, ">m")`, num(4)},23+ {`=COUNTIFS(B1:B5, "north", C1:C5, "<5")`, num(2)},24+ {"=COUNTBLANK(A1:A7)", num(1)},25+ {"=COUNTBLANK(B1:B7)", num(1)},26+ {"=MEDIAN(A1:A7)", num(15)},27+ {"=MEDIAN(C1:C5)", num(3)},28+ {"=MEDIAN(A4)", ErrNum},29+ {`=MEDIAN("a")`, ErrValue},30+ {"=MODE(G1:G4)", num(2)},31+ {"=MODE(C1:C5)", ErrNA},32+ {"=STDEV(A1:A7)", num(14.930394055974098)},33+ {"=STDEVP(A1:A7)", num(12.93010054098575)},34+ {"=VAR(A1:A7)", num(222.91666666666666)},35+ {"=VARP(A1:A7)", num(167.1875)},36+ {"=STDEV(5)", ErrDiv0},37+ {"=LARGE(A1:A7, 1)", num(30)},38+ {"=LARGE(A1:A7, 4)", num(-5)},39+ {"=LARGE(A1:A7, 5)", ErrNum},40+ {"=SMALL(C1:C7, 2)", num(2)},41+ {"=RANK(20, A1:A7)", num(2)},42+ {"=RANK(20, A1:A7, 1)", num(3)},43+ {"=RANK(2, G1:G4)", num(2)},44+ {"=RANK(99, A1:A7)", ErrNA},45+ })46+}