Skip to main content

hfx_core/
area.rs

1//! Area and weight measurement types.
2
3/// Errors from constructing measured quantities.
4#[derive(Debug, thiserror::Error)]
5pub enum MeasureError {
6    /// Returned when a value is negative.
7    #[error("value must be non-negative, got {value}")]
8    NegativeValue {
9        /// The invalid value.
10        value: f32,
11    },
12
13    /// Returned when a value is NaN or infinite.
14    #[error("value must be finite, got {value}")]
15    NonFiniteValue {
16        /// The invalid value.
17        value: f32,
18    },
19}
20
21/// A catchment area expressed in square kilometres.
22///
23/// Invariant: the wrapped value is always finite and non-negative.
24///
25/// `Eq` is intentionally not derived — deriving `Eq` on `f32` is a Rust
26/// footgun because IEEE-754 NaN != NaN. The constructor ensures the value is
27/// finite, but the derive would still be misleading. Use `PartialEq` directly
28/// or compare via [`AreaKm2::get`].
29#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
30pub struct AreaKm2(f32);
31
32impl AreaKm2 {
33    /// Construct an [`AreaKm2`] from a raw `f32`.
34    ///
35    /// # Errors
36    ///
37    /// | Condition | Error variant |
38    /// |-----------|---------------|
39    /// | `raw` is NaN or infinite | [`MeasureError::NonFiniteValue`] |
40    /// | `raw < 0.0` | [`MeasureError::NegativeValue`] |
41    pub fn new(raw: f32) -> Result<Self, MeasureError> {
42        if !raw.is_finite() {
43            return Err(MeasureError::NonFiniteValue { value: raw });
44        }
45        if raw < 0.0 {
46            return Err(MeasureError::NegativeValue { value: raw });
47        }
48        Ok(Self(raw))
49    }
50
51    /// Return the underlying `f32` value.
52    pub fn get(self) -> f32 {
53        self.0
54    }
55}
56
57/// Snap ranking priority weight.
58///
59/// Higher values indicate preferred snap targets during outlet resolution.
60/// Typically upstream drainage area in km² or upstream cell count.
61/// Invariant: the wrapped value is always finite and non-negative.
62///
63/// `Eq` is intentionally not derived for the same reason as [`AreaKm2`].
64#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
65pub struct Weight(f32);
66
67impl Weight {
68    /// Construct a [`Weight`] from a raw `f32`.
69    ///
70    /// # Errors
71    ///
72    /// | Condition | Error variant |
73    /// |-----------|---------------|
74    /// | `raw` is NaN or infinite | [`MeasureError::NonFiniteValue`] |
75    /// | `raw < 0.0` | [`MeasureError::NegativeValue`] |
76    pub fn new(raw: f32) -> Result<Self, MeasureError> {
77        if !raw.is_finite() {
78            return Err(MeasureError::NonFiniteValue { value: raw });
79        }
80        if raw < 0.0 {
81            return Err(MeasureError::NegativeValue { value: raw });
82        }
83        Ok(Self(raw))
84    }
85
86    /// Return the underlying `f32` value.
87    pub fn get(self) -> f32 {
88        self.0
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn area_km2_accepts_zero() {
98        let a = AreaKm2::new(0.0).unwrap();
99        assert_eq!(a.get(), 0.0);
100    }
101
102    #[test]
103    fn area_km2_accepts_positive() {
104        let a = AreaKm2::new(123.45).unwrap();
105        assert_eq!(a.get(), 123.45);
106    }
107
108    #[test]
109    fn area_km2_rejects_negative() {
110        assert!(matches!(
111            AreaKm2::new(-1.0),
112            Err(MeasureError::NegativeValue { value: _ })
113        ));
114    }
115
116    #[test]
117    fn area_km2_rejects_nan() {
118        assert!(matches!(
119            AreaKm2::new(f32::NAN),
120            Err(MeasureError::NonFiniteValue { value: _ })
121        ));
122    }
123
124    #[test]
125    fn area_km2_rejects_inf() {
126        assert!(matches!(
127            AreaKm2::new(f32::INFINITY),
128            Err(MeasureError::NonFiniteValue { value: _ })
129        ));
130    }
131
132    #[test]
133    fn area_km2_rejects_neg_inf() {
134        assert!(matches!(
135            AreaKm2::new(f32::NEG_INFINITY),
136            Err(MeasureError::NonFiniteValue { value: _ })
137        ));
138    }
139
140    #[test]
141    fn weight_accepts_zero() {
142        let w = Weight::new(0.0).unwrap();
143        assert_eq!(w.get(), 0.0);
144    }
145
146    #[test]
147    fn weight_accepts_positive() {
148        let w = Weight::new(0.75).unwrap();
149        assert_eq!(w.get(), 0.75);
150    }
151
152    #[test]
153    fn weight_rejects_negative() {
154        assert!(matches!(
155            Weight::new(-0.1),
156            Err(MeasureError::NegativeValue { value: _ })
157        ));
158    }
159
160    #[test]
161    fn weight_rejects_nan() {
162        assert!(matches!(
163            Weight::new(f32::NAN),
164            Err(MeasureError::NonFiniteValue { value: _ })
165        ));
166    }
167
168    #[test]
169    fn weight_rejects_inf() {
170        assert!(matches!(
171            Weight::new(f32::INFINITY),
172            Err(MeasureError::NonFiniteValue { value: _ })
173        ));
174    }
175
176    #[test]
177    fn area_km2_min_positive_succeeds() {
178        let a = AreaKm2::new(f32::MIN_POSITIVE).unwrap();
179        assert_eq!(a.get(), f32::MIN_POSITIVE);
180    }
181
182    #[test]
183    fn weight_neg_infinity_fails_with_non_finite_not_negative() {
184        // Finiteness is checked before the sign check, so NEG_INFINITY must
185        // produce NonFiniteValue, not NegativeValue.
186        assert!(matches!(
187            Weight::new(f32::NEG_INFINITY),
188            Err(MeasureError::NonFiniteValue { value: _ })
189        ));
190    }
191}