MEDIAN, MODE, LARGE, SMALL and the new percentiles read their numbers once in ascending order through one interface (Sorted), held and sorted on a sheet, so a Book can hand them out sorted some other way. RANK counts above and below as it reads rather than holding the range. Excel's _xlfn. prefix is written for the dotted names.
- commit
- 7920678c606cb21f8a298c68c3b5886776e35af6
- parent
- d59b5ded81a8238b7c4169656d5857066844b4ee
- author
- Dami <dami.etoile@gmail.com>
- kind
- feat risk score 2.0 of 3, Jev's estimate
| M | docs/ | 10 | +- |
| M | internal/ | 2 | + |
| A | internal/ | 236 | ++++++++++++++++++++++++++++++ |
| M | internal/ | 114 | ++++++--------- |
| M | internal/ | 19 | ++ |
| M | oracle/ | 5 | + |
@@ -7,7 +7,7 @@ sidebar_position: 3
77 88 <!-- Generated from the engine's function table by TestFunctionsDoc; do not edit. -->99 10-012 has 142 functions. They follow Google Sheets' names, arguments and10+012 has 150 functions. They follow Google Sheets' names, arguments and1111 semantics; `[brackets]` mark optional arguments. Function names are1212 case-insensitive, and 1-2-3's `@SUM(A1..A5)` spelling still works.1313 @@ -73,7 +73,15 @@ Aliases: `AVG` for `AVERAGE`.
7373 | `LARGE(data, n)` | The nth largest number |7474 | `MEDIAN(value1, [value2, ...])` | Middle value of numbers |7575 | `MODE(value1, [value2, ...])` | Most common number (the first, on a tie) |76+| `MODE.SNGL(value1, [value2, ...])` | Most common number, the first on a tie (MODE) |77+| `PERCENTILE(data, percentile)` | The value at a percentile from 0 to 1, interpolated |78+| `PERCENTILE.EXC(data, percentile)` | The value at a percentile strictly between 0 and 1, interpolated |79+| `PERCENTILE.INC(data, percentile)` | The value at a percentile from 0 to 1, interpolated (PERCENTILE) |80+| `QUARTILE(data, quartile)` | The minimum (0), a quartile (1 to 3) or the maximum (4) |81+| `QUARTILE.EXC(data, quartile)` | A quartile from 1 to 3, between the numbers rather than at them |82+| `QUARTILE.INC(data, quartile)` | The minimum (0), a quartile (1 to 3) or the maximum (4) (QUARTILE) |7683 | `RANK(value, data, [is_ascending])` | Rank of a number among others, largest first by default |84+| `RANK.EQ(value, data, [is_ascending])` | Rank of a number among others, largest first by default (RANK) |7785 | `SMALL(data, n)` | The nth smallest number |7886 | `STDEV(value1, [value2, ...])` | Standard deviation of a sample |7987 | `STDEVP(value1, [value2, ...])` | Standard deviation of a whole population |@@ -19,6 +19,8 @@ var xlfn = map[string]string{
1919 "CHOOSECOLS": "_xlfn.", "CHOOSEROWS": "_xlfn.", "LET": "_xlfn.", "LAMBDA": "_xlfn.",2020 "MAP": "_xlfn.", "REDUCE": "_xlfn.", "SCAN": "_xlfn.", "BYROW": "_xlfn.", "BYCOL": "_xlfn.",2121 "MAKEARRAY": "_xlfn.", "REGEXREPLACE": "_xlfn.",22+ "PERCENTILE.INC": "_xlfn.", "PERCENTILE.EXC": "_xlfn.", "QUARTILE.INC": "_xlfn.", "QUARTILE.EXC": "_xlfn.",23+ "MODE.SNGL": "_xlfn.", "RANK.EQ": "_xlfn.",2224 }2325 2426 // sheetsOnly lists 012's functions, from Sheets, that Excel has nothing@@ -0,0 +1,236 @@
1+package functions2+3+import (4+ "cmp"5+ "math"6+ "slices"7+8+ "github.com/FelineStateMachine/012/internal/formula"9+ "github.com/FelineStateMachine/012/internal/value"10+)11+12+// Order statistics: MEDIAN, MODE, PERCENTILE, QUARTILE, LARGE and SMALL13+// read their numbers once, in ascending order, picking the ones they14+// need as they pass. On a sheet the numbers are held and sorted; over a15+// linked source's column, computing a StreamCall, the Book sorts them16+// on disk (Sorter), so a source of any size takes the memory of a sort17+// run rather than of its column. Either way the same code picks, so the18+// results are the same.19+20+// Sorted is numbers read in ascending order, once.21+type Sorted interface {22+ // Len counts the numbers.23+ Len() int24+ // Next is the next number, ties in the order they were read, with25+ // its position in that order; false at the end, or on an error,26+ // which Err says.27+ Next() (v float64, pos int64, ok bool)28+ Err() error29+ Close() error30+}31+32+// Sorter is a Book that sorts the numbers of a range of a paged sheet33+// (a linked source's column) on disk: its numbers, skipping the cells34+// holding anything else, as a range's are read by SUM, or the first35+// error in it.36+type Sorter interface {37+ SortedNums(sheet string, r Rect) (Sorted, *Value)38+}39+40+// held is numbers held in memory, sorted.41+type held struct {42+ x []posNum43+ i int44+}45+46+type posNum struct {47+ v float6448+ pos int6449+}50+51+func (h *held) Len() int { return len(h.x) }52+func (h *held) Err() error { return nil }53+func (h *held) Close() error { return nil }54+func (h *held) Next() (float64, int64, bool) {55+ if h.i >= len(h.x) {56+ return 0, 0, false57+ }58+ h.i++59+ return h.x[h.i-1].v, h.x[h.i-1].pos, true60+}61+62+// sortedNums is the numbers of args in ascending order, with aggregate63+// semantics (nums): sorted by the Book on disk when computing a64+// StreamCall over one range of a source, else held.65+func sortedNums(args []Node, get lookup) (Sorted, *Value) {66+ if s, ok := get.book.(Sorter); ok && get.stream && len(args) == 1 {67+ if r, ok := get.refOf(args[0]).(formula.Range); ok && r.Sheet != "" {68+ return s.SortedNums(r.Sheet, r.Rect)69+ }70+ }71+ x, err := nums(args, get)72+ if err != nil {73+ return nil, err74+ }75+ h := &held{x: make([]posNum, len(x))}76+ for i, v := range x {77+ h.x[i] = posNum{v, int64(i)}78+ }79+ slices.SortFunc(h.x, func(a, b posNum) int { return cmp.Or(cmpFloat(a.v, b.v), cmp.Compare(a.pos, b.pos)) })80+ return h, nil81+}82+83+// pickAt reads the numbers at the ascending indexes ks of s, in order.84+func pickAt(s Sorted, ks ...int) ([]float64, *Value) {85+ out := make([]float64, 0, len(ks))86+ for i := 0; len(out) < len(ks); i++ {87+ v, _, ok := s.Next()88+ if !ok {89+ return nil, &value.ErrRef // the source failed as it was read90+ }91+ for len(out) < len(ks) && ks[len(out)] == i {92+ out = append(out, v)93+ }94+ }95+ return out, nil96+}97+98+// withSorted passes the numbers of the first argument, or with all of99+// every argument (MEDIAN, MODE), sorted, to f with the rest.100+func withSorted(all bool, f func(s Sorted, rest []Node, get lookup) Value) func([]Node, lookup) Value {101+ return func(args []Node, get lookup) Value {102+ upto := 1103+ if all {104+ upto = len(args)105+ }106+ s, err := sortedNums(args[:upto], get)107+ if err != nil {108+ return *err109+ }110+ defer s.Close()111+ return f(s, args[upto:], get)112+ }113+}114+115+func medianOf(s Sorted, _ []Node, _ lookup) Value {116+ n := s.Len()117+ if n == 0 {118+ return value.ErrNum119+ }120+ if n%2 == 1 {121+ x, err := pickAt(s, n/2)122+ if err != nil {123+ return *err124+ }125+ return num(x[0])126+ }127+ x, err := pickAt(s, n/2-1, n/2)128+ if err != nil {129+ return *err130+ }131+ return num((x[0] + x[1]) / 2)132+}133+134+// modeOf is the most common number, the one read first on a tie, or135+// #N/A when none repeats: one pass over the runs of equal numbers.136+func modeOf(s Sorted, _ []Node, _ lookup) Value {137+ best, bestN, bestPos := 0.0, 1, int64(math.MaxInt64)138+ cur, n, first := 0.0, 0, int64(0)139+ end := func() {140+ if n > bestN || n == bestN && n > 1 && first < bestPos {141+ best, bestN, bestPos = cur, n, first142+ }143+ }144+ for {145+ v, pos, ok := s.Next()146+ if !ok {147+ break148+ }149+ if n > 0 && v == cur {150+ n++151+ continue152+ }153+ end()154+ cur, n, first = v, 1, pos155+ }156+ if s.Err() != nil {157+ return value.ErrRef158+ }159+ end()160+ if bestN < 2 {161+ return value.ErrNA162+ }163+ return num(best)164+}165+166+// nthOf builds LARGE and SMALL: the kth largest or smallest number.167+func nthOf(largest bool) func(Sorted, []Node, lookup) Value {168+ return func(s Sorted, rest []Node, get lookup) Value {169+ n, err := numArg(rest[0], get)170+ if err != nil {171+ return *err172+ }173+ k := int(math.Ceil(n))174+ if k < 1 || k > s.Len() {175+ return value.ErrNum176+ }177+ if largest {178+ k = s.Len() - k + 1179+ }180+ x, err := pickAt(s, k-1)181+ if err != nil {182+ return *err183+ }184+ return num(x[0])185+ }186+}187+188+// percentileOf builds PERCENTILE.INC (exc false) and PERCENTILE.EXC,189+// and with quart QUARTILE.INC and QUARTILE.EXC, whose argument counts190+// quarters.191+func percentileOf(exc, quart bool) func(Sorted, []Node, lookup) Value {192+ return func(s Sorted, rest []Node, get lookup) Value {193+ p, err := numArg(rest[0], get)194+ if err != nil {195+ return *err196+ }197+ if quart {198+ q := math.Trunc(p)199+ if q < 0 || q > 4 || exc && (q < 1 || q > 3) {200+ return value.ErrNum201+ }202+ p = q / 4203+ }204+ return percentile(s, p, exc)205+ }206+}207+208+// percentile interpolates between the numbers either side of rank p of209+// s: (n-1)p counting from 0, or (n+1)p counting from 1 for the210+// exclusive kind, which has no rank before the first or past the last.211+func percentile(s Sorted, p float64, exc bool) Value {212+ n := s.Len()213+ if n == 0 || p < 0 || p > 1 || exc && (p <= 0 || p >= 1) {214+ return value.ErrNum215+ }216+ h := float64(n-1) * p217+ if exc {218+ h = float64(n+1)*p - 1219+ if h < 0 || h > float64(n-1) {220+ return value.ErrNum221+ }222+ }223+ lo := int(math.Floor(h))224+ if lo >= n-1 {225+ x, err := pickAt(s, n-1)226+ if err != nil {227+ return *err228+ }229+ return num(x[0])230+ }231+ x, err := pickAt(s, lo, lo+1)232+ if err != nil {233+ return *err234+ }235+ return num(x[0] + (h-float64(lo))*(x[1]-x[0]))236+}@@ -2,7 +2,6 @@ package functions
22 33 import (44 "math"5- "slices"65 76 "github.com/FelineStateMachine/012/internal/value"87 )@@ -20,9 +19,9 @@ func init() {
2019 &FuncDef{Name: "COUNTBLANK", Args: "range", Desc: "Count of empty cells, including empty text", Min: 1, Max: 1,2120 eval: countBlank},2221 &FuncDef{Name: "MEDIAN", Args: "value1, [value2, ...]", Desc: "Middle value of numbers", Min: 1, Max: -1,23- eval: withNums(median), format: inherit},22+ eval: withSorted(true, medianOf), format: inherit},2423 &FuncDef{Name: "MODE", Args: "value1, [value2, ...]", Desc: "Most common number (the first, on a tie)", Min: 1, Max: -1,25- eval: withNums(mode), format: inherit},24+ eval: withSorted(true, modeOf), format: inherit},2625 &FuncDef{Name: "STDEV", Args: "value1, [value2, ...]", Desc: "Standard deviation of a sample", Min: 1, Max: -1,2726 eval: withNums(func(x []float64) Value { return spread(x, 1, true) })},2827 &FuncDef{Name: "STDEVP", Args: "value1, [value2, ...]", Desc: "Standard deviation of a whole population", Min: 1, Max: -1,@@ -32,53 +31,60 @@ func init() {
3231 &FuncDef{Name: "VARP", Args: "value1, [value2, ...]", Desc: "Variance of a whole population", Min: 1, Max: -1,3332 eval: withNums(func(x []float64) Value { return spread(x, 0, false) })},3433 &FuncDef{Name: "LARGE", Args: "data, n", Desc: "The nth largest number", Min: 2, Max: 2,35- eval: nth(func(a, b float64) int { return cmpFloat(b, a) }), format: inheritFrom(0)},34+ eval: withSorted(false, nthOf(true)), format: inheritFrom(0)},3635 &FuncDef{Name: "SMALL", Args: "data, n", Desc: "The nth smallest number", Min: 2, Max: 2,37- eval: nth(cmpFloat), format: inheritFrom(0)},36+ eval: withSorted(false, nthOf(false)), format: inheritFrom(0)},3837 &FuncDef{Name: "RANK", Args: "value, data, [is_ascending]", Desc: "Rank of a number among others, largest first by default", Min: 2, Max: 3,3938 eval: rank},39+ &FuncDef{Name: "RANK.EQ", Args: "value, data, [is_ascending]", Desc: "Rank of a number among others, largest first by default (RANK)", Min: 2, Max: 3,40+ eval: rank},41+ &FuncDef{Name: "MODE.SNGL", Args: "value1, [value2, ...]", Desc: "Most common number, the first on a tie (MODE)", Min: 1, Max: -1,42+ eval: withSorted(true, modeOf), format: inherit},43+ &FuncDef{Name: "PERCENTILE", Args: "data, percentile", Desc: "The value at a percentile from 0 to 1, interpolated", Min: 2, Max: 2,44+ eval: withSorted(false, percentileOf(false, false)), format: inheritFrom(0)},45+ &FuncDef{Name: "PERCENTILE.INC", Args: "data, percentile", Desc: "The value at a percentile from 0 to 1, interpolated (PERCENTILE)", Min: 2, Max: 2,46+ eval: withSorted(false, percentileOf(false, false)), format: inheritFrom(0)},47+ &FuncDef{Name: "PERCENTILE.EXC", Args: "data, percentile", Desc: "The value at a percentile strictly between 0 and 1, interpolated", Min: 2, Max: 2,48+ eval: withSorted(false, percentileOf(true, false)), format: inheritFrom(0)},49+ &FuncDef{Name: "QUARTILE", Args: "data, quartile", Desc: "The minimum (0), a quartile (1 to 3) or the maximum (4)", Min: 2, Max: 2,50+ eval: withSorted(false, percentileOf(false, true)), format: inheritFrom(0)},51+ &FuncDef{Name: "QUARTILE.INC", Args: "data, quartile", Desc: "The minimum (0), a quartile (1 to 3) or the maximum (4) (QUARTILE)", Min: 2, Max: 2,52+ eval: withSorted(false, percentileOf(false, true)), format: inheritFrom(0)},53+ &FuncDef{Name: "QUARTILE.EXC", Args: "data, quartile", Desc: "A quartile from 1 to 3, between the numbers rather than at them", Min: 2, Max: 2,54+ eval: withSorted(false, percentileOf(true, true)), format: inheritFrom(0)},4055 )4156 }4257 4358 // Evaluators for the table above, in its order.4459 45-func median(x []float64) Value {46- if len(x) == 0 {47- return value.ErrNum48- }49- slices.Sort(x)50- n := len(x)51- if n%2 == 1 {52- return num(x[n/2])53- }54- return num((x[n/2-1] + x[n/2]) / 2)55-}56-57-func mode(x []float64) Value {58- best, bestN := 0.0, 159- for i, v := range x {60- n := 061- for _, w := range x[i:] {62- if w == v {63- n++64- }65- }66- if n > bestN {67- best, bestN = v, n68- }69- }70- if bestN < 2 {71- return value.ErrNA72- }73- return num(best)74-}75-60+// rank counts the numbers of data above and below the value as it reads61+// them, holding none, so a linked source's column of any size streams.7662 func rank(args []Node, get lookup) Value {7763 v, err := numArg(args[0], get)7864 if err != nil {7965 return *err8066 }81- data, err := nums(args[1:2], get)67+ above, below, found := 0, 0, false68+ err = each(args[1:2], get, func(d Value, direct bool) *Value {69+ switch {70+ case d.Kind == value.Error:71+ return errOf(d)72+ case d.Kind == value.Empty, d.Kind != value.Number && !direct:73+ return nil74+ }75+ f, err := toNum(d)76+ switch {77+ case err != nil:78+ return err79+ case f == v:80+ found = true81+ case f > v:82+ above++83+ default:84+ below++85+ }86+ return nil87+ })8288 if err != nil {8389 return *err8490 }@@ -86,19 +92,13 @@ func rank(args []Node, get lookup) Value {
8692 if err != nil {8793 return *err8894 }89- rank, found := 1, false90- for _, d := range data {91- switch {92- case d == v:93- found = true94- case asc && d < v, !asc && d > v:95- rank++96- }97- }9895 if !found {9996 return value.ErrNA10097 }101- return num(float64(rank))98+ if asc {99+ return num(float64(below + 1))100+ }101+ return num(float64(above + 1))102102 }103103 104104 // withNums passes the numbers of all arguments, with aggregate@@ -136,26 +136,6 @@ func spread(x []float64, ddof int, root bool) Value {
136136 return num(v)137137 }138138 139-// nth builds LARGE and SMALL: the nth number of data in cmp order.140-func nth(cmp func(a, b float64) int) func([]Node, lookup) Value {141- return func(args []Node, get lookup) Value {142- x, err := nums(args[:1], get)143- if err != nil {144- return *err145- }146- n, err := numArg(args[1], get)147- if err != nil {148- return *err149- }150- k := int(math.Ceil(n))151- if k < 1 || k > len(x) {152- return value.ErrNum153- }154- slices.SortFunc(x, cmp)155- return num(x[k-1])156- }157-}158-159139 func cmpFloat(a, b float64) int {160140 switch {161141 case a < b:@@ -42,5 +42,24 @@ func TestStatsFunctions(t *testing.T) {
4242 {"=RANK(20, A1:A7, 1)", num(3)},4343 {"=RANK(2, G1:G4)", num(2)},4444 {"=RANK(99, A1:A7)", ErrNA},45+ {"=RANK.EQ(20, A1:A7)", num(2)},46+ {"=RANK(1/0, A1:A7)", ErrDiv0},47+ {"=MODE.SNGL(G1:G4)", num(2)},48+ {"=MODE(3, 2, 2, 3, 1)", num(3)}, // the first read of the most common49+ {"=PERCENTILE(A1:A7, 0.5)", num(15)},50+ {"=PERCENTILE(A1:A7, 0.25)", num(6.25)},51+ {"=PERCENTILE.INC(A1:A7, 1)", num(30)},52+ {"=PERCENTILE(A1:A7, 1.5)", ErrNum},53+ {"=PERCENTILE(A4, 0.5)", ErrNum},54+ {"=PERCENTILE.EXC(C1:C5, 0.25)", num(1.5)},55+ {"=PERCENTILE.EXC(C1:C5, 0.1)", ErrNum},56+ {"=PERCENTILE.EXC(C1:C5, 0)", ErrNum},57+ {"=QUARTILE(C1:C5, 1)", num(2)},58+ {"=QUARTILE(C1:C5, 0)", num(1)},59+ {"=QUARTILE(C1:C5, 4.9)", num(5)},60+ {"=QUARTILE(C1:C5, 5)", ErrNum},61+ {"=QUARTILE.INC(C1:C5, 3)", num(4)},62+ {"=QUARTILE.EXC(C1:C5, 1)", num(1.5)},63+ {"=QUARTILE.EXC(C1:C5, 0)", ErrNum},4564 })4665 }@@ -62,6 +62,10 @@ var formulas = []string{
6262 `=MEDIAN(A1:A7)`, `=MEDIAN(C1:C5)`, `=MODE(G1:G4)`, `=MODE(C1:C5)`, `=STDEV(A1:A7)`, `=STDEVP(A1:A7)`,6363 `=VAR(A1:A7)`, `=VARP(A1:A7)`, `=STDEV(5)`, `=LARGE(A1:A7, 1)`, `=LARGE(A1:A7, 4)`, `=LARGE(A1:A7, 5)`,6464 `=SMALL(C1:C7, 2)`, `=RANK(20, A1:A7)`, `=RANK(20, A1:A7, 1)`, `=RANK(2, G1:G4)`, `=RANK(99, A1:A7)`,65+ `=RANK.EQ(20, A1:A7)`, `=MODE.SNGL(G1:G4)`, `=PERCENTILE(A1:A7, 0.25)`, `=PERCENTILE.INC(A1:A7, 0.9)`,66+ `=PERCENTILE(A1:A7, 1.5)`, `=PERCENTILE.EXC(C1:C5, 0.25)`, `=PERCENTILE.EXC(C1:C5, 0.5)`,67+ `=QUARTILE(C1:C5, 1)`, `=QUARTILE(A1:A7, 3)`, `=QUARTILE.INC(C1:C5, 4)`, `=QUARTILE.EXC(C1:C5, 3)`,68+ `=QUARTILE.EXC(C1:C5, 0)`,6569 // Text6670 `=CONCATENATE("a", 1, TRUE)`, `=CONCAT(A1, "x")`, `=TEXTJOIN(", ", TRUE, A3:A5)`, `=TEXTJOIN("-", FALSE, A3:A5)`,6771 `=LEFT("hello", 2)`, `=LEFT("hello")`, `=LEFT("hi", 10)`, `=LEFT("hi", -1)`, `=RIGHT("hello", 3)`,@@ -117,6 +121,7 @@ var skipped = map[string]string{
117121 `=CHOOSE(4, "a", "b", "c")`: "Sheets gives #NUM! for an index past the choices; Excel gives #VALUE!",118122 119123 // excelize departs from Excel (and Sheets) here.124+ `=PERCENTILE(A1:A7, 1.5)`: "excelize gives #N/A for a percentile past 1; Excel and Sheets give #NUM!",120125 `=SUM(C1:C2, E6)`: "excelize counts TRUE in a referenced cell as 1; Excel and Sheets ignore it",121126 `=SUMIF(B1:B5, "n*", C1)`: "excelize doesn't stretch a smaller sum range from its corner as Excel does",122127 `=SUMPRODUCT(A1:A4, C1:C4)`: "excelize gives #VALUE! for text; Excel and Sheets treat it as 0",