Skip to main content

nereids_core/
validation.rs

1//! Small numeric-invariant helpers shared across NEREIDS crates.
2//!
3//! These live in `nereids-core` (the dependency-free foundation crate) so that
4//! the same invariant is enforced identically everywhere instead of being
5//! re-implemented per crate.  Each helper returns the *first* offending
6//! `(index, value)` rather than a formatted error, so the calling crate can
7//! map the failure onto its own error type / message wording without
8//! `nereids-core` having to know about `IoError`, `FittingError`, etc.
9
10/// Locate the first element that is not finite-and-non-negative.
11///
12/// Detector counts (and other count-like quantities) are non-negative by
13/// construction — zero is legitimate, but a NaN, ±∞, or negative entry signals
14/// an upstream loader / normalisation bug.  A bare `v < 0.0` test is *not*
15/// sufficient because `NaN < 0.0` is `false`; this helper therefore rejects on
16/// `!v.is_finite() || v < 0.0`, pairing the order comparison with a finiteness
17/// check (NaN bypasses `<`).
18///
19/// Returns `Err((i, v))` for the first offending element at flat index `i`, or
20/// `Ok(())` if every element is finite and `>= 0.0`.  An empty iterator is
21/// vacuously `Ok(())`.
22pub fn first_non_finite_or_negative<I>(values: I) -> Result<(), (usize, f64)>
23where
24    I: IntoIterator<Item = f64>,
25{
26    for (i, v) in values.into_iter().enumerate() {
27        if !v.is_finite() || v < 0.0 {
28            return Err((i, v));
29        }
30    }
31    Ok(())
32}
33
34#[cfg(test)]
35mod tests {
36    use super::*;
37
38    #[test]
39    fn empty_is_ok() {
40        assert!(first_non_finite_or_negative(std::iter::empty::<f64>()).is_ok());
41    }
42
43    #[test]
44    fn all_finite_non_negative_is_ok() {
45        assert!(first_non_finite_or_negative([0.0, 1.0, 1e9, 2.5]).is_ok());
46    }
47
48    #[test]
49    fn rejects_negative_with_index() {
50        assert_eq!(
51            first_non_finite_or_negative([1.0, 2.0, -3.0, 4.0]),
52            Err((2, -3.0))
53        );
54    }
55
56    #[test]
57    fn rejects_nan_negative_does_not_bypass() {
58        // `NaN < 0.0` is `false`; the `is_finite()` half of the guard is what
59        // catches it.  Reported value compares unequal to itself, so match on
60        // index + NaN-ness rather than `assert_eq!` on the tuple.
61        let err = first_non_finite_or_negative([1.0, f64::NAN, 3.0]).unwrap_err();
62        assert_eq!(err.0, 1);
63        assert!(err.1.is_nan());
64    }
65
66    #[test]
67    fn rejects_positive_and_negative_infinity() {
68        assert_eq!(
69            first_non_finite_or_negative([1.0, f64::INFINITY]),
70            Err((1, f64::INFINITY))
71        );
72        assert_eq!(
73            first_non_finite_or_negative([f64::NEG_INFINITY]),
74            Err((0, f64::NEG_INFINITY))
75        );
76    }
77
78    #[test]
79    fn reports_first_offender_only() {
80        // -1.0 at index 1 comes before NaN at index 3.
81        assert_eq!(
82            first_non_finite_or_negative([1.0, -1.0, 2.0, f64::NAN]),
83            Err((1, -1.0))
84        );
85    }
86}