Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
fn isqrt(n: i128) -> i128 {
if n < 2 {
return n;
}
let mut x = n;
let mut y = (x + 1) / 2;
while y < x {
x = y;
y = (x + n / x) / 2;
}
x
}
// Half-up with halves away from zero, as math.round-div rounds.
fn half_up(n: i128, d: i128) -> i128 {
let q = (2 * n.abs() + d) / (2 * d);
if n < 0 { -q } else { q }
}
fn median_twice(sorted: &[i128]) -> i128 {
let m = sorted.len() / 2;
if sorted.len() % 2 == 1 { 2 * sorted[m] } else { sorted[m - 1] + sorted[m] }
}
/// Whether `value` is an outlier against `history`, by z-score (`stddev`) or
/// by Iglewicz and Hoaglin's modified z-score (`mad`). Exact integer
/// arithmetic: the threshold comparison never depends on float rounding.
///
/// # Panics
/// Panics on a history under 2 values, a threshold under 1 or an unknown method.
pub fn detect_anomaly(history: &[i64], value: i64, method: &str, threshold_hundredths: i64) -> Anomaly {
if history.len() < 2 {
panic!("history must have at least 2 values, received {}", history.len());
}
if threshold_hundredths < 1 {
panic!("thresholdHundredths must be at least 1, received {}", threshold_hundredths);
}
let xs: Vec<i128> = history.iter().map(|&x| x as i128).collect();
let n = xs.len() as i128;
let v = value as i128;
let t = threshold_hundredths as i128;
let (anomaly, signed, center, spread, score) = match method {
"stddev" => {
let s: i128 = xs.iter().sum();
let ss: i128 = xs.iter().map(|x| x * x).sum();
// n^2 * variance and n * (value - mean), both whole numbers.
let q = n * ss - s * s;
let signed = n * v - s;
let center = half_up(s * 1000, n);
let spread = isqrt(1_000_000 * q) / n;
if q == 0 {
(signed != 0, signed, center, spread, None)
} else {
let anomaly = 10000 * signed * signed > t * t * q;
(anomaly, signed, center, spread, Some(isqrt(10000 * signed * signed / q)))
}
}
"mad" => {
// Twice the median and four times the MAD are whole numbers.
let mut sorted = xs.clone();
sorted.sort();
let med2 = median_twice(&sorted);
let mut devs: Vec<i128> = xs.iter().map(|x| (2 * x - med2).abs()).collect();
devs.sort();
let mad4 = median_twice(&devs);
let signed = 2 * v - med2;
if mad4 == 0 {
(signed != 0, signed, med2 * 500, 0, None)
} else {
// |M| = 0.6745 * |2v - med2| / 2 / (mad4 / 4) = 1349 * |signed| / (1000 * mad4)
let anomaly = 1349 * signed.abs() * 100 > t * 1000 * mad4;
let score = 1349 * signed.abs() * 100 / (1000 * mad4);
(anomaly, signed, med2 * 500, mad4 * 250, Some(score))
}
}
other => panic!("unknown anomaly method: {}", other),
};
let direction = if !anomaly { "normal" } else if signed > 0 { "high" } else { "low" };
Anomaly {
anomaly,
direction: direction.to_string(),
center_milli: center as i64,
spread_milli: spread as i64,
score_hundredths: score.map(|s| s as i64),
}
}
pub fn anomaly_to_value(a: &Anomaly) -> Value {
Value::obj(vec![
("anomaly", Value::Bool(a.anomaly)),
("direction", Value::str(&a.direction)),
("centerMilli", Value::Int(a.center_milli)),
("spreadMilli", Value::Int(a.spread_milli)),
("scoreHundredths", match a.score_hundredths {
Some(s) => Value::Int(s),
None => Value::Null,
}),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
let history: Vec<i64> = args[0].as_arr().iter().map(|x| x.as_i64()).collect();
anomaly_to_value(&detect_anomaly(&history, args[1].as_i64(), args[2].as_str(), args[3].as_i64()))
}