use rhai::{Array, Engine, EvalAltResult};
pub fn register_functions(engine: &mut Engine) {
engine.register_fn("percentile", percentile);
}
fn percentile(arr: Array, p: f64) -> Result<f64, Box<EvalAltResult>> {
if arr.is_empty() {
return Err("Cannot calculate percentile of empty array".into());
}
if !(0.0..=100.0).contains(&p) {
return Err("Percentile must be between 0 and 100".into());
}
let mut values: Vec<f64> = Vec::new();
for item in arr {
let number_opt = if item.is_int() {
Some(item.as_int().unwrap_or(0) as f64)
} else if item.is_float() {
Some(item.as_float().unwrap_or(0.0))
} else if item.is_bool() {
Some(if item.as_bool().unwrap_or(false) {
1.0
} else {
0.0
})
} else if item.is_string() {
let str_value = item.clone().into_string().unwrap_or_default();
str_value.parse::<f64>().ok()
} else {
item.to_string().parse::<f64>().ok()
};
if let Some(number) = number_opt {
values.push(number);
}
}
if values.is_empty() {
return Err("No numeric values found in array".into());
}
values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let index = (p / 100.0) * (values.len() - 1) as f64;
let lower = index.floor() as usize;
let upper = index.ceil() as usize;
if lower == upper {
Ok(values[lower])
} else {
let weight = index - lower as f64;
Ok(values[lower] * (1.0 - weight) + values[upper] * weight)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rhai::Dynamic;
#[test]
fn test_percentile_basic() {
let arr = vec![
Dynamic::from(1.0),
Dynamic::from(2.0),
Dynamic::from(3.0),
Dynamic::from(4.0),
Dynamic::from(5.0),
];
let median = percentile(arr.clone(), 50.0).unwrap();
assert_eq!(median, 3.0);
let min = percentile(arr.clone(), 0.0).unwrap();
assert_eq!(min, 1.0);
let max = percentile(arr, 100.0).unwrap();
assert_eq!(max, 5.0);
}
#[test]
fn test_percentile_interpolation() {
let arr = vec![
Dynamic::from(1.0),
Dynamic::from(2.0),
Dynamic::from(3.0),
Dynamic::from(4.0),
];
let p25 = percentile(arr.clone(), 25.0).unwrap();
assert_eq!(p25, 1.75);
let p75 = percentile(arr, 75.0).unwrap();
assert_eq!(p75, 3.25);
}
#[test]
fn test_percentile_mixed_types() {
let arr = vec![
Dynamic::from(42i64), Dynamic::from(2.5), Dynamic::from("123.5"), Dynamic::from(true), Dynamic::from(false), ];
let median = percentile(arr, 50.0).unwrap();
assert_eq!(median, 2.5);
}
#[test]
fn test_percentile_filters_non_numeric() {
let arr = vec![
Dynamic::from(1.0),
Dynamic::from("not_a_number"),
Dynamic::from(3.0),
Dynamic::from(5.0),
];
let median = percentile(arr, 50.0).unwrap();
assert_eq!(median, 3.0);
}
#[test]
fn test_percentile_error_cases() {
let empty_arr = Array::new();
assert!(percentile(empty_arr, 50.0).is_err());
let non_numeric_arr = vec![Dynamic::from("text"), Dynamic::from("more_text")];
assert!(percentile(non_numeric_arr, 50.0).is_err());
let valid_arr = vec![Dynamic::from(1.0)];
assert!(percentile(valid_arr.clone(), -1.0).is_err()); assert!(percentile(valid_arr, 101.0).is_err()); }
#[test]
fn test_percentile_single_value() {
let arr = vec![Dynamic::from(42.0)];
assert_eq!(percentile(arr.clone(), 0.0).unwrap(), 42.0);
assert_eq!(percentile(arr.clone(), 50.0).unwrap(), 42.0);
assert_eq!(percentile(arr, 100.0).unwrap(), 42.0);
}
}