pub fn change(values: &[f64], lag: usize) -> Vec<f64> {
if values.len() <= lag {
return Vec::new();
}
values[lag..]
.iter()
.zip(values)
.map(|(¤t, &previous)| current - previous)
.collect()
}
pub fn change_ratio(values: &[f64], lag: usize) -> Vec<f64> {
if values.len() <= lag {
return Vec::new();
}
values[lag..]
.iter()
.zip(values)
.map(|(¤t, &previous)| current / previous - 1.0)
.collect()
}
pub fn since(values: &[f64]) -> Vec<usize> {
let mut out = Vec::with_capacity(values.len());
let mut count = 0usize;
let mut last: Option<f64> = None;
for &v in values {
match last {
Some(l) if l == v => count += 1,
_ => {
last = Some(v);
count = 0;
}
}
out.push(count);
}
out
}
pub fn percent_rank(values: &[f64], period: usize) -> Vec<f64> {
assert!(period > 1, "period must be greater than 1");
if values.len() <= period {
return Vec::new();
}
let mut out = Vec::with_capacity(values.len() - period);
for i in period..values.len() {
let window = &values[i + 1 - period..i];
let less = window.iter().filter(|&&v| v < values[i]).count();
out.push(less as f64 * 100.0 / (period - 1) as f64);
}
out
}
pub fn ratio_index(positive: f64, negative: f64) -> f64 {
let ratio = positive / negative;
100.0 - (100.0 / (1.0 + ratio))
}
#[cfg(test)]
mod tests {
use super::super::math_test_support::{EPS, assert_vec_close};
use super::{change, change_ratio, percent_rank, ratio_index, since};
#[test]
fn change_basic() {
let out = change(&[1.0, 2.0, 3.0, 4.0, 5.0], 2);
assert_vec_close(&out, &[2.0, 2.0, 2.0], EPS);
}
#[test]
fn change_ratio_basic() {
let out = change_ratio(&[1.0, 2.0, 4.0, 3.0], 1);
assert_vec_close(&out, &[1.0, 1.0, -0.25], EPS);
}
#[test]
fn changes_cover_empty_short_equal_lag_and_lag_zero() {
let cases: [(&[f64], usize, &[f64]); 6] = [
(&[], 0, &[]),
(&[1.0], 1, &[]),
(&[1.0, 2.0], 2, &[]),
(&[1.0, 2.0], 1, &[1.0]),
(&[1.0, 2.0], 0, &[0.0, 0.0]),
(&[2.0, 4.0], 0, &[0.0, 0.0]),
];
for (values, lag, expected) in cases {
assert_eq!(change(values, lag), expected);
assert_eq!(change_ratio(values, lag).len(), expected.len());
}
assert_eq!(change_ratio(&[2.0, 4.0], 0), [0.0, 0.0]);
}
#[test]
fn since_basic() {
let out = since(&[1.0, 1.0, 2.0, 2.0, 2.0, 3.0]);
assert_eq!(out, vec![0, 1, 0, 1, 2, 0]);
}
#[test]
fn since_keeps_equal_runs_separate_across_nan() {
let values = [1.0, 1.0, f64::NAN, f64::NAN, 1.0, 1.0];
assert_eq!(since(&values), [0, 1, 0, 0, 0, 1]);
}
#[test]
fn percent_rank_basic() {
let out = percent_rank(&[1.0, 2.0, 3.0, 0.0, 4.0], 3);
assert_vec_close(&out, &[0.0, 100.0], EPS);
}
#[test]
#[should_panic(expected = "period must be greater than 1")]
fn percent_rank_period_zero_panics_before_short_input_gate() {
percent_rank(&[], 0);
}
#[test]
#[should_panic(expected = "period must be greater than 1")]
fn percent_rank_period_one_panics_before_short_input_gate() {
percent_rank(&[], 1);
}
#[test]
fn percent_rank_oversized_period_is_empty() {
assert!(percent_rank(&[1.0, 2.0], 3).is_empty());
}
#[test]
fn percent_rank_preserves_ties_descending_and_nonfinite_comparisons() {
let cases: [(&[f64], usize, &[f64]); 5] = [
(&[2.0, 2.0, 2.0, 2.0, 2.0], 3, &[0.0, 0.0]),
(&[4.0, 3.0, 2.0, 1.0, 0.0], 3, &[0.0, 0.0]),
(&[1.0, 2.0, 3.0, f64::NAN], 3, &[0.0]),
(&[1.0, 2.0, 3.0, f64::INFINITY], 3, &[100.0]),
(&[f64::NEG_INFINITY, 1.0, 2.0, 3.0], 3, &[100.0]),
];
for (values, period, expected) in cases {
assert_eq!(percent_rank(values, period), expected);
}
}
#[test]
fn ratio_index_equal_positive_negative_is_fifty() {
assert!((ratio_index(50.0, 50.0) - 50.0).abs() <= EPS);
}
#[test]
fn ratio_index_zero_positive_is_zero() {
assert!((ratio_index(0.0, 100.0) - 0.0).abs() <= EPS);
}
#[test]
fn ratio_index_zero_negative_is_one_hundred() {
assert!((ratio_index(100.0, 0.0) - 100.0).abs() <= EPS);
}
#[test]
fn ratio_index_positive_greater_than_negative_above_fifty() {
assert!((ratio_index(75.0, 25.0) - 75.0).abs() <= EPS);
}
#[test]
fn ratio_index_positive_less_than_negative_below_fifty() {
assert!((ratio_index(25.0, 75.0) - 25.0).abs() <= EPS);
}
#[test]
fn ratio_index_both_zero_is_nan() {
assert!(ratio_index(0.0, 0.0).is_nan());
}
#[test]
fn ratio_index_negative_equals_negative_positive_is_infinite() {
let result = ratio_index(50.0, -50.0);
assert!(result.is_infinite());
}
#[test]
fn ratio_index_negative_denominator_positive_numerator_below_zero() {
assert!((ratio_index(100.0, -50.0) - 200.0).abs() <= EPS);
}
#[test]
fn ratio_index_both_negative_above_fifty() {
assert!((ratio_index(-50.0, -50.0) - 50.0).abs() <= EPS);
}
#[test]
fn ratio_index_large_equal_values_is_fifty() {
assert!((ratio_index(1e12, 1e12) - 50.0).abs() <= EPS);
}
#[test]
fn ratio_index_symmetric_around_fifty() {
let a = 80.0;
let b = 20.0;
let sum = ratio_index(a, b) + ratio_index(b, a);
assert!((sum - 100.0).abs() <= EPS);
}
#[test]
fn ratio_index_tiny_positive_tiny_negative_is_fifty() {
assert!((ratio_index(1e-10, 1e-10) - 50.0).abs() <= EPS);
}
#[test]
fn ratio_index_extreme_positive_dominance_approaches_one_hundred() {
let result = ratio_index(1e9, 1.0);
assert!((result - 100.0).abs() <= 1e-6);
}
#[test]
fn ratio_index_extreme_negative_dominance_approaches_zero() {
let result = ratio_index(1.0, 1e9);
assert!((result - 0.0).abs() <= 1e-6);
}
#[test]
fn ratio_index_preserves_signed_zero_and_nonfinite_results() {
let cases: [(f64, f64, fn(f64) -> bool); 6] = [
(0.0, -1.0, f64::is_sign_positive),
(-0.0, 1.0, f64::is_sign_positive),
(f64::INFINITY, f64::INFINITY, f64::is_nan),
(f64::NEG_INFINITY, f64::INFINITY, f64::is_nan),
(1.0, f64::INFINITY, |value| value == 0.0),
(1.0, f64::NEG_INFINITY, |value| value == 0.0),
];
for (positive, negative, matches) in cases {
assert!(matches(ratio_index(positive, negative)));
}
}
}