use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "f32", into = "f32")]
pub struct NonNegativeRate(f32);
impl NonNegativeRate {
#[must_use]
pub const fn new(r: f32) -> Self {
assert!(
r.is_finite() && r >= 0.0,
"NonNegativeRate::new: value must be finite and >= 0 (NaN/Inf/negative rejected)"
);
Self(r)
}
pub fn try_new(r: f32) -> Result<Self, NonNegativeRateError> {
if r.is_finite() && r >= 0.0 {
Ok(Self(r))
} else {
Err(NonNegativeRateError { got: r })
}
}
#[must_use]
pub const fn get(self) -> f32 {
self.0
}
}
impl TryFrom<f32> for NonNegativeRate {
type Error = NonNegativeRateError;
fn try_from(r: f32) -> Result<Self, Self::Error> {
Self::try_new(r)
}
}
impl From<NonNegativeRate> for f32 {
fn from(r: NonNegativeRate) -> Self {
r.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, thiserror::Error)]
#[error("invalid rate: {got} must be finite and >= 0 (NaN, infinite, and negative are rejected)")]
pub struct NonNegativeRateError {
pub got: f32,
}
#[cfg(test)]
mod tests {
use super::{NonNegativeRate, NonNegativeRateError};
#[test]
fn new_accepts_zero_and_large_finite() {
assert_eq!(NonNegativeRate::new(0.0).get(), 0.0);
assert_eq!(NonNegativeRate::new(0.5).get(), 0.5);
assert_eq!(NonNegativeRate::new(42.0).get(), 42.0); }
#[test]
#[should_panic(expected = "finite and >= 0")]
fn new_panics_on_negative() {
let _ = NonNegativeRate::new(-0.1);
}
#[test]
#[should_panic(expected = "finite and >= 0")]
fn new_panics_on_nan() {
let _ = NonNegativeRate::new(f32::NAN);
}
#[test]
#[should_panic(expected = "finite and >= 0")]
fn new_panics_on_infinite() {
let _ = NonNegativeRate::new(f32::INFINITY);
}
#[test]
fn try_new_accepts_valid_including_large() {
assert!(NonNegativeRate::try_new(0.0).is_ok());
assert!(NonNegativeRate::try_new(0.3).is_ok());
assert!(NonNegativeRate::try_new(100.0).is_ok());
}
#[test]
fn try_new_rejects_negative_nan_and_inf() {
assert_eq!(
NonNegativeRate::try_new(-1.0),
Err(NonNegativeRateError { got: -1.0 })
);
assert!(NonNegativeRate::try_new(f32::NAN).is_err());
assert!(NonNegativeRate::try_new(f32::INFINITY).is_err());
assert!(NonNegativeRate::try_new(f32::NEG_INFINITY).is_err());
}
#[test]
fn round_trip_via_try_from_and_into() {
let r = NonNegativeRate::try_from(0.3).unwrap();
let back: f32 = r.into();
assert_eq!(back, 0.3);
assert!(NonNegativeRate::try_from(f32::INFINITY).is_err());
}
#[test]
fn error_display_names_the_value() {
let s = NonNegativeRateError { got: -2.0 }.to_string();
assert!(s.contains("-2"));
assert!(s.contains("finite"));
}
}