#[must_use]
pub fn wrap_position(p: f64, circumference: f64) -> f64 {
if !circumference.is_finite() || circumference <= 0.0 || !p.is_finite() {
return p;
}
let r = p.rem_euclid(circumference);
if r >= circumference { 0.0 } else { r }
}
#[must_use]
pub fn forward_distance(from: f64, to: f64, circumference: f64) -> f64 {
if !circumference.is_finite() || circumference <= 0.0 || !from.is_finite() || !to.is_finite() {
return 0.0;
}
let from = wrap_position(from, circumference);
let to = wrap_position(to, circumference);
let d = to - from;
if d < 0.0 { d + circumference } else { d }
}
#[must_use]
pub fn cyclic_distance(a: f64, b: f64, circumference: f64) -> f64 {
if !circumference.is_finite() || circumference <= 0.0 {
return 0.0;
}
let fwd = forward_distance(a, b, circumference);
let back = circumference - fwd;
fwd.min(back)
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(actual: f64, expected: f64) {
assert!(
(actual - expected).abs() < 1e-12,
"expected {expected}, got {actual}",
);
}
#[test]
fn wrap_handles_zero_circumference() {
approx(wrap_position(50.0, 0.0), 50.0);
approx(wrap_position(50.0, -1.0), 50.0);
}
#[test]
fn wrap_handles_non_finite() {
assert!(wrap_position(f64::NAN, 100.0).is_nan());
assert!(wrap_position(f64::INFINITY, 100.0).is_infinite());
}
#[test]
fn forward_distance_is_directional() {
approx(forward_distance(10.0, 30.0, 100.0), 20.0);
approx(forward_distance(30.0, 10.0, 100.0), 80.0);
}
#[test]
fn forward_distance_returns_zero_on_non_finite() {
approx(forward_distance(f64::INFINITY, 30.0, 100.0), 0.0);
approx(forward_distance(30.0, f64::INFINITY, 100.0), 0.0);
approx(forward_distance(f64::NAN, 30.0, 100.0), 0.0);
approx(forward_distance(30.0, f64::NAN, 100.0), 0.0);
approx(forward_distance(f64::NEG_INFINITY, 30.0, 100.0), 0.0);
}
#[test]
fn helpers_handle_nan_circumference() {
approx(wrap_position(5.0, f64::NAN), 5.0);
approx(forward_distance(5.0, 10.0, f64::NAN), 0.0);
approx(cyclic_distance(5.0, 10.0, f64::NAN), 0.0);
}
#[test]
fn forward_distance_zero_on_coincident() {
approx(forward_distance(50.0, 50.0, 100.0), 0.0);
approx(forward_distance(0.0, 100.0, 100.0), 0.0);
}
#[test]
fn cyclic_distance_is_symmetric() {
for &(a, b) in &[(10.0_f64, 30.0_f64), (5.0, 95.0), (0.0, 50.0)] {
let ab = cyclic_distance(a, b, 100.0);
let ba = cyclic_distance(b, a, 100.0);
assert!((ab - ba).abs() < 1e-12, "{a} -> {b}: {ab} vs {ba}");
}
}
#[test]
fn cyclic_distance_capped_at_half_circumference() {
for d in 0..=100 {
let result = cyclic_distance(0.0, f64::from(d), 100.0);
assert!(result <= 50.0 + 1e-12, "d={d} result={result}");
}
}
}