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