Skip to content

Commit 0b687d0

Browse files
authored
feat(go): add 14 formula functions (MEDIAN, STDEV, STDEVP, VAR, VARP, RANK, LARGE, SMALL, PERCENTILE, RAND, RANDBETWEEN, IFS, SWITCH, XLOOKUP) (#4)
1 parent 7baafb2 commit 0b687d0

3 files changed

Lines changed: 587 additions & 5 deletions

File tree

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Pure Go library for reading, writing, and evaluating ODS (OpenDocument Spreadshe
1212

1313
- Read and write `.ods` files (OpenDocument Spreadsheet)
1414
- Cell value management with typed getters/setters (string, int, float, bool, date)
15-
- Formula evaluation engine with 20+ spreadsheet functions
15+
- Formula evaluation engine with 45+ spreadsheet functions
1616
- Formula recalculation with dependency graph and circular reference detection
1717
- Auto-recalc mode: formulas update automatically when cell values change
1818
- Sheet management (create, copy, rename, delete)
@@ -102,12 +102,12 @@ f.SetCellFloat("Sheet1", "A1", 50)
102102

103103
| Category | Functions |
104104
|----------|-----------|
105-
| Math | `SUM`, `AVERAGE`, `MIN`, `MAX`, `ABS`, `ROUND`, `FLOOR`, `CEIL`, `INT`, `MOD`, `POWER`, `SQRT` |
106-
| Logic | `IF`, `AND`, `OR`, `NOT`, `IFERROR` |
105+
| Math | `SUM`, `AVERAGE`, `MIN`, `MAX`, `ABS`, `ROUND`, `FLOOR`, `CEIL`, `INT`, `MOD`, `POWER`, `SQRT`, `RAND`, `RANDBETWEEN` |
106+
| Logic | `IF`, `IFS`, `SWITCH`, `AND`, `OR`, `NOT`, `IFERROR` |
107107
| Text | `CONCATENATE`, `LEN`, `TRIM`, `UPPER`, `LOWER`, `LEFT`, `RIGHT`, `MID`, `FIND`, `SEARCH`, `SUBSTITUTE`, `REPLACE`, `TEXT`, `VALUE` |
108-
| Stats | `COUNT`, `COUNTA`, `COUNTIF`, `COUNTIFS` |
108+
| Stats | `COUNT`, `COUNTA`, `COUNTIF`, `COUNTIFS`, `MEDIAN`, `STDEV`, `STDEVP`, `VAR`, `VARP`, `RANK`, `LARGE`, `SMALL`, `PERCENTILE` |
109109
| Conditional | `SUMIF`, `SUMIFS`, `SUMPRODUCT` |
110-
| Lookup | `VLOOKUP`, `HLOOKUP`, `INDEX`, `MATCH` |
110+
| Lookup | `VLOOKUP`, `HLOOKUP`, `XLOOKUP`, `INDEX`, `MATCH` |
111111
| Date/Time | `DATE`, `TODAY`, `NOW`, `YEAR`, `MONTH`, `DAY` |
112112

113113
## Reading ODS Files

formula_functions_advanced.go

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
package goods
2+
3+
import (
4+
"fmt"
5+
"math"
6+
"math/rand/v2"
7+
"sort"
8+
)
9+
10+
func collectNumbers(args []interface{}) []float64 {
11+
var out []float64
12+
for _, a := range args {
13+
if vals := flattenArg(a); vals != nil {
14+
for _, v := range vals {
15+
if isEmptyValue(v) {
16+
continue
17+
}
18+
if f, ok := toFloat(v); ok {
19+
out = append(out, f)
20+
}
21+
}
22+
} else {
23+
if isEmptyValue(a) {
24+
continue
25+
}
26+
if f, ok := toFloat(a); ok {
27+
out = append(out, f)
28+
}
29+
}
30+
}
31+
return out
32+
}
33+
34+
func evalMEDIAN(args []interface{}) (interface{}, error) {
35+
nums := collectNumbers(args)
36+
if len(nums) == 0 {
37+
return nil, fmt.Errorf("MEDIAN: no numeric values")
38+
}
39+
sort.Float64s(nums)
40+
n := len(nums)
41+
if n%2 == 1 {
42+
return nums[n/2], nil
43+
}
44+
return (nums[n/2-1] + nums[n/2]) / 2, nil
45+
}
46+
47+
func variance(nums []float64, sample bool) (float64, error) {
48+
if len(nums) == 0 || (sample && len(nums) < 2) {
49+
return 0, fmt.Errorf("not enough data points")
50+
}
51+
var mean float64
52+
for _, v := range nums {
53+
mean += v
54+
}
55+
mean /= float64(len(nums))
56+
var sum float64
57+
for _, v := range nums {
58+
d := v - mean
59+
sum += d * d
60+
}
61+
denom := float64(len(nums))
62+
if sample {
63+
denom = float64(len(nums) - 1)
64+
}
65+
return sum / denom, nil
66+
}
67+
68+
func evalVAR(args []interface{}) (interface{}, error) {
69+
nums := collectNumbers(args)
70+
return variance(nums, true)
71+
}
72+
73+
func evalVARP(args []interface{}) (interface{}, error) {
74+
nums := collectNumbers(args)
75+
return variance(nums, false)
76+
}
77+
78+
func evalSTDEV(args []interface{}) (interface{}, error) {
79+
v, err := evalVAR(args)
80+
if err != nil {
81+
return nil, err
82+
}
83+
return math.Sqrt(v.(float64)), nil
84+
}
85+
86+
func evalSTDEVP(args []interface{}) (interface{}, error) {
87+
v, err := evalVARP(args)
88+
if err != nil {
89+
return nil, err
90+
}
91+
return math.Sqrt(v.(float64)), nil
92+
}
93+
94+
func evalRANK(args []interface{}) (interface{}, error) {
95+
if len(args) < 2 {
96+
return nil, fmt.Errorf("RANK requires at least 2 arguments")
97+
}
98+
target, ok := toFloat(args[0])
99+
if !ok {
100+
return nil, fmt.Errorf("RANK: number must be numeric")
101+
}
102+
nums := collectNumbers(args[1:2])
103+
if len(nums) == 0 {
104+
return nil, fmt.Errorf("RANK: empty reference")
105+
}
106+
107+
ascending := false
108+
if len(args) >= 3 {
109+
if f, ok := toFloat(args[2]); ok && f != 0 {
110+
ascending = true
111+
}
112+
}
113+
114+
sorted := make([]float64, len(nums))
115+
copy(sorted, nums)
116+
if ascending {
117+
sort.Float64s(sorted)
118+
} else {
119+
sort.Sort(sort.Reverse(sort.Float64Slice(sorted)))
120+
}
121+
122+
for i, v := range sorted {
123+
if v == target {
124+
return float64(i + 1), nil
125+
}
126+
}
127+
return nil, fmt.Errorf("RANK: value not found in reference")
128+
}
129+
130+
func evalLARGE(args []interface{}) (interface{}, error) {
131+
if len(args) < 2 {
132+
return nil, fmt.Errorf("LARGE requires 2 arguments")
133+
}
134+
k, ok := toFloat(args[1])
135+
if !ok || k < 1 {
136+
return nil, fmt.Errorf("LARGE: k must be >= 1")
137+
}
138+
nums := collectNumbers(args[:1])
139+
if int(k) > len(nums) {
140+
return nil, fmt.Errorf("LARGE: k exceeds dataset size")
141+
}
142+
sort.Sort(sort.Reverse(sort.Float64Slice(nums)))
143+
return nums[int(k)-1], nil
144+
}
145+
146+
func evalSMALL(args []interface{}) (interface{}, error) {
147+
if len(args) < 2 {
148+
return nil, fmt.Errorf("SMALL requires 2 arguments")
149+
}
150+
k, ok := toFloat(args[1])
151+
if !ok || k < 1 {
152+
return nil, fmt.Errorf("SMALL: k must be >= 1")
153+
}
154+
nums := collectNumbers(args[:1])
155+
if int(k) > len(nums) {
156+
return nil, fmt.Errorf("SMALL: k exceeds dataset size")
157+
}
158+
sort.Float64s(nums)
159+
return nums[int(k)-1], nil
160+
}
161+
162+
func evalPERCENTILE(args []interface{}) (interface{}, error) {
163+
if len(args) < 2 {
164+
return nil, fmt.Errorf("PERCENTILE requires 2 arguments")
165+
}
166+
p, ok := toFloat(args[1])
167+
if !ok || p < 0 || p > 1 {
168+
return nil, fmt.Errorf("PERCENTILE: k must be between 0 and 1")
169+
}
170+
nums := collectNumbers(args[:1])
171+
if len(nums) == 0 {
172+
return nil, fmt.Errorf("PERCENTILE: empty dataset")
173+
}
174+
sort.Float64s(nums)
175+
if len(nums) == 1 {
176+
return nums[0], nil
177+
}
178+
179+
pos := p * float64(len(nums)-1)
180+
lower := int(math.Floor(pos))
181+
upper := int(math.Ceil(pos))
182+
if lower == upper {
183+
return nums[lower], nil
184+
}
185+
frac := pos - float64(lower)
186+
return nums[lower] + frac*(nums[upper]-nums[lower]), nil
187+
}
188+
189+
func evalRAND(args []interface{}) (interface{}, error) {
190+
if len(args) != 0 {
191+
return nil, fmt.Errorf("RAND takes no arguments")
192+
}
193+
return rand.Float64(), nil
194+
}
195+
196+
func evalRANDBETWEEN(args []interface{}) (interface{}, error) {
197+
if len(args) != 2 {
198+
return nil, fmt.Errorf("RANDBETWEEN requires 2 arguments")
199+
}
200+
lo, lok := toFloat(args[0])
201+
hi, hok := toFloat(args[1])
202+
if !lok || !hok {
203+
return nil, fmt.Errorf("RANDBETWEEN: bounds must be numeric")
204+
}
205+
if hi < lo {
206+
return nil, fmt.Errorf("RANDBETWEEN: upper bound below lower")
207+
}
208+
loInt := int64(math.Ceil(lo))
209+
hiInt := int64(math.Floor(hi))
210+
if hiInt < loInt {
211+
return nil, fmt.Errorf("RANDBETWEEN: no integer in range")
212+
}
213+
return float64(loInt + rand.Int64N(hiInt-loInt+1)), nil
214+
}
215+
216+
func evalIFS(args []interface{}) (interface{}, error) {
217+
if len(args) < 2 || len(args)%2 != 0 {
218+
return nil, fmt.Errorf("IFS requires pairs of condition/value")
219+
}
220+
for i := 0; i < len(args); i += 2 {
221+
if toBool(args[i]) {
222+
return args[i+1], nil
223+
}
224+
}
225+
return nil, fmt.Errorf("IFS: no condition matched")
226+
}
227+
228+
func evalSWITCH(args []interface{}) (interface{}, error) {
229+
if len(args) < 3 {
230+
return nil, fmt.Errorf("SWITCH requires at least 3 arguments")
231+
}
232+
key := args[0]
233+
i := 1
234+
for i+1 < len(args) {
235+
if valuesEqual(key, args[i]) {
236+
return args[i+1], nil
237+
}
238+
i += 2
239+
}
240+
if i < len(args) {
241+
return args[i], nil
242+
}
243+
return nil, fmt.Errorf("SWITCH: no match and no default")
244+
}
245+
246+
func evalXLOOKUP(args []interface{}) (interface{}, error) {
247+
if len(args) < 3 {
248+
return nil, fmt.Errorf("XLOOKUP requires at least 3 arguments")
249+
}
250+
lookup := args[0]
251+
lookupArray := flattenArg(args[1])
252+
returnArray := flattenArg(args[2])
253+
if lookupArray == nil || returnArray == nil {
254+
return nil, fmt.Errorf("XLOOKUP: lookup_array and return_array must be ranges")
255+
}
256+
if len(lookupArray) != len(returnArray) {
257+
return nil, fmt.Errorf("XLOOKUP: arrays must have matching lengths")
258+
}
259+
260+
for i, v := range lookupArray {
261+
if valuesEqual(lookup, v) {
262+
return returnArray[i], nil
263+
}
264+
}
265+
if len(args) >= 4 {
266+
return args[3], nil
267+
}
268+
return nil, fmt.Errorf("XLOOKUP: value not found")
269+
}
270+
271+
func valuesEqual(a, b interface{}) bool {
272+
if af, aok := toFloat(a); aok {
273+
if bf, bok := toFloat(b); bok {
274+
return af == bf
275+
}
276+
}
277+
as := fmt.Sprintf("%v", a)
278+
bs := fmt.Sprintf("%v", b)
279+
return as == bs
280+
}
281+
282+
func init() {
283+
additional := map[string]formulaFunc{
284+
"MEDIAN": evalMEDIAN,
285+
"STDEV": evalSTDEV,
286+
"STDEVP": evalSTDEVP,
287+
"VAR": evalVAR,
288+
"VARP": evalVARP,
289+
"RANK": evalRANK,
290+
"LARGE": evalLARGE,
291+
"SMALL": evalSMALL,
292+
"PERCENTILE": evalPERCENTILE,
293+
"RAND": evalRAND,
294+
"RANDBETWEEN": evalRANDBETWEEN,
295+
"IFS": evalIFS,
296+
"SWITCH": evalSWITCH,
297+
"XLOOKUP": evalXLOOKUP,
298+
}
299+
for k, v := range additional {
300+
formulaFunctions[k] = v
301+
}
302+
}

0 commit comments

Comments
 (0)