use rhai::{Array, Dynamic, Engine, EvalAltResult};
pub fn register_functions(engine: &mut Engine) {
engine.register_fn("window_values", window_values);
engine.register_fn("window_numbers", window_numbers);
engine.register_fn("percentile", percentile);
}
fn window_values(window: Array, field_name: String) -> Result<Array, Box<EvalAltResult>> {
let mut results = Array::new();
for event_dynamic in window {
if let Some(event_map) = event_dynamic.try_cast::<rhai::Map>() {
if let Some(field_value) = event_map.get(field_name.as_str()) {
let value_str = if field_value.is_string() {
field_value.clone().into_string().unwrap_or_default()
} else {
field_value.to_string()
};
if !value_str.is_empty() {
results.push(Dynamic::from(value_str));
}
}
}
}
Ok(results)
}
fn window_numbers(window: Array, field_name: String) -> Result<Array, Box<EvalAltResult>> {
let mut results = Array::new();
for event_dynamic in window {
if let Some(event_map) = event_dynamic.try_cast::<rhai::Map>() {
if let Some(field_value) = event_map.get(field_name.as_str()) {
let number_opt = if field_value.is_int() {
Some(field_value.as_int().unwrap_or(0) as f64)
} else if field_value.is_float() {
Some(field_value.as_float().unwrap_or(0.0))
} else if field_value.is_bool() {
Some(if field_value.as_bool().unwrap_or(false) {
1.0
} else {
0.0
})
} else if field_value.is_string() {
let str_value = field_value.clone().into_string().unwrap_or_default();
str_value.parse::<f64>().ok()
} else {
field_value.to_string().parse::<f64>().ok()
};
if let Some(number) = number_opt {
results.push(Dynamic::from(number));
}
}
}
}
Ok(results)
}
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::{Array, Dynamic, Map};
fn create_test_event(fields: Vec<(&str, Dynamic)>) -> Dynamic {
let mut map = Map::new();
for (key, value) in fields {
map.insert(key.into(), value);
}
Dynamic::from(map)
}
#[test]
fn test_window_values_basic() {
let window = vec![
create_test_event(vec![("status", Dynamic::from("200"))]),
create_test_event(vec![("status", Dynamic::from("404"))]),
create_test_event(vec![("status", Dynamic::from("500"))]),
];
let result = window_values(window, "status".to_string()).unwrap();
assert_eq!(result.len(), 3);
assert_eq!(result[0].clone().into_string().unwrap(), "200");
assert_eq!(result[1].clone().into_string().unwrap(), "404");
assert_eq!(result[2].clone().into_string().unwrap(), "500");
}
#[test]
fn test_window_values_missing_fields() {
let window = vec![
create_test_event(vec![("status", Dynamic::from("200"))]),
create_test_event(vec![("message", Dynamic::from("error"))]), create_test_event(vec![("status", Dynamic::from("404"))]),
];
let result = window_values(window, "status".to_string()).unwrap();
assert_eq!(result.len(), 2); assert_eq!(result[0].clone().into_string().unwrap(), "200");
assert_eq!(result[1].clone().into_string().unwrap(), "404");
}
#[test]
fn test_window_numbers_basic() {
let window = vec![
create_test_event(vec![("response_time", Dynamic::from(0.15))]),
create_test_event(vec![("response_time", Dynamic::from(0.23))]),
create_test_event(vec![("response_time", Dynamic::from(0.89))]),
];
let result = window_numbers(window, "response_time".to_string()).unwrap();
assert_eq!(result.len(), 3);
assert_eq!(result[0].as_float().unwrap(), 0.15);
assert_eq!(result[1].as_float().unwrap(), 0.23);
assert_eq!(result[2].as_float().unwrap(), 0.89);
}
#[test]
fn test_window_numbers_string_parsing() {
let window = vec![
create_test_event(vec![("count", Dynamic::from("123"))]),
create_test_event(vec![("count", Dynamic::from("45.67"))]),
create_test_event(vec![("count", Dynamic::from("invalid"))]), ];
let result = window_numbers(window, "count".to_string()).unwrap();
assert_eq!(result.len(), 2); assert_eq!(result[0].as_float().unwrap(), 123.0);
assert_eq!(result[1].as_float().unwrap(), 45.67);
}
#[test]
fn test_window_numbers_mixed_types() {
let window = vec![
create_test_event(vec![("value", Dynamic::from(42i64))]), create_test_event(vec![("value", Dynamic::from(2.5))]), create_test_event(vec![("value", Dynamic::from(true))]), create_test_event(vec![("value", Dynamic::from(false))]), ];
let result = window_numbers(window, "value".to_string()).unwrap();
assert_eq!(result.len(), 4);
assert_eq!(result[0].as_float().unwrap(), 42.0);
assert_eq!(result[1].as_float().unwrap(), 2.5);
assert_eq!(result[2].as_float().unwrap(), 1.0);
assert_eq!(result[3].as_float().unwrap(), 0.0);
}
#[test]
fn test_empty_window() {
let window = Array::new();
let values_result = window_values(window.clone(), "field".to_string()).unwrap();
let numbers_result = window_numbers(window, "field".to_string()).unwrap();
assert_eq!(values_result.len(), 0);
assert_eq!(numbers_result.len(), 0);
}
#[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);
}
}