Skip to main content

jpx_core/extensions/
math.rs

1//! Mathematical functions.
2
3use std::collections::HashSet;
4
5use serde_json::{Number, Value};
6
7use crate::functions::{Function, number_value};
8use crate::interpreter::SearchResult;
9use crate::registry::register_if_enabled;
10use crate::{Context, Runtime, arg, defn};
11
12/// Maximum decimal precision for `to_fixed`. An f64 carries ~17 significant
13/// digits, so anything beyond this is meaningless and only risks a huge
14/// allocation from a user-controlled precision (e.g. `to_fixed(`1`, `1e15`)`).
15const MAX_FLOAT_PRECISION: usize = 100;
16/// Maximum number of bins accepted by `histogram`, to bound allocation.
17const MAX_HISTOGRAM_BINS: usize = 1_000_000;
18
19/// Register only the math functions that are in the enabled set.
20pub fn register_filtered(runtime: &mut Runtime, enabled: &HashSet<&str>) {
21    register_if_enabled(runtime, "round", enabled, Box::new(RoundFn::new()));
22    register_if_enabled(runtime, "floor_fn", enabled, Box::new(FloorFnExt::new()));
23    register_if_enabled(runtime, "ceil_fn", enabled, Box::new(CeilFnExt::new()));
24    register_if_enabled(runtime, "abs_fn", enabled, Box::new(AbsFnExt::new()));
25    register_if_enabled(runtime, "mod_fn", enabled, Box::new(ModFn::new()));
26    register_if_enabled(runtime, "pow", enabled, Box::new(PowFn::new()));
27    register_if_enabled(runtime, "sqrt", enabled, Box::new(SqrtFn::new()));
28    register_if_enabled(runtime, "log", enabled, Box::new(LogFn::new()));
29    register_if_enabled(runtime, "clamp", enabled, Box::new(ClampFn::new()));
30    register_if_enabled(runtime, "median", enabled, Box::new(MedianFn::new()));
31    register_if_enabled(
32        runtime,
33        "percentile",
34        enabled,
35        Box::new(PercentileFn::new()),
36    );
37    register_if_enabled(runtime, "variance", enabled, Box::new(VarianceFn::new()));
38    register_if_enabled(runtime, "stddev", enabled, Box::new(StddevFn::new()));
39    register_if_enabled(runtime, "sin", enabled, Box::new(SinFn::new()));
40    register_if_enabled(runtime, "cos", enabled, Box::new(CosFn::new()));
41    register_if_enabled(runtime, "tan", enabled, Box::new(TanFn::new()));
42    register_if_enabled(runtime, "asin", enabled, Box::new(AsinFn::new()));
43    register_if_enabled(runtime, "acos", enabled, Box::new(AcosFn::new()));
44    register_if_enabled(runtime, "atan", enabled, Box::new(AtanFn::new()));
45    register_if_enabled(runtime, "atan2", enabled, Box::new(Atan2Fn::new()));
46    register_if_enabled(runtime, "deg_to_rad", enabled, Box::new(DegToRadFn::new()));
47    register_if_enabled(runtime, "rad_to_deg", enabled, Box::new(RadToDegFn::new()));
48    register_if_enabled(runtime, "sign", enabled, Box::new(SignFn::new()));
49    register_if_enabled(runtime, "add", enabled, Box::new(AddFn::new()));
50    register_if_enabled(runtime, "subtract", enabled, Box::new(SubtractFn::new()));
51    register_if_enabled(runtime, "multiply", enabled, Box::new(MultiplyFn::new()));
52    register_if_enabled(runtime, "divide", enabled, Box::new(DivideFn::new()));
53    register_if_enabled(runtime, "mode", enabled, Box::new(ModeFn::new()));
54    register_if_enabled(runtime, "to_fixed", enabled, Box::new(ToFixedFn::new()));
55    register_if_enabled(
56        runtime,
57        "format_number",
58        enabled,
59        Box::new(FormatNumberFn::new()),
60    );
61    register_if_enabled(runtime, "histogram", enabled, Box::new(HistogramFn::new()));
62    register_if_enabled(runtime, "normalize", enabled, Box::new(NormalizeFn::new()));
63    register_if_enabled(runtime, "z_score", enabled, Box::new(ZScoreFn::new()));
64    register_if_enabled(
65        runtime,
66        "correlation",
67        enabled,
68        Box::new(CorrelationFn::new()),
69    );
70    register_if_enabled(runtime, "quantile", enabled, Box::new(QuantileFn::new()));
71    register_if_enabled(runtime, "moving_avg", enabled, Box::new(MovingAvgFn::new()));
72    register_if_enabled(runtime, "ewma", enabled, Box::new(EwmaFn::new()));
73    register_if_enabled(
74        runtime,
75        "covariance",
76        enabled,
77        Box::new(CovarianceFn::new()),
78    );
79    register_if_enabled(
80        runtime,
81        "standardize",
82        enabled,
83        Box::new(StandardizeFn::new()),
84    );
85    register_if_enabled(runtime, "quartiles", enabled, Box::new(QuartilesFn::new()));
86    register_if_enabled(
87        runtime,
88        "outliers_iqr",
89        enabled,
90        Box::new(OutliersIqrFn::new()),
91    );
92    register_if_enabled(
93        runtime,
94        "outliers_zscore",
95        enabled,
96        Box::new(OutliersZscoreFn::new()),
97    );
98    register_if_enabled(runtime, "skew", enabled, Box::new(SkewFn::new()));
99    register_if_enabled(runtime, "kurtosis", enabled, Box::new(KurtosisFn::new()));
100    register_if_enabled(runtime, "mad", enabled, Box::new(MadFn::new()));
101    // Time series functions
102    register_if_enabled(runtime, "trend", enabled, Box::new(TrendFn::new()));
103    register_if_enabled(
104        runtime,
105        "trend_slope",
106        enabled,
107        Box::new(TrendSlopeFn::new()),
108    );
109    register_if_enabled(
110        runtime,
111        "rate_of_change",
112        enabled,
113        Box::new(RateOfChangeFn::new()),
114    );
115    register_if_enabled(
116        runtime,
117        "cumulative_sum",
118        enabled,
119        Box::new(CumulativeSumFn::new()),
120    );
121}
122
123// =============================================================================
124// round(number, precision?) -> number
125// =============================================================================
126
127defn!(RoundFn, vec![arg!(number)], Some(arg!(number)));
128
129impl Function for RoundFn {
130    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
131        self.signature.validate(args, ctx)?;
132
133        let n = args[0].as_f64().unwrap();
134
135        let precision = if args.len() > 1 {
136            args[1].as_f64().map(|p| p as i32).unwrap_or(0)
137        } else {
138            0
139        };
140
141        let result = if precision == 0 {
142            n.round()
143        } else {
144            let multiplier = 10_f64.powi(precision);
145            (n * multiplier).round() / multiplier
146        };
147
148        Ok(number_value(result))
149    }
150}
151
152// =============================================================================
153// floor_fn(number) -> number
154// =============================================================================
155
156defn!(FloorFnExt, vec![arg!(number)], None);
157
158impl Function for FloorFnExt {
159    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
160        self.signature.validate(args, ctx)?;
161        let n = args[0].as_f64().unwrap();
162        Ok(Value::Number(Number::from(n.floor() as i64)))
163    }
164}
165
166// =============================================================================
167// ceil_fn(number) -> number
168// =============================================================================
169
170defn!(CeilFnExt, vec![arg!(number)], None);
171
172impl Function for CeilFnExt {
173    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
174        self.signature.validate(args, ctx)?;
175        let n = args[0].as_f64().unwrap();
176        Ok(Value::Number(Number::from(n.ceil() as i64)))
177    }
178}
179
180// =============================================================================
181// abs_fn(number) -> number
182// =============================================================================
183
184defn!(AbsFnExt, vec![arg!(number)], None);
185
186impl Function for AbsFnExt {
187    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
188        self.signature.validate(args, ctx)?;
189        let n = args[0].as_f64().unwrap();
190        Ok(number_value(n.abs()))
191    }
192}
193
194// =============================================================================
195// mod_fn(number, divisor) -> number
196// =============================================================================
197
198defn!(ModFn, vec![arg!(number), arg!(number)], None);
199
200impl Function for ModFn {
201    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
202        self.signature.validate(args, ctx)?;
203
204        let n = args[0].as_f64().unwrap();
205        let divisor = args[1].as_f64().unwrap();
206
207        if divisor == 0.0 {
208            return Err(crate::functions::custom_error(ctx, "Division by zero"));
209        }
210
211        Ok(number_value(n % divisor))
212    }
213}
214
215// =============================================================================
216// pow(base, exponent) -> number
217// =============================================================================
218
219defn!(PowFn, vec![arg!(number), arg!(number)], None);
220
221impl Function for PowFn {
222    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
223        self.signature.validate(args, ctx)?;
224        let base = args[0].as_f64().unwrap();
225        let exp = args[1].as_f64().unwrap();
226        Ok(number_value(base.powf(exp)))
227    }
228}
229
230// =============================================================================
231// sqrt(number) -> number
232// =============================================================================
233
234defn!(SqrtFn, vec![arg!(number)], None);
235
236impl Function for SqrtFn {
237    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
238        self.signature.validate(args, ctx)?;
239        let n = args[0].as_f64().unwrap();
240
241        if n < 0.0 {
242            return Err(crate::functions::custom_error(
243                ctx,
244                "Cannot take square root of negative number",
245            ));
246        }
247
248        Ok(number_value(n.sqrt()))
249    }
250}
251
252// =============================================================================
253// log(number, base?) -> number (default base e)
254// =============================================================================
255
256defn!(LogFn, vec![arg!(number)], Some(arg!(number)));
257
258impl Function for LogFn {
259    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
260        self.signature.validate(args, ctx)?;
261
262        let n = args[0].as_f64().unwrap();
263
264        if n <= 0.0 {
265            return Err(crate::functions::custom_error(
266                ctx,
267                "Logarithm requires positive number",
268            ));
269        }
270
271        let result = if args.len() > 1 {
272            let base = args[1].as_f64().unwrap();
273            n.log(base)
274        } else {
275            n.ln()
276        };
277
278        Ok(number_value(result))
279    }
280}
281
282// =============================================================================
283// clamp(number, min, max) -> number
284// =============================================================================
285
286defn!(
287    ClampFn,
288    vec![arg!(number), arg!(number), arg!(number)],
289    None
290);
291
292impl Function for ClampFn {
293    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
294        self.signature.validate(args, ctx)?;
295
296        let n = args[0].as_f64().unwrap();
297        let min = args[1].as_f64().unwrap();
298        let max = args[2].as_f64().unwrap();
299
300        let result = n.max(min).min(max);
301
302        Ok(number_value(result))
303    }
304}
305
306// =============================================================================
307// median(array) -> number
308// =============================================================================
309
310defn!(MedianFn, vec![arg!(array)], None);
311
312impl Function for MedianFn {
313    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
314        self.signature.validate(args, ctx)?;
315
316        let arr = args[0].as_array().unwrap();
317
318        let mut numbers: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
319
320        if numbers.is_empty() {
321            return Ok(Value::Null);
322        }
323
324        numbers.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
325
326        let len = numbers.len();
327        let median = if len.is_multiple_of(2) {
328            (numbers[len / 2 - 1] + numbers[len / 2]) / 2.0
329        } else {
330            numbers[len / 2]
331        };
332
333        Ok(number_value(median))
334    }
335}
336
337// =============================================================================
338// percentile(array, p) -> number (pth percentile, p in 0-100)
339// =============================================================================
340
341defn!(PercentileFn, vec![arg!(array), arg!(number)], None);
342
343impl Function for PercentileFn {
344    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
345        self.signature.validate(args, ctx)?;
346
347        let arr = args[0].as_array().unwrap();
348        let p = args[1].as_f64().unwrap();
349
350        if !(0.0..=100.0).contains(&p) {
351            return Err(crate::functions::custom_error(
352                ctx,
353                "Percentile must be between 0 and 100",
354            ));
355        }
356
357        let mut numbers: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
358
359        if numbers.is_empty() {
360            return Ok(Value::Null);
361        }
362
363        numbers.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
364
365        let len = numbers.len();
366        if len == 1 {
367            return Ok(number_value(numbers[0]));
368        }
369
370        let rank = (p / 100.0) * (len - 1) as f64;
371        let lower_idx = rank.floor() as usize;
372        let upper_idx = rank.ceil() as usize;
373        let fraction = rank - lower_idx as f64;
374
375        let result = if lower_idx == upper_idx {
376            numbers[lower_idx]
377        } else {
378            numbers[lower_idx] * (1.0 - fraction) + numbers[upper_idx] * fraction
379        };
380
381        Ok(number_value(result))
382    }
383}
384
385// =============================================================================
386// variance(array) -> number (population variance)
387// =============================================================================
388
389defn!(VarianceFn, vec![arg!(array)], None);
390
391impl Function for VarianceFn {
392    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
393        self.signature.validate(args, ctx)?;
394
395        let arr = args[0].as_array().unwrap();
396
397        let numbers: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
398
399        if numbers.is_empty() {
400            return Ok(Value::Null);
401        }
402
403        let mean = numbers.iter().sum::<f64>() / numbers.len() as f64;
404        let variance =
405            numbers.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / numbers.len() as f64;
406
407        Ok(number_value(variance))
408    }
409}
410
411// =============================================================================
412// stddev(array) -> number (population standard deviation)
413// =============================================================================
414
415defn!(StddevFn, vec![arg!(array)], None);
416
417impl Function for StddevFn {
418    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
419        self.signature.validate(args, ctx)?;
420
421        let arr = args[0].as_array().unwrap();
422
423        let numbers: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
424
425        if numbers.is_empty() {
426            return Ok(Value::Null);
427        }
428
429        let mean = numbers.iter().sum::<f64>() / numbers.len() as f64;
430        let variance =
431            numbers.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / numbers.len() as f64;
432        let stddev = variance.sqrt();
433
434        Ok(number_value(stddev))
435    }
436}
437
438// =============================================================================
439// Trigonometric functions
440// =============================================================================
441
442defn!(SinFn, vec![arg!(number)], None);
443
444impl Function for SinFn {
445    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
446        self.signature.validate(args, ctx)?;
447        let n = args[0].as_f64().unwrap();
448        Ok(number_value(n.sin()))
449    }
450}
451
452defn!(CosFn, vec![arg!(number)], None);
453
454impl Function for CosFn {
455    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
456        self.signature.validate(args, ctx)?;
457        let n = args[0].as_f64().unwrap();
458        Ok(number_value(n.cos()))
459    }
460}
461
462defn!(TanFn, vec![arg!(number)], None);
463
464impl Function for TanFn {
465    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
466        self.signature.validate(args, ctx)?;
467        let n = args[0].as_f64().unwrap();
468        Ok(number_value(n.tan()))
469    }
470}
471
472defn!(AsinFn, vec![arg!(number)], None);
473
474impl Function for AsinFn {
475    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
476        self.signature.validate(args, ctx)?;
477        let n = args[0].as_f64().unwrap();
478        let result = n.asin();
479        // Return null for out-of-domain values (|n| > 1 produces NaN)
480        if result.is_nan() {
481            Ok(Value::Null)
482        } else {
483            Ok(number_value(result))
484        }
485    }
486}
487
488defn!(AcosFn, vec![arg!(number)], None);
489
490impl Function for AcosFn {
491    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
492        self.signature.validate(args, ctx)?;
493        let n = args[0].as_f64().unwrap();
494        let result = n.acos();
495        // Return null for out-of-domain values (|n| > 1 produces NaN)
496        if result.is_nan() {
497            Ok(Value::Null)
498        } else {
499            Ok(number_value(result))
500        }
501    }
502}
503
504defn!(AtanFn, vec![arg!(number)], None);
505
506impl Function for AtanFn {
507    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
508        self.signature.validate(args, ctx)?;
509        let n = args[0].as_f64().unwrap();
510        Ok(number_value(n.atan()))
511    }
512}
513
514defn!(Atan2Fn, vec![arg!(number), arg!(number)], None);
515
516impl Function for Atan2Fn {
517    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
518        self.signature.validate(args, ctx)?;
519        let y = args[0].as_f64().unwrap();
520        let x = args[1].as_f64().unwrap();
521        Ok(number_value(y.atan2(x)))
522    }
523}
524
525defn!(DegToRadFn, vec![arg!(number)], None);
526
527impl Function for DegToRadFn {
528    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
529        self.signature.validate(args, ctx)?;
530        let n = args[0].as_f64().unwrap();
531        Ok(number_value(n.to_radians()))
532    }
533}
534
535defn!(RadToDegFn, vec![arg!(number)], None);
536
537impl Function for RadToDegFn {
538    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
539        self.signature.validate(args, ctx)?;
540        let n = args[0].as_f64().unwrap();
541        Ok(number_value(n.to_degrees()))
542    }
543}
544
545defn!(SignFn, vec![arg!(number)], None);
546
547impl Function for SignFn {
548    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
549        self.signature.validate(args, ctx)?;
550        let n = args[0].as_f64().unwrap();
551        let sign = if n > 0.0 {
552            1
553        } else if n < 0.0 {
554            -1
555        } else {
556            0
557        };
558        Ok(Value::Number(Number::from(sign)))
559    }
560}
561
562// =============================================================================
563// add(a, b) -> number
564// =============================================================================
565
566defn!(AddFn, vec![arg!(number), arg!(number)], None);
567
568impl Function for AddFn {
569    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
570        self.signature.validate(args, ctx)?;
571        let a = args[0].as_f64().unwrap();
572        let b = args[1].as_f64().unwrap();
573        Ok(number_value(a + b))
574    }
575}
576
577// =============================================================================
578// subtract(a, b) -> number
579// =============================================================================
580
581defn!(SubtractFn, vec![arg!(number), arg!(number)], None);
582
583impl Function for SubtractFn {
584    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
585        self.signature.validate(args, ctx)?;
586        let a = args[0].as_f64().unwrap();
587        let b = args[1].as_f64().unwrap();
588        Ok(number_value(a - b))
589    }
590}
591
592// =============================================================================
593// multiply(a, b) -> number
594// =============================================================================
595
596defn!(MultiplyFn, vec![arg!(number), arg!(number)], None);
597
598impl Function for MultiplyFn {
599    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
600        self.signature.validate(args, ctx)?;
601        let a = args[0].as_f64().unwrap();
602        let b = args[1].as_f64().unwrap();
603        Ok(number_value(a * b))
604    }
605}
606
607// =============================================================================
608// divide(a, b) -> number
609// =============================================================================
610
611defn!(DivideFn, vec![arg!(number), arg!(number)], None);
612
613impl Function for DivideFn {
614    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
615        self.signature.validate(args, ctx)?;
616        let a = args[0].as_f64().unwrap();
617        let b = args[1].as_f64().unwrap();
618        if b == 0.0 {
619            return Err(crate::functions::custom_error(ctx, "Division by zero"));
620        }
621        Ok(number_value(a / b))
622    }
623}
624
625// =============================================================================
626// mode(array) -> any (most common value)
627// =============================================================================
628
629defn!(ModeFn, vec![arg!(array)], None);
630
631impl Function for ModeFn {
632    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
633        self.signature.validate(args, ctx)?;
634
635        let arr = args[0].as_array().unwrap();
636
637        if arr.is_empty() {
638            return Ok(Value::Null);
639        }
640
641        // Count occurrences - use JSON string representation as key
642        let mut counts: std::collections::HashMap<String, (usize, Value)> =
643            std::collections::HashMap::new();
644
645        for item in arr.iter() {
646            let key = serde_json::to_string(item).unwrap_or_default();
647            counts
648                .entry(key)
649                .and_modify(|(count, _)| *count += 1)
650                .or_insert((1, item.clone()));
651        }
652
653        // Find the value with highest count
654        let (_, (_, mode_value)) = counts
655            .into_iter()
656            .max_by_key(|(_, (count, _))| *count)
657            .unwrap();
658
659        Ok(mode_value)
660    }
661}
662
663// =============================================================================
664// to_fixed(number, precision) -> string
665// =============================================================================
666
667defn!(ToFixedFn, vec![arg!(number), arg!(number)], None);
668
669impl Function for ToFixedFn {
670    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
671        self.signature.validate(args, ctx)?;
672
673        let num = args[0].as_f64().unwrap();
674        // Clamp to a meaningful maximum -- f64 has ~17 significant digits, and a
675        // huge precision would otherwise allocate an enormous string.
676        let precision = (args[1].as_f64().unwrap() as usize).min(MAX_FLOAT_PRECISION);
677
678        let result = format!("{:.prec$}", num, prec = precision);
679        Ok(Value::String(result))
680    }
681}
682
683// =============================================================================
684// format_number(number, precision?, suffix?) -> string
685// =============================================================================
686
687defn!(FormatNumberFn, vec![arg!(number)], Some(arg!(any)));
688
689impl Function for FormatNumberFn {
690    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
691        self.signature.validate(args, ctx)?;
692
693        let num = args[0].as_f64().unwrap();
694
695        let precision = args
696            .get(1)
697            .and_then(|v| v.as_f64())
698            .map(|n| n as usize)
699            .unwrap_or(0);
700
701        let suffix = args.get(2).and_then(|v| v.as_str()).map(|s| s.to_string());
702
703        // Handle suffix scaling (k, M, B, T)
704        let (scaled_num, auto_suffix) = if let Some(ref s) = suffix {
705            match s.as_str() {
706                "k" | "K" => (num / 1_000.0, "k"),
707                "M" => (num / 1_000_000.0, "M"),
708                "B" => (num / 1_000_000_000.0, "B"),
709                "T" => (num / 1_000_000_000_000.0, "T"),
710                "auto" => {
711                    let abs_num = num.abs();
712                    if abs_num >= 1_000_000_000_000.0 {
713                        (num / 1_000_000_000_000.0, "T")
714                    } else if abs_num >= 1_000_000_000.0 {
715                        (num / 1_000_000_000.0, "B")
716                    } else if abs_num >= 1_000_000.0 {
717                        (num / 1_000_000.0, "M")
718                    } else if abs_num >= 1_000.0 {
719                        (num / 1_000.0, "k")
720                    } else {
721                        (num, "")
722                    }
723                }
724                _ => (num, s.as_str()),
725            }
726        } else {
727            (num, "")
728        };
729
730        // Format with precision
731        let formatted = format!("{:.prec$}", scaled_num, prec = precision);
732
733        // Add thousand separators to the integer part
734        let result = if suffix.is_none() || suffix.as_deref() == Some("") {
735            add_thousand_separators(&formatted)
736        } else {
737            format!("{}{}", formatted, auto_suffix)
738        };
739
740        Ok(Value::String(result))
741    }
742}
743
744/// Add thousand separators (commas) to a number string.
745fn add_thousand_separators(s: &str) -> String {
746    let parts: Vec<&str> = s.split('.').collect();
747    let int_part = parts[0];
748    let dec_part = parts.get(1);
749
750    // Handle negative sign
751    let (sign, digits) = if let Some(stripped) = int_part.strip_prefix('-') {
752        ("-", stripped)
753    } else {
754        ("", int_part)
755    };
756
757    // Add commas every 3 digits from the right
758    let digit_chars: Vec<char> = digits.chars().collect();
759    let len = digit_chars.len();
760    let with_commas: String = digit_chars
761        .iter()
762        .enumerate()
763        .map(|(i, c)| {
764            let pos_from_right = len - 1 - i;
765            if pos_from_right > 0 && pos_from_right.is_multiple_of(3) {
766                format!("{},", c)
767            } else {
768                c.to_string()
769            }
770        })
771        .collect();
772
773    match dec_part {
774        Some(dec) => format!("{}{}.{}", sign, with_commas, dec),
775        None => format!("{}{}", sign, with_commas),
776    }
777}
778
779// =============================================================================
780// histogram(array, bins) -> array
781// =============================================================================
782
783defn!(HistogramFn, vec![arg!(array), arg!(number)], None);
784
785impl Function for HistogramFn {
786    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
787        self.signature.validate(args, ctx)?;
788
789        let arr = args[0].as_array().unwrap();
790
791        let num_bins = args[1].as_f64().unwrap() as usize;
792
793        if num_bins > MAX_HISTOGRAM_BINS {
794            return Err(crate::functions::custom_error(
795                ctx,
796                "Number of bins exceeds the maximum allowed",
797            ));
798        }
799
800        if num_bins == 0 {
801            return Err(crate::functions::custom_error(
802                ctx,
803                "Number of bins must be greater than 0",
804            ));
805        }
806
807        // Extract numeric values
808        let values: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
809
810        if values.is_empty() {
811            return Ok(Value::Array(vec![]));
812        }
813
814        let min_val = values.iter().cloned().fold(f64::INFINITY, f64::min);
815        let max_val = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
816
817        // Handle case where all values are the same
818        let bin_width = if (max_val - min_val).abs() < f64::EPSILON {
819            1.0
820        } else {
821            (max_val - min_val) / num_bins as f64
822        };
823
824        // Initialize bins
825        let mut bins: Vec<(f64, f64, usize)> = (0..num_bins)
826            .map(|i| {
827                let bin_min = min_val + (i as f64 * bin_width);
828                let bin_max = if i == num_bins - 1 {
829                    max_val
830                } else {
831                    min_val + ((i + 1) as f64 * bin_width)
832                };
833                (bin_min, bin_max, 0)
834            })
835            .collect();
836
837        // Count values in each bin
838        for val in &values {
839            let bin_idx = if (max_val - min_val).abs() < f64::EPSILON {
840                0
841            } else {
842                let idx = ((val - min_val) / bin_width) as usize;
843                idx.min(num_bins - 1)
844            };
845            bins[bin_idx].2 += 1;
846        }
847
848        // Convert to array of objects
849        let result: Vec<Value> = bins
850            .into_iter()
851            .map(|(bin_min, bin_max, count)| {
852                let mut map = serde_json::Map::new();
853                map.insert("min".to_string(), number_value(bin_min));
854                map.insert("max".to_string(), number_value(bin_max));
855                map.insert("count".to_string(), Value::Number(Number::from(count)));
856                Value::Object(map)
857            })
858            .collect();
859
860        Ok(Value::Array(result))
861    }
862}
863
864// =============================================================================
865// normalize(array) -> array
866// =============================================================================
867
868defn!(NormalizeFn, vec![arg!(array)], None);
869
870impl Function for NormalizeFn {
871    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
872        self.signature.validate(args, ctx)?;
873
874        let arr = args[0].as_array().unwrap();
875
876        let values: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
877
878        if values.is_empty() {
879            return Ok(Value::Array(vec![]));
880        }
881
882        let min_val = values.iter().cloned().fold(f64::INFINITY, f64::min);
883        let max_val = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
884        let range = max_val - min_val;
885
886        let result: Vec<Value> = values
887            .iter()
888            .map(|v| {
889                let normalized = if range.abs() < f64::EPSILON {
890                    0.0
891                } else {
892                    (v - min_val) / range
893                };
894                number_value(normalized)
895            })
896            .collect();
897
898        Ok(Value::Array(result))
899    }
900}
901
902// =============================================================================
903// z_score(array) -> array
904// =============================================================================
905
906defn!(ZScoreFn, vec![arg!(array)], None);
907
908impl Function for ZScoreFn {
909    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
910        self.signature.validate(args, ctx)?;
911
912        let arr = args[0].as_array().unwrap();
913
914        let values: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
915
916        if values.is_empty() {
917            return Ok(Value::Array(vec![]));
918        }
919
920        let n = values.len() as f64;
921        let mean = values.iter().sum::<f64>() / n;
922        let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n;
923        let stddev = variance.sqrt();
924
925        let result: Vec<Value> = values
926            .iter()
927            .map(|v| {
928                let z = if stddev.abs() < f64::EPSILON {
929                    0.0
930                } else {
931                    (v - mean) / stddev
932                };
933                number_value(z)
934            })
935            .collect();
936
937        Ok(Value::Array(result))
938    }
939}
940
941// =============================================================================
942// correlation(arr1, arr2) -> number
943// =============================================================================
944
945defn!(CorrelationFn, vec![arg!(array), arg!(array)], None);
946
947impl Function for CorrelationFn {
948    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
949        self.signature.validate(args, ctx)?;
950
951        let arr1 = args[0].as_array().unwrap();
952        let arr2 = args[1].as_array().unwrap();
953
954        let values1: Vec<f64> = arr1.iter().filter_map(|v| v.as_f64()).collect();
955        let values2: Vec<f64> = arr2.iter().filter_map(|v| v.as_f64()).collect();
956
957        if values1.is_empty() || values2.is_empty() {
958            return Ok(Value::Null);
959        }
960
961        // Use the shorter length
962        let n = values1.len().min(values2.len());
963        if n == 0 {
964            return Ok(Value::Null);
965        }
966
967        let values1 = &values1[..n];
968        let values2 = &values2[..n];
969
970        let mean1 = values1.iter().sum::<f64>() / n as f64;
971        let mean2 = values2.iter().sum::<f64>() / n as f64;
972
973        let mut cov = 0.0;
974        let mut var1 = 0.0;
975        let mut var2 = 0.0;
976
977        for i in 0..n {
978            let d1 = values1[i] - mean1;
979            let d2 = values2[i] - mean2;
980            cov += d1 * d2;
981            var1 += d1 * d1;
982            var2 += d2 * d2;
983        }
984
985        let denom = (var1 * var2).sqrt();
986        let correlation = if denom.abs() < f64::EPSILON {
987            0.0
988        } else {
989            cov / denom
990        };
991
992        Ok(number_value(correlation))
993    }
994}
995
996// =============================================================================
997// quantile(array, q) -> number (q in [0, 1])
998// =============================================================================
999
1000defn!(QuantileFn, vec![arg!(array), arg!(number)], None);
1001
1002impl Function for QuantileFn {
1003    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1004        self.signature.validate(args, ctx)?;
1005
1006        let arr = args[0].as_array().unwrap();
1007        let q = args[1].as_f64().unwrap();
1008
1009        if !(0.0..=1.0).contains(&q) {
1010            return Ok(Value::Null);
1011        }
1012
1013        let mut values: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
1014
1015        if values.is_empty() {
1016            return Ok(Value::Null);
1017        }
1018
1019        values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1020
1021        let n = values.len();
1022        let pos = q * (n - 1) as f64;
1023        let lower = pos.floor() as usize;
1024        let upper = pos.ceil() as usize;
1025        let frac = pos - lower as f64;
1026
1027        let result = if lower == upper {
1028            values[lower]
1029        } else {
1030            values[lower] * (1.0 - frac) + values[upper] * frac
1031        };
1032
1033        Ok(number_value(result))
1034    }
1035}
1036
1037// =============================================================================
1038// moving_avg(array, window) -> array
1039// =============================================================================
1040
1041defn!(MovingAvgFn, vec![arg!(array), arg!(number)], None);
1042
1043impl Function for MovingAvgFn {
1044    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1045        self.signature.validate(args, ctx)?;
1046
1047        let arr = args[0].as_array().unwrap();
1048
1049        let window = args[1].as_f64().unwrap() as usize;
1050
1051        if window == 0 {
1052            return Ok(Value::Null);
1053        }
1054
1055        let values: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
1056
1057        if values.is_empty() || window > values.len() {
1058            return Ok(Value::Array(vec![]));
1059        }
1060
1061        let mut result: Vec<Value> = Vec::new();
1062
1063        for i in 0..values.len() {
1064            if i + 1 < window {
1065                result.push(Value::Null);
1066            } else {
1067                let start = i + 1 - window;
1068                let sum: f64 = values[start..=i].iter().sum();
1069                let avg = sum / window as f64;
1070                result.push(number_value(avg));
1071            }
1072        }
1073
1074        Ok(Value::Array(result))
1075    }
1076}
1077
1078// =============================================================================
1079// ewma(array, alpha) -> array
1080// =============================================================================
1081
1082defn!(EwmaFn, vec![arg!(array), arg!(number)], None);
1083
1084impl Function for EwmaFn {
1085    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1086        self.signature.validate(args, ctx)?;
1087
1088        let arr = args[0].as_array().unwrap();
1089        let alpha = args[1].as_f64().unwrap();
1090
1091        if !(0.0..=1.0).contains(&alpha) {
1092            return Ok(Value::Null);
1093        }
1094
1095        let values: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
1096
1097        if values.is_empty() {
1098            return Ok(Value::Array(vec![]));
1099        }
1100
1101        let mut result: Vec<Value> = Vec::new();
1102        let mut ewma = values[0];
1103
1104        for value in &values {
1105            ewma = alpha * value + (1.0 - alpha) * ewma;
1106            result.push(number_value(ewma));
1107        }
1108
1109        Ok(Value::Array(result))
1110    }
1111}
1112
1113// =============================================================================
1114// covariance(arr1, arr2) -> number
1115// =============================================================================
1116
1117defn!(CovarianceFn, vec![arg!(array), arg!(array)], None);
1118
1119impl Function for CovarianceFn {
1120    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1121        self.signature.validate(args, ctx)?;
1122
1123        let arr1 = args[0].as_array().unwrap();
1124        let arr2 = args[1].as_array().unwrap();
1125
1126        let values1: Vec<f64> = arr1.iter().filter_map(|v| v.as_f64()).collect();
1127        let values2: Vec<f64> = arr2.iter().filter_map(|v| v.as_f64()).collect();
1128
1129        if values1.is_empty() || values1.len() != values2.len() {
1130            return Ok(Value::Null);
1131        }
1132
1133        let n = values1.len() as f64;
1134        let mean1: f64 = values1.iter().sum::<f64>() / n;
1135        let mean2: f64 = values2.iter().sum::<f64>() / n;
1136
1137        let cov: f64 = values1
1138            .iter()
1139            .zip(values2.iter())
1140            .map(|(x, y)| (x - mean1) * (y - mean2))
1141            .sum::<f64>()
1142            / n;
1143
1144        Ok(number_value(cov))
1145    }
1146}
1147
1148// =============================================================================
1149// standardize(array) -> array (z-score normalization)
1150// =============================================================================
1151
1152defn!(StandardizeFn, vec![arg!(array)], None);
1153
1154impl Function for StandardizeFn {
1155    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1156        self.signature.validate(args, ctx)?;
1157
1158        let arr = args[0].as_array().unwrap();
1159
1160        let values: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
1161
1162        if values.is_empty() {
1163            return Ok(Value::Array(vec![]));
1164        }
1165
1166        let n = values.len() as f64;
1167        let mean: f64 = values.iter().sum::<f64>() / n;
1168        let variance: f64 = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / n;
1169        let std_dev = variance.sqrt();
1170
1171        let result: Vec<Value> = values
1172            .iter()
1173            .map(|x| {
1174                let standardized = if std_dev.abs() < f64::EPSILON {
1175                    0.0
1176                } else {
1177                    (x - mean) / std_dev
1178                };
1179                number_value(standardized)
1180            })
1181            .collect();
1182
1183        Ok(Value::Array(result))
1184    }
1185}
1186
1187// =============================================================================
1188// quartiles(array) -> object {q1, q2, q3, min, max, iqr}
1189// =============================================================================
1190
1191defn!(QuartilesFn, vec![arg!(array)], None);
1192
1193impl Function for QuartilesFn {
1194    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1195        self.signature.validate(args, ctx)?;
1196
1197        let arr = args[0].as_array().unwrap();
1198
1199        let mut values: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
1200
1201        if values.is_empty() {
1202            return Ok(Value::Null);
1203        }
1204
1205        values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1206
1207        let n = values.len();
1208        let min = values[0];
1209        let max = values[n - 1];
1210
1211        let q1 = percentile_value(&values, 25.0);
1212        let q2 = percentile_value(&values, 50.0);
1213        let q3 = percentile_value(&values, 75.0);
1214        let iqr = q3 - q1;
1215
1216        let mut result = serde_json::Map::new();
1217        result.insert("min".to_string(), number_value(min));
1218        result.insert("q1".to_string(), number_value(q1));
1219        result.insert("q2".to_string(), number_value(q2));
1220        result.insert("q3".to_string(), number_value(q3));
1221        result.insert("max".to_string(), number_value(max));
1222        result.insert("iqr".to_string(), number_value(iqr));
1223
1224        Ok(Value::Object(result))
1225    }
1226}
1227
1228fn percentile_value(sorted_values: &[f64], p: f64) -> f64 {
1229    let n = sorted_values.len();
1230    if n == 1 {
1231        return sorted_values[0];
1232    }
1233
1234    let k = (p / 100.0) * (n - 1) as f64;
1235    let f = k.floor() as usize;
1236    let c = k.ceil() as usize;
1237
1238    if f == c {
1239        sorted_values[f]
1240    } else {
1241        let d = k - f as f64;
1242        sorted_values[f] * (1.0 - d) + sorted_values[c] * d
1243    }
1244}
1245
1246// =============================================================================
1247// outliers_iqr(array, multiplier?) -> array
1248// =============================================================================
1249
1250defn!(OutliersIqrFn, vec![arg!(array)], Some(arg!(number)));
1251
1252impl Function for OutliersIqrFn {
1253    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1254        self.signature.validate(args, ctx)?;
1255
1256        let arr = args[0].as_array().unwrap();
1257
1258        let multiplier = if args.len() > 1 {
1259            args[1].as_f64().unwrap_or(1.5)
1260        } else {
1261            1.5
1262        };
1263
1264        let mut values: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
1265
1266        if values.is_empty() {
1267            return Ok(Value::Array(vec![]));
1268        }
1269
1270        values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1271
1272        let q1 = percentile_value(&values, 25.0);
1273        let q3 = percentile_value(&values, 75.0);
1274        let iqr = q3 - q1;
1275
1276        let lower_bound = q1 - multiplier * iqr;
1277        let upper_bound = q3 + multiplier * iqr;
1278
1279        let outliers: Vec<Value> = arr
1280            .iter()
1281            .filter(|v| {
1282                if let Some(n) = v.as_f64() {
1283                    n < lower_bound || n > upper_bound
1284                } else {
1285                    false
1286                }
1287            })
1288            .cloned()
1289            .collect();
1290
1291        Ok(Value::Array(outliers))
1292    }
1293}
1294
1295// =============================================================================
1296// outliers_zscore(array, threshold?) -> array
1297// =============================================================================
1298
1299defn!(OutliersZscoreFn, vec![arg!(array)], Some(arg!(number)));
1300
1301impl Function for OutliersZscoreFn {
1302    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1303        self.signature.validate(args, ctx)?;
1304
1305        let arr = args[0].as_array().unwrap();
1306
1307        let threshold = if args.len() > 1 {
1308            args[1].as_f64().unwrap_or(2.0)
1309        } else {
1310            2.0
1311        };
1312
1313        let values: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
1314
1315        if values.is_empty() {
1316            return Ok(Value::Array(vec![]));
1317        }
1318
1319        let n = values.len() as f64;
1320        let mean: f64 = values.iter().sum::<f64>() / n;
1321        let variance: f64 = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n;
1322        let stddev = variance.sqrt();
1323
1324        if stddev.abs() < f64::EPSILON {
1325            return Ok(Value::Array(vec![]));
1326        }
1327
1328        let outliers: Vec<Value> = arr
1329            .iter()
1330            .filter(|v| {
1331                if let Some(n) = v.as_f64() {
1332                    let z = (n - mean) / stddev;
1333                    z.abs() > threshold
1334                } else {
1335                    false
1336                }
1337            })
1338            .cloned()
1339            .collect();
1340
1341        Ok(Value::Array(outliers))
1342    }
1343}
1344
1345// =============================================================================
1346// trend(array) -> string ("increasing", "decreasing", "stable")
1347// =============================================================================
1348
1349defn!(TrendFn, vec![arg!(array)], None);
1350
1351impl Function for TrendFn {
1352    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1353        self.signature.validate(args, ctx)?;
1354
1355        let arr = args[0].as_array().unwrap();
1356
1357        if arr.len() < 2 {
1358            return Ok(Value::String("stable".to_string()));
1359        }
1360
1361        let values: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
1362
1363        if values.len() < 2 {
1364            return Ok(Value::String("stable".to_string()));
1365        }
1366
1367        // Calculate linear regression slope
1368        let n = values.len() as f64;
1369        let sum_x: f64 = (0..values.len()).map(|i| i as f64).sum();
1370        let sum_y: f64 = values.iter().sum();
1371        let sum_xy: f64 = values.iter().enumerate().map(|(i, y)| i as f64 * y).sum();
1372        let sum_x2: f64 = (0..values.len()).map(|i| (i as f64).powi(2)).sum();
1373
1374        let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x.powi(2));
1375
1376        // Determine trend based on slope relative to data magnitude
1377        let mean: f64 = sum_y / n;
1378        let threshold = mean.abs() * 0.01;
1379
1380        let trend = if slope > threshold {
1381            "increasing"
1382        } else if slope < -threshold {
1383            "decreasing"
1384        } else {
1385            "stable"
1386        };
1387
1388        Ok(Value::String(trend.to_string()))
1389    }
1390}
1391
1392// =============================================================================
1393// trend_slope(array) -> number (linear regression slope)
1394// =============================================================================
1395
1396defn!(TrendSlopeFn, vec![arg!(array)], None);
1397
1398impl Function for TrendSlopeFn {
1399    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1400        self.signature.validate(args, ctx)?;
1401
1402        let arr = args[0].as_array().unwrap();
1403
1404        if arr.len() < 2 {
1405            return Ok(number_value(0.0));
1406        }
1407
1408        let values: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
1409
1410        if values.len() < 2 {
1411            return Ok(number_value(0.0));
1412        }
1413
1414        // Calculate linear regression slope
1415        let n = values.len() as f64;
1416        let sum_x: f64 = (0..values.len()).map(|i| i as f64).sum();
1417        let sum_y: f64 = values.iter().sum();
1418        let sum_xy: f64 = values.iter().enumerate().map(|(i, y)| i as f64 * y).sum();
1419        let sum_x2: f64 = (0..values.len()).map(|i| (i as f64).powi(2)).sum();
1420
1421        let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x.powi(2));
1422
1423        Ok(number_value(slope))
1424    }
1425}
1426
1427// =============================================================================
1428// rate_of_change(array) -> array (percentage change between consecutive elements)
1429// =============================================================================
1430
1431defn!(RateOfChangeFn, vec![arg!(array)], None);
1432
1433impl Function for RateOfChangeFn {
1434    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1435        self.signature.validate(args, ctx)?;
1436
1437        let arr = args[0].as_array().unwrap();
1438
1439        if arr.len() < 2 {
1440            return Ok(Value::Array(vec![]));
1441        }
1442
1443        let values: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
1444
1445        if values.len() < 2 {
1446            return Ok(Value::Array(vec![]));
1447        }
1448
1449        let changes: Vec<Value> = values
1450            .windows(2)
1451            .map(|w| {
1452                let prev = w[0];
1453                let curr = w[1];
1454                let pct_change = if prev != 0.0 {
1455                    ((curr - prev) / prev) * 100.0
1456                } else {
1457                    0.0
1458                };
1459                number_value(pct_change)
1460            })
1461            .collect();
1462
1463        Ok(Value::Array(changes))
1464    }
1465}
1466
1467// =============================================================================
1468// cumulative_sum(array) -> array (running total)
1469// =============================================================================
1470
1471defn!(CumulativeSumFn, vec![arg!(array)], None);
1472
1473impl Function for CumulativeSumFn {
1474    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1475        self.signature.validate(args, ctx)?;
1476
1477        let arr = args[0].as_array().unwrap();
1478
1479        let values: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
1480
1481        let mut running_sum = 0.0;
1482        let cumsum: Vec<Value> = values
1483            .iter()
1484            .map(|v| {
1485                running_sum += v;
1486                number_value(running_sum)
1487            })
1488            .collect();
1489
1490        Ok(Value::Array(cumsum))
1491    }
1492}
1493
1494// =============================================================================
1495// skew(array) -> number (Fisher-Pearson coefficient of skewness)
1496// =============================================================================
1497
1498defn!(SkewFn, vec![arg!(array)], None);
1499
1500impl Function for SkewFn {
1501    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1502        self.signature.validate(args, ctx)?;
1503        let arr = args[0].as_array().unwrap();
1504        let numbers: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
1505        let n = numbers.len();
1506        if n < 3 {
1507            return Ok(Value::Null);
1508        }
1509        let mean = numbers.iter().sum::<f64>() / n as f64;
1510        let m2: f64 = numbers.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / n as f64;
1511        let m3: f64 = numbers.iter().map(|x| (x - mean).powi(3)).sum::<f64>() / n as f64;
1512        if m2 == 0.0 {
1513            return Ok(number_value(0.0));
1514        }
1515        let skewness = m3 / m2.powf(1.5);
1516        Ok(number_value(skewness))
1517    }
1518}
1519
1520// =============================================================================
1521// kurtosis(array) -> number (excess kurtosis: normal distribution = 0)
1522// =============================================================================
1523
1524defn!(KurtosisFn, vec![arg!(array)], None);
1525
1526impl Function for KurtosisFn {
1527    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1528        self.signature.validate(args, ctx)?;
1529        let arr = args[0].as_array().unwrap();
1530        let numbers: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
1531        let n = numbers.len();
1532        if n < 4 {
1533            return Ok(Value::Null);
1534        }
1535        let mean = numbers.iter().sum::<f64>() / n as f64;
1536        let m2: f64 = numbers.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / n as f64;
1537        let m4: f64 = numbers.iter().map(|x| (x - mean).powi(4)).sum::<f64>() / n as f64;
1538        if m2 == 0.0 {
1539            return Ok(number_value(0.0));
1540        }
1541        let kurt = (m4 / (m2 * m2)) - 3.0;
1542        Ok(number_value(kurt))
1543    }
1544}
1545
1546// =============================================================================
1547// mad(array) -> number (median absolute deviation)
1548// =============================================================================
1549
1550defn!(MadFn, vec![arg!(array)], None);
1551
1552impl Function for MadFn {
1553    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1554        self.signature.validate(args, ctx)?;
1555        let arr = args[0].as_array().unwrap();
1556        let mut numbers: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
1557        if numbers.is_empty() {
1558            return Ok(Value::Null);
1559        }
1560        numbers.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1561        let len = numbers.len();
1562        let median = if len.is_multiple_of(2) {
1563            (numbers[len / 2 - 1] + numbers[len / 2]) / 2.0
1564        } else {
1565            numbers[len / 2]
1566        };
1567        let mut abs_devs: Vec<f64> = numbers.iter().map(|x| (x - median).abs()).collect();
1568        abs_devs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1569        let mad = if len.is_multiple_of(2) {
1570            (abs_devs[len / 2 - 1] + abs_devs[len / 2]) / 2.0
1571        } else {
1572            abs_devs[len / 2]
1573        };
1574        Ok(number_value(mad))
1575    }
1576}
1577
1578#[cfg(test)]
1579mod tests {
1580    use crate::Runtime;
1581    use serde_json::json;
1582
1583    fn setup_runtime() -> Runtime {
1584        Runtime::builder()
1585            .with_standard()
1586            .with_all_extensions()
1587            .build()
1588    }
1589
1590    #[test]
1591    #[allow(clippy::approx_constant)]
1592    fn test_round() {
1593        let runtime = setup_runtime();
1594        let expr = runtime.compile("round(`3.14159`, `2`)").unwrap();
1595        let result = expr.search(&json!(null)).unwrap();
1596        assert!((result.as_f64().unwrap() - 3.14_f64).abs() < 0.001);
1597    }
1598
1599    #[test]
1600    fn test_to_fixed_clamps_huge_precision() {
1601        // A huge precision must be clamped rather than allocating a giant string.
1602        let runtime = setup_runtime();
1603        let expr = runtime.compile("to_fixed(`1`, `1000000000`)").unwrap();
1604        let result = expr.search(&json!(null)).unwrap();
1605        // "1." + at most MAX_FLOAT_PRECISION (100) fractional digits.
1606        assert!(result.as_str().unwrap().len() <= 103);
1607    }
1608
1609    #[test]
1610    fn test_histogram_rejects_excessive_bins() {
1611        let runtime = setup_runtime();
1612        let expr = runtime
1613            .compile("histogram(`[1,2,3]`, `100000000`)")
1614            .unwrap();
1615        assert!(expr.search(&json!(null)).is_err());
1616    }
1617
1618    #[test]
1619    fn test_sqrt() {
1620        let runtime = setup_runtime();
1621        let expr = runtime.compile("sqrt(`16`)").unwrap();
1622        let result = expr.search(&json!(null)).unwrap();
1623        assert_eq!(result.as_f64().unwrap() as i64, 4);
1624    }
1625
1626    #[test]
1627    fn test_clamp() {
1628        let runtime = setup_runtime();
1629        let expr = runtime.compile("clamp(`5`, `0`, `3`)").unwrap();
1630        let result = expr.search(&json!(null)).unwrap();
1631        assert_eq!(result.as_f64().unwrap() as i64, 3);
1632    }
1633
1634    #[test]
1635    fn test_add() {
1636        let runtime = setup_runtime();
1637        let expr = runtime.compile("add(`1`, `2`)").unwrap();
1638        let result = expr.search(&json!(null)).unwrap();
1639        assert_eq!(result.as_f64().unwrap() as i64, 3);
1640    }
1641
1642    #[test]
1643    fn test_subtract() {
1644        let runtime = setup_runtime();
1645        let expr = runtime.compile("subtract(`10`, `3`)").unwrap();
1646        let result = expr.search(&json!(null)).unwrap();
1647        assert_eq!(result.as_f64().unwrap() as i64, 7);
1648    }
1649
1650    #[test]
1651    fn test_multiply() {
1652        let runtime = setup_runtime();
1653        let expr = runtime.compile("multiply(`4`, `5`)").unwrap();
1654        let result = expr.search(&json!(null)).unwrap();
1655        assert_eq!(result.as_f64().unwrap() as i64, 20);
1656    }
1657
1658    #[test]
1659    fn test_divide() {
1660        let runtime = setup_runtime();
1661        let expr = runtime.compile("divide(`10`, `4`)").unwrap();
1662        let result = expr.search(&json!(null)).unwrap();
1663        assert_eq!(result.as_f64().unwrap(), 2.5);
1664    }
1665
1666    #[test]
1667    fn test_mode_numbers() {
1668        let runtime = setup_runtime();
1669        let expr = runtime.compile("mode(`[1, 2, 2, 3]`)").unwrap();
1670        let result = expr.search(&json!(null)).unwrap();
1671        assert_eq!(result.as_f64().unwrap() as i64, 2);
1672    }
1673
1674    #[test]
1675    fn test_mode_strings() {
1676        let runtime = setup_runtime();
1677        let expr = runtime
1678            .compile("mode(`[\"a\", \"b\", \"a\", \"c\"]`)")
1679            .unwrap();
1680        let result = expr.search(&json!(null)).unwrap();
1681        assert_eq!(result.as_str().unwrap(), "a");
1682    }
1683
1684    #[test]
1685    fn test_mode_empty() {
1686        let runtime = setup_runtime();
1687        let expr = runtime.compile("mode(`[]`)").unwrap();
1688        let result = expr.search(&json!(null)).unwrap();
1689        assert!(result.is_null());
1690    }
1691
1692    #[test]
1693    fn test_to_fixed() {
1694        let runtime = setup_runtime();
1695        let expr = runtime.compile("to_fixed(`3.14159`, `2`)").unwrap();
1696        let result = expr.search(&json!(null)).unwrap();
1697        assert_eq!(result.as_str().unwrap(), "3.14");
1698    }
1699
1700    #[test]
1701    fn test_to_fixed_padding() {
1702        let runtime = setup_runtime();
1703        let expr = runtime.compile("to_fixed(`3.1`, `3`)").unwrap();
1704        let result = expr.search(&json!(null)).unwrap();
1705        assert_eq!(result.as_str().unwrap(), "3.100");
1706    }
1707
1708    #[test]
1709    fn test_format_number_with_separators() {
1710        let runtime = setup_runtime();
1711        let expr = runtime.compile("format_number(`1234567.89`, `2`)").unwrap();
1712        let result = expr.search(&json!(null)).unwrap();
1713        assert_eq!(result.as_str().unwrap(), "1,234,567.89");
1714    }
1715
1716    #[test]
1717    fn test_format_number_with_k_suffix() {
1718        let runtime = setup_runtime();
1719        let expr = runtime.compile("format_number(`1500`, `1`, 'k')").unwrap();
1720        let result = expr.search(&json!(null)).unwrap();
1721        assert_eq!(result.as_str().unwrap(), "1.5k");
1722    }
1723
1724    #[test]
1725    fn test_format_number_with_m_suffix() {
1726        let runtime = setup_runtime();
1727        let expr = runtime
1728            .compile("format_number(`1500000`, `1`, 'M')")
1729            .unwrap();
1730        let result = expr.search(&json!(null)).unwrap();
1731        assert_eq!(result.as_str().unwrap(), "1.5M");
1732    }
1733
1734    #[test]
1735    fn test_format_number_auto_suffix() {
1736        let runtime = setup_runtime();
1737        let expr = runtime
1738            .compile("format_number(`1500000000`, `2`, 'auto')")
1739            .unwrap();
1740        let result = expr.search(&json!(null)).unwrap();
1741        assert_eq!(result.as_str().unwrap(), "1.50B");
1742    }
1743
1744    #[test]
1745    fn test_histogram() {
1746        let runtime = setup_runtime();
1747        let expr = runtime.compile("histogram(@, `3`)").unwrap();
1748        let data = json!([1, 2, 3, 4, 5, 6, 7, 8, 9]);
1749        let result = expr.search(&data).unwrap();
1750        let arr = result.as_array().unwrap();
1751        assert_eq!(arr.len(), 3);
1752        // Each bin should have 3 values
1753        for bin in arr {
1754            let obj = bin.as_object().unwrap();
1755            assert!(obj.contains_key("min"));
1756            assert!(obj.contains_key("max"));
1757            assert!(obj.contains_key("count"));
1758        }
1759    }
1760
1761    #[test]
1762    fn test_normalize() {
1763        let runtime = setup_runtime();
1764        let expr = runtime.compile("normalize(@)").unwrap();
1765        let data = json!([0, 50, 100]);
1766        let result = expr.search(&data).unwrap();
1767        let arr = result.as_array().unwrap();
1768        assert_eq!(arr.len(), 3);
1769        assert!((arr[0].as_f64().unwrap() - 0.0).abs() < 0.001);
1770        assert!((arr[1].as_f64().unwrap() - 0.5).abs() < 0.001);
1771        assert!((arr[2].as_f64().unwrap() - 1.0).abs() < 0.001);
1772    }
1773
1774    #[test]
1775    fn test_z_score() {
1776        let runtime = setup_runtime();
1777        let expr = runtime.compile("z_score(@)").unwrap();
1778        let data = json!([1, 2, 3, 4, 5]);
1779        let result = expr.search(&data).unwrap();
1780        let arr = result.as_array().unwrap();
1781        assert_eq!(arr.len(), 5);
1782        // Middle value (3) should have z-score of 0
1783        assert!((arr[2].as_f64().unwrap() - 0.0).abs() < 0.001);
1784    }
1785
1786    #[test]
1787    fn test_correlation_positive() {
1788        let runtime = setup_runtime();
1789        let expr = runtime
1790            .compile("correlation(`[1, 2, 3]`, `[1, 2, 3]`)")
1791            .unwrap();
1792        let result = expr.search(&json!(null)).unwrap();
1793        assert!((result.as_f64().unwrap() - 1.0).abs() < 0.001);
1794    }
1795
1796    #[test]
1797    fn test_correlation_negative() {
1798        let runtime = setup_runtime();
1799        let expr = runtime
1800            .compile("correlation(`[1, 2, 3]`, `[3, 2, 1]`)")
1801            .unwrap();
1802        let result = expr.search(&json!(null)).unwrap();
1803        assert!((result.as_f64().unwrap() - (-1.0)).abs() < 0.001);
1804    }
1805
1806    #[test]
1807    fn test_quantile_median() {
1808        let runtime = setup_runtime();
1809        let expr = runtime
1810            .compile("quantile(`[1, 2, 3, 4, 5]`, `0.5`)")
1811            .unwrap();
1812        let result = expr.search(&json!(null)).unwrap();
1813        assert_eq!(result.as_f64().unwrap(), 3.0);
1814    }
1815
1816    #[test]
1817    fn test_quantile_quartiles() {
1818        let runtime = setup_runtime();
1819        // First quartile
1820        let expr = runtime
1821            .compile("quantile(`[1, 2, 3, 4, 5]`, `0.25`)")
1822            .unwrap();
1823        let result = expr.search(&json!(null)).unwrap();
1824        assert_eq!(result.as_f64().unwrap(), 2.0);
1825
1826        // Third quartile
1827        let expr = runtime
1828            .compile("quantile(`[1, 2, 3, 4, 5]`, `0.75`)")
1829            .unwrap();
1830        let result = expr.search(&json!(null)).unwrap();
1831        assert_eq!(result.as_f64().unwrap(), 4.0);
1832    }
1833
1834    #[test]
1835    fn test_moving_avg() {
1836        let runtime = setup_runtime();
1837        let expr = runtime
1838            .compile("moving_avg(`[1, 2, 3, 4, 5, 6]`, `3`)")
1839            .unwrap();
1840        let result = expr.search(&json!(null)).unwrap();
1841        let arr = result.as_array().unwrap();
1842        assert_eq!(arr.len(), 6);
1843        assert!(arr[0].is_null());
1844        assert!(arr[1].is_null());
1845        assert_eq!(arr[2].as_f64().unwrap(), 2.0); // (1+2+3)/3
1846        assert_eq!(arr[3].as_f64().unwrap(), 3.0); // (2+3+4)/3
1847        assert_eq!(arr[4].as_f64().unwrap(), 4.0); // (3+4+5)/3
1848        assert_eq!(arr[5].as_f64().unwrap(), 5.0); // (4+5+6)/3
1849    }
1850
1851    #[test]
1852    fn test_ewma() {
1853        let runtime = setup_runtime();
1854        let expr = runtime.compile("ewma(`[1, 2, 3, 4, 5]`, `0.5`)").unwrap();
1855        let result = expr.search(&json!(null)).unwrap();
1856        let arr = result.as_array().unwrap();
1857        assert_eq!(arr.len(), 5);
1858        // First value is just the first value
1859        assert_eq!(arr[0].as_f64().unwrap(), 1.0);
1860        // Subsequent values: alpha * current + (1-alpha) * prev_ewma
1861        assert_eq!(arr[1].as_f64().unwrap(), 1.5); // 0.5*2 + 0.5*1
1862        assert_eq!(arr[2].as_f64().unwrap(), 2.25); // 0.5*3 + 0.5*1.5
1863    }
1864
1865    #[test]
1866    fn test_covariance() {
1867        let runtime = setup_runtime();
1868        let expr = runtime
1869            .compile("covariance(`[1, 2, 3]`, `[1, 2, 3]`)")
1870            .unwrap();
1871        let result = expr.search(&json!(null)).unwrap();
1872        // Variance of [1,2,3] is 2/3
1873        assert!((result.as_f64().unwrap() - 0.666666).abs() < 0.01);
1874    }
1875
1876    #[test]
1877    fn test_covariance_negative() {
1878        let runtime = setup_runtime();
1879        let expr = runtime
1880            .compile("covariance(`[1, 2, 3]`, `[3, 2, 1]`)")
1881            .unwrap();
1882        let result = expr.search(&json!(null)).unwrap();
1883        assert!((result.as_f64().unwrap() - (-0.666666)).abs() < 0.01);
1884    }
1885
1886    #[test]
1887    fn test_standardize() {
1888        let runtime = setup_runtime();
1889        let expr = runtime
1890            .compile("standardize(`[10, 20, 30, 40, 50]`)")
1891            .unwrap();
1892        let result = expr.search(&json!(null)).unwrap();
1893        let arr = result.as_array().unwrap();
1894        assert_eq!(arr.len(), 5);
1895        // Mean is 30, std is ~14.14
1896        // First value: (10-30)/14.14 ~ -1.41
1897        assert!((arr[0].as_f64().unwrap() - (-1.414)).abs() < 0.01);
1898        // Middle value should be 0
1899        assert!(arr[2].as_f64().unwrap().abs() < 0.001);
1900        // Last value: (50-30)/14.14 ~ 1.41
1901        assert!((arr[4].as_f64().unwrap() - 1.414).abs() < 0.01);
1902    }
1903
1904    #[test]
1905    fn test_trend_increasing() {
1906        let runtime = setup_runtime();
1907        let expr = runtime.compile("trend(`[1, 2, 3, 5, 8]`)").unwrap();
1908        let result = expr.search(&json!(null)).unwrap();
1909        assert_eq!(result.as_str().unwrap(), "increasing");
1910    }
1911
1912    #[test]
1913    fn test_trend_decreasing() {
1914        let runtime = setup_runtime();
1915        let expr = runtime.compile("trend(`[10, 9, 8, 7, 6]`)").unwrap();
1916        let result = expr.search(&json!(null)).unwrap();
1917        assert_eq!(result.as_str().unwrap(), "decreasing");
1918    }
1919
1920    #[test]
1921    fn test_trend_stable() {
1922        let runtime = setup_runtime();
1923        let expr = runtime.compile("trend(`[5, 5, 5, 5, 5]`)").unwrap();
1924        let result = expr.search(&json!(null)).unwrap();
1925        assert_eq!(result.as_str().unwrap(), "stable");
1926    }
1927
1928    #[test]
1929    fn test_trend_slope() {
1930        let runtime = setup_runtime();
1931        let expr = runtime.compile("trend_slope(`[0, 1, 2, 3, 4]`)").unwrap();
1932        let result = expr.search(&json!(null)).unwrap();
1933        // Perfect linear increase with slope 1
1934        assert!((result.as_f64().unwrap() - 1.0).abs() < 0.001);
1935    }
1936
1937    #[test]
1938    fn test_rate_of_change() {
1939        let runtime = setup_runtime();
1940        let expr = runtime
1941            .compile("rate_of_change(`[100, 110, 105]`)")
1942            .unwrap();
1943        let result = expr.search(&json!(null)).unwrap();
1944        let arr = result.as_array().unwrap();
1945        assert_eq!(arr.len(), 2);
1946        // 100 -> 110 = 10% increase
1947        assert!((arr[0].as_f64().unwrap() - 10.0).abs() < 0.01);
1948        // 110 -> 105 = -4.545% decrease
1949        assert!((arr[1].as_f64().unwrap() - (-4.545)).abs() < 0.01);
1950    }
1951
1952    #[test]
1953    fn test_cumulative_sum() {
1954        let runtime = setup_runtime();
1955        let expr = runtime.compile("cumulative_sum(`[1, 2, 3, 4]`)").unwrap();
1956        let result = expr.search(&json!(null)).unwrap();
1957        let arr = result.as_array().unwrap();
1958        assert_eq!(arr.len(), 4);
1959        assert_eq!(arr[0].as_f64().unwrap(), 1.0);
1960        assert_eq!(arr[1].as_f64().unwrap(), 3.0);
1961        assert_eq!(arr[2].as_f64().unwrap(), 6.0);
1962        assert_eq!(arr[3].as_f64().unwrap(), 10.0);
1963    }
1964
1965    #[test]
1966    fn test_skew_symmetric() {
1967        let runtime = setup_runtime();
1968        let expr = runtime.compile("skew(`[1, 2, 3, 4, 5]`)").unwrap();
1969        let result = expr.search(&json!(null)).unwrap();
1970        assert!((result.as_f64().unwrap() - 0.0).abs() < 0.001);
1971    }
1972
1973    #[test]
1974    fn test_skew_right() {
1975        let runtime = setup_runtime();
1976        let expr = runtime.compile("skew(`[1, 1, 1, 10]`)").unwrap();
1977        let result = expr.search(&json!(null)).unwrap();
1978        // Right-skewed: positive skewness
1979        assert!(result.as_f64().unwrap() > 0.0);
1980    }
1981
1982    #[test]
1983    fn test_skew_too_few() {
1984        let runtime = setup_runtime();
1985        let expr = runtime.compile("skew(`[1, 2]`)").unwrap();
1986        let result = expr.search(&json!(null)).unwrap();
1987        assert!(result.is_null());
1988    }
1989
1990    #[test]
1991    fn test_skew_constant() {
1992        let runtime = setup_runtime();
1993        let expr = runtime.compile("skew(`[5, 5, 5, 5]`)").unwrap();
1994        let result = expr.search(&json!(null)).unwrap();
1995        assert_eq!(result.as_f64().unwrap(), 0.0);
1996    }
1997
1998    #[test]
1999    fn test_kurtosis_uniform_like() {
2000        let runtime = setup_runtime();
2001        let expr = runtime.compile("kurtosis(`[1, 2, 3, 4, 5]`)").unwrap();
2002        let result = expr.search(&json!(null)).unwrap();
2003        // Uniform-like: negative excess kurtosis (platykurtic)
2004        assert!(result.as_f64().unwrap() < 0.0);
2005    }
2006
2007    #[test]
2008    fn test_kurtosis_too_few() {
2009        let runtime = setup_runtime();
2010        let expr = runtime.compile("kurtosis(`[1, 2, 3]`)").unwrap();
2011        let result = expr.search(&json!(null)).unwrap();
2012        assert!(result.is_null());
2013    }
2014
2015    #[test]
2016    fn test_kurtosis_constant() {
2017        let runtime = setup_runtime();
2018        let expr = runtime.compile("kurtosis(`[5, 5, 5, 5]`)").unwrap();
2019        let result = expr.search(&json!(null)).unwrap();
2020        assert_eq!(result.as_f64().unwrap(), 0.0);
2021    }
2022
2023    #[test]
2024    fn test_mad_simple() {
2025        let runtime = setup_runtime();
2026        let expr = runtime.compile("mad(`[1, 2, 3, 4, 5]`)").unwrap();
2027        let result = expr.search(&json!(null)).unwrap();
2028        // Median = 3, deviations = [2, 1, 0, 1, 2], sorted = [0, 1, 1, 2, 2], median = 1.0
2029        assert_eq!(result.as_f64().unwrap(), 1.0);
2030    }
2031
2032    #[test]
2033    fn test_mad_empty() {
2034        let runtime = setup_runtime();
2035        let expr = runtime.compile("mad(`[]`)").unwrap();
2036        let result = expr.search(&json!(null)).unwrap();
2037        assert!(result.is_null());
2038    }
2039
2040    #[test]
2041    fn test_mad_single() {
2042        let runtime = setup_runtime();
2043        let expr = runtime.compile("mad(`[42]`)").unwrap();
2044        let result = expr.search(&json!(null)).unwrap();
2045        // Single value: median = 42, deviation = 0, MAD = 0
2046        assert_eq!(result.as_f64().unwrap(), 0.0);
2047    }
2048
2049    #[test]
2050    fn test_mad_even_length() {
2051        let runtime = setup_runtime();
2052        let expr = runtime.compile("mad(`[1, 2, 3, 4]`)").unwrap();
2053        let result = expr.search(&json!(null)).unwrap();
2054        // Median = 2.5, deviations = [1.5, 0.5, 0.5, 1.5], sorted = [0.5, 0.5, 1.5, 1.5], median = 1.0
2055        assert_eq!(result.as_f64().unwrap(), 1.0);
2056    }
2057}