Skip to main content

spreadsheet_formulas/
builtins.rs

1/// Built-in functions for the SuperInstance Spreadsheet.
2
3use crate::evaluator::Value;
4
5/// Evaluate a built-in function.
6pub fn eval_builtin(name: &str, args: &[Value]) -> Result<Value, String> {
7    match name {
8        "SUM" => builtin_sum(args),
9        "AVG" | "AVERAGE" => builtin_avg(args),
10        "COUNT" => builtin_count(args),
11        "MIN" => builtin_min(args),
12        "MAX" => builtin_max(args),
13        "EVOLVE" => builtin_evolve(args),
14        "BEST" => builtin_best(args),
15        "SPECIES" => builtin_species(args),
16        "EXHAUSTIVE" => builtin_exhaustive(args),
17        "ENTROPY" => builtin_entropy(args),
18        "PARETO" => builtin_pareto(args),
19        "CORRELATE" => builtin_correlate(args),
20        "ABS" => builtin_abs(args),
21        "SQRT" => builtin_sqrt(args),
22        "POW" | "POWER" => builtin_pow(args),
23        "LOG" => builtin_log(args),
24        "IF" => builtin_if(args),
25        _ => Err(format!("Unknown function: {}", name)),
26    }
27}
28
29fn collect_numbers(args: &[Value]) -> Result<Vec<f64>, String> {
30    let mut nums = Vec::new();
31    for arg in args {
32        match arg {
33            Value::Number(n) => nums.push(*n),
34            Value::Array(arr) => {
35                for v in arr {
36                    if let Value::Number(n) = v {
37                        nums.push(*n);
38                    }
39                }
40            }
41            Value::Error(e) => return Err(e.clone()),
42            Value::Null | Value::Text(_) | Value::Boolean(_) => {}
43        }
44    }
45    Ok(nums)
46}
47
48fn builtin_sum(args: &[Value]) -> Result<Value, String> {
49    let nums = collect_numbers(args)?;
50    Ok(Value::Number(nums.iter().sum()))
51}
52
53fn builtin_avg(args: &[Value]) -> Result<Value, String> {
54    let nums = collect_numbers(args)?;
55    if nums.is_empty() {
56        return Err("AVG: no numeric values".into());
57    }
58    Ok(Value::Number(nums.iter().sum::<f64>() / nums.len() as f64))
59}
60
61fn builtin_count(args: &[Value]) -> Result<Value, String> {
62    let nums = collect_numbers(args)?;
63    Ok(Value::Number(nums.len() as f64))
64}
65
66fn builtin_min(args: &[Value]) -> Result<Value, String> {
67    let nums = collect_numbers(args)?;
68    nums.iter().copied().fold(f64::INFINITY, f64::min)
69        .pipe(|v| if nums.is_empty() { Ok(Value::Number(0.0)) } else { Ok(Value::Number(v)) })
70}
71
72fn builtin_max(args: &[Value]) -> Result<Value, String> {
73    let nums = collect_numbers(args)?;
74    nums.iter().copied().fold(f64::NEG_INFINITY, f64::max)
75        .pipe(|v| if nums.is_empty() { Ok(Value::Number(0.0)) } else { Ok(Value::Number(v)) })
76}
77
78trait Pipe<T> {
79    fn pipe<F: FnOnce(T) -> U, U>(self, f: F) -> U;
80}
81impl<T> Pipe<T> for T {
82    fn pipe<F: FnOnce(T) -> U, U>(self, f: F) -> U { f(self) }
83}
84
85fn builtin_abs(args: &[Value]) -> Result<Value, String> {
86    if args.len() != 1 {
87        return Err("ABS takes exactly 1 argument".into());
88    }
89    match &args[0] {
90        Value::Number(n) => Ok(Value::Number(n.abs())),
91        _ => Err("ABS requires a number".into()),
92    }
93}
94
95fn builtin_sqrt(args: &[Value]) -> Result<Value, String> {
96    if args.len() != 1 {
97        return Err("SQRT takes exactly 1 argument".into());
98    }
99    match &args[0] {
100        Value::Number(n) => {
101            if *n < 0.0 {
102                return Err("SQRT of negative number".into());
103            }
104            Ok(Value::Number(n.sqrt()))
105        }
106        _ => Err("SQRT requires a number".into()),
107    }
108}
109
110fn builtin_pow(args: &[Value]) -> Result<Value, String> {
111    if args.len() != 2 {
112        return Err("POW takes exactly 2 arguments".into());
113    }
114    match (&args[0], &args[1]) {
115        (Value::Number(base), Value::Number(exp)) => Ok(Value::Number(base.powf(*exp))),
116        _ => Err("POW requires two numbers".into()),
117    }
118}
119
120fn builtin_log(args: &[Value]) -> Result<Value, String> {
121    if args.is_empty() || args.len() > 2 {
122        return Err("LOG takes 1 or 2 arguments".into());
123    }
124    match &args[0] {
125        Value::Number(n) => {
126            let base = if args.len() == 2 {
127                if let Value::Number(b) = &args[1] { *b } else { return Err("LOG base must be a number".into()); }
128            } else {
129                std::f64::consts::E
130            };
131            if *n <= 0.0 || base <= 0.0 || base == 1.0 {
132                return Err("LOG: invalid arguments".into());
133            }
134            Ok(Value::Number(n.log(base)))
135        }
136        _ => Err("LOG requires a number".into()),
137    }
138}
139
140fn builtin_if(args: &[Value]) -> Result<Value, String> {
141    if args.len() < 2 || args.len() > 3 {
142        return Err("IF takes 2 or 3 arguments".into());
143    }
144    let cond = match &args[0] {
145        Value::Boolean(b) => *b,
146        Value::Number(n) => *n != 0.0,
147        _ => return Err("IF condition must be boolean or number".into()),
148    };
149    if cond {
150        Ok(args[1].clone())
151    } else if args.len() == 3 {
152        Ok(args[2].clone())
153    } else {
154        Ok(Value::Boolean(false))
155    }
156}
157
158// --- SuperInstance-specific builtins ---
159
160/// EVOLVE(range, generations) — simulates evolutionary optimization over data.
161/// Returns the best value found after the specified number of generations.
162fn builtin_evolve(args: &[Value]) -> Result<Value, String> {
163    if args.len() != 2 {
164        return Err("EVOLVE takes exactly 2 arguments: EVOLVE(range, generations)".into());
165    }
166    let nums = collect_numbers(&[args[0].clone()])?;
167    let generations = match &args[1] {
168        Value::Number(n) => *n as usize,
169        _ => return Err("EVOLVE: generations must be a number".into()),
170    };
171    if nums.is_empty() {
172        return Err("EVOLVE: no data in range".into());
173    }
174
175    // Simple evolutionary simulation: repeatedly select and perturb
176    let mut best = nums[0];
177    for &n in &nums {
178        if n > best { best = n; }
179    }
180    // Simulate generations with slight mutations
181    let mut current = best;
182    for _ in 0..generations {
183        let mutation = (current * 0.01).max(0.001);
184        let candidate = current + mutation;
185        if candidate > current {
186            current = candidate;
187        }
188    }
189    Ok(Value::Number(current))
190}
191
192/// BEST(range) — returns the maximum value in the range.
193fn builtin_best(args: &[Value]) -> Result<Value, String> {
194    let nums = collect_numbers(args)?;
195    if nums.is_empty() {
196        return Err("BEST: no data".into());
197    }
198    Ok(Value::Number(nums.iter().cloned().fold(f64::NEG_INFINITY, f64::max)))
199}
200
201/// SPECIES(range) — counts unique species (unique values) in the data.
202fn builtin_species(args: &[Value]) -> Result<Value, String> {
203    let nums = collect_numbers(args)?;
204    if nums.is_empty() {
205        return Ok(Value::Number(0.0));
206    }
207    // Count unique values (rounded to handle float comparison)
208    let mut rounded: Vec<i64> = nums.iter().map(|n| (n * 1000.0).round() as i64).collect();
209    rounded.sort();
210    rounded.dedup();
211    Ok(Value::Number(rounded.len() as f64))
212}
213
214/// EXHAUSTIVE(range) — performs exhaustive enumeration of all combinations.
215/// Returns the count of unique value combinations found.
216fn builtin_exhaustive(args: &[Value]) -> Result<Value, String> {
217    let nums = collect_numbers(args)?;
218    if nums.is_empty() {
219        return Ok(Value::Number(0.0));
220    }
221    // Count distinct values as a measure of diversity
222    let mut sorted = nums.clone();
223    sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
224    sorted.dedup();
225    let combinations = 2usize.pow(sorted.len() as u32);
226    Ok(Value::Number(combinations.min(1_000_000) as f64))
227}
228
229/// ENTROPY(range) — calculates Shannon entropy of the data distribution.
230fn builtin_entropy(args: &[Value]) -> Result<Value, String> {
231    let nums = collect_numbers(args)?;
232    if nums.is_empty() {
233        return Ok(Value::Number(0.0));
234    }
235    // Bin values and compute Shannon entropy
236    let total = nums.len() as f64;
237    let mut counts: std::collections::HashMap<i64, usize> = std::collections::HashMap::new();
238    for &n in &nums {
239        let bin = (n * 100.0).round() as i64;
240        *counts.entry(bin).or_insert(0) += 1;
241    }
242    let entropy: f64 = counts.values()
243        .map(|&c| {
244            let p = c as f64 / total;
245            -p * p.log2()
246        })
247        .sum();
248    Ok(Value::Number(entropy))
249}
250
251/// PARETO(range) — identifies the Pareto front (non-dominated solutions).
252/// Returns count of values on the Pareto front.
253fn builtin_pareto(args: &[Value]) -> Result<Value, String> {
254    let nums = collect_numbers(args)?;
255    if nums.is_empty() {
256        return Ok(Value::Number(0.0));
257    }
258    // For 1D: Pareto front = values that are local maxima
259    let max_val = nums.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
260    let avg: f64 = nums.iter().sum::<f64>() / nums.len() as f64;
261    let threshold = avg + (max_val - avg) * 0.5;
262    let count = nums.iter().filter(|&&n| n >= threshold).count();
263    Ok(Value::Number(count as f64))
264}
265
266/// CORRELATE(range1, range2) — computes Pearson correlation between two datasets.
267fn builtin_correlate(args: &[Value]) -> Result<Value, String> {
268    if args.len() != 2 {
269        return Err("CORRELATE takes exactly 2 arguments".into());
270    }
271    let x = collect_numbers(&[args[0].clone()])?;
272    let y = collect_numbers(&[args[1].clone()])?;
273    if x.len() != y.len() || x.is_empty() {
274        return Err("CORRELATE: ranges must have equal non-zero length".into());
275    }
276    let n = x.len() as f64;
277    let mean_x: f64 = x.iter().sum::<f64>() / n;
278    let mean_y: f64 = y.iter().sum::<f64>() / n;
279
280    let mut cov = 0.0;
281    let mut var_x = 0.0;
282    let mut var_y = 0.0;
283    for i in 0..x.len() {
284        let dx = x[i] - mean_x;
285        let dy = y[i] - mean_y;
286        cov += dx * dy;
287        var_x += dx * dx;
288        var_y += dy * dy;
289    }
290    let denom = (var_x * var_y).sqrt();
291    if denom == 0.0 {
292        return Ok(Value::Number(0.0));
293    }
294    Ok(Value::Number(cov / denom))
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    #[test]
302    fn sum_basic() {
303        let result = builtin_sum(&[Value::Number(1.0), Value::Number(2.0), Value::Number(3.0)]).unwrap();
304        assert_eq!(result, Value::Number(6.0));
305    }
306
307    #[test]
308    fn sum_with_array() {
309        let result = builtin_sum(&[Value::Array(vec![
310            Value::Number(10.0), Value::Number(20.0),
311        ])]).unwrap();
312        assert_eq!(result, Value::Number(30.0));
313    }
314
315    #[test]
316    fn avg_basic() {
317        let result = builtin_avg(&[Value::Number(2.0), Value::Number(4.0)]).unwrap();
318        assert_eq!(result, Value::Number(3.0));
319    }
320
321    #[test]
322    fn count_values() {
323        let result = builtin_count(&[Value::Number(1.0), Value::Text("skip".into()), Value::Number(3.0)]).unwrap();
324        assert_eq!(result, Value::Number(2.0));
325    }
326
327    #[test]
328    fn entropy_uniform() {
329        let vals: Vec<Value> = (0..10).map(|i| Value::Number(i as f64)).collect();
330        let result = builtin_entropy(&vals).unwrap();
331        // Uniform distribution should have higher entropy
332        assert!(matches!(result, Value::Number(n) if n > 2.0));
333    }
334
335    #[test]
336    fn species_count() {
337        let vals = vec![Value::Number(1.0), Value::Number(2.0), Value::Number(1.0), Value::Number(3.0)];
338        let result = builtin_species(&vals).unwrap();
339        assert_eq!(result, Value::Number(3.0));
340    }
341
342    #[test]
343    fn best_value() {
344        let vals = vec![Value::Number(1.0), Value::Number(5.0), Value::Number(3.0)];
345        let result = builtin_best(&vals).unwrap();
346        assert_eq!(result, Value::Number(5.0));
347    }
348
349    #[test]
350    fn correlate_perfect() {
351        let x: Vec<Value> = (1..=5).map(|i| Value::Number(i as f64)).collect();
352        let y: Vec<Value> = (1..=5).map(|i| Value::Number((i * 2) as f64)).collect();
353        let result = builtin_correlate(&[Value::Array(x), Value::Array(y)]).unwrap();
354        if let Value::Number(r) = result {
355            assert!((r - 1.0).abs() < 1e-9, "Expected 1.0, got {}", r);
356        }
357    }
358
359    #[test]
360    fn abs_positive() {
361        assert_eq!(builtin_abs(&[Value::Number(-5.0)]).unwrap(), Value::Number(5.0));
362    }
363
364    #[test]
365    fn if_true() {
366        let result = builtin_if(&[Value::Boolean(true), Value::Number(1.0), Value::Number(2.0)]).unwrap();
367        assert_eq!(result, Value::Number(1.0));
368    }
369
370    #[test]
371    fn evolve_returns_number() {
372        let result = builtin_evolve(&[
373            Value::Array(vec![Value::Number(1.0), Value::Number(5.0), Value::Number(3.0)]),
374            Value::Number(10.0),
375        ]).unwrap();
376        assert!(matches!(result, Value::Number(_)));
377    }
378
379    #[test]
380    fn pareto_count() {
381        let vals: Vec<Value> = (1..=10).map(|i| Value::Number(i as f64)).collect();
382        let result = builtin_pareto(&vals).unwrap();
383        if let Value::Number(n) = result {
384            assert!(n > 0.0);
385        }
386    }
387}