1use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
19pub struct PvRange {
20 pub high: f32,
21 pub low: f32,
22}
23
24impl PvRange {
25 pub fn new(high: f32, low: f32) -> Result<Self, RangeError> {
33 if !high.is_finite() {
34 return Err(RangeError::NotFinite {
35 field: "pv_range_high",
36 value: high,
37 });
38 }
39 if !low.is_finite() {
40 return Err(RangeError::NotFinite {
41 field: "pv_range_low",
42 value: low,
43 });
44 }
45 if high == low {
46 return Err(RangeError::ZeroSpan { high, low });
47 }
48 Ok(PvRange { high, low })
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
56pub struct MvRange {
57 pub high: f32,
58 pub low: f32,
59}
60
61impl MvRange {
62 pub fn new(high: f32, low: f32) -> Result<Self, RangeError> {
68 if !high.is_finite() {
69 return Err(RangeError::NotFinite {
70 field: "mv_range_high",
71 value: high,
72 });
73 }
74 if !low.is_finite() {
75 return Err(RangeError::NotFinite {
76 field: "mv_range_low",
77 value: low,
78 });
79 }
80 if low >= high {
81 return Err(RangeError::LowNotBelowHigh { low, high });
82 }
83 Ok(MvRange { high, low })
84 }
85
86 pub fn contains(&self, value: f32) -> bool {
89 value >= self.low && value <= self.high
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq)]
95pub enum RangeError {
96 NotFinite { field: &'static str, value: f32 },
98 ZeroSpan { high: f32, low: f32 },
100 LowNotBelowHigh { low: f32, high: f32 },
102}
103
104impl std::fmt::Display for RangeError {
105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106 match self {
107 RangeError::NotFinite { field, value } => {
108 write!(f, "{field} value {value} is not a finite number")
109 }
110 RangeError::ZeroSpan { high, low } => write!(
111 f,
112 "range has zero span: high ({high}) and low ({low}) must not be equal"
113 ),
114 RangeError::LowNotBelowHigh { low, high } => write!(
115 f,
116 "range low ({low}) must be strictly less than high ({high})"
117 ),
118 }
119 }
120}
121
122impl std::error::Error for RangeError {}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn pv_range_accepts_a_typical_range() {
130 assert!(PvRange::new(100.0, 0.0).is_ok());
131 }
132
133 #[test]
134 fn pv_range_accepts_a_descending_range() {
135 assert!(PvRange::new(0.0, 100.0).is_ok());
137 }
138
139 #[test]
140 fn pv_range_rejects_zero_span() {
141 assert_eq!(
142 PvRange::new(50.0, 50.0),
143 Err(RangeError::ZeroSpan {
144 high: 50.0,
145 low: 50.0
146 })
147 );
148 }
149
150 #[test]
151 fn pv_range_rejects_non_finite_high() {
152 assert!(matches!(
153 PvRange::new(f32::NAN, 0.0),
154 Err(RangeError::NotFinite {
155 field: "pv_range_high",
156 ..
157 })
158 ));
159 }
160
161 #[test]
162 fn pv_range_rejects_non_finite_low() {
163 assert!(matches!(
164 PvRange::new(100.0, f32::INFINITY),
165 Err(RangeError::NotFinite {
166 field: "pv_range_low",
167 ..
168 })
169 ));
170 }
171
172 #[test]
173 fn mv_range_accepts_a_typical_range() {
174 assert!(MvRange::new(100.0, 0.0).is_ok());
175 }
176
177 #[test]
178 fn mv_range_rejects_equal_bounds() {
179 assert!(matches!(
180 MvRange::new(50.0, 50.0),
181 Err(RangeError::LowNotBelowHigh { .. })
182 ));
183 }
184
185 #[test]
186 fn mv_range_rejects_descending_bounds() {
187 assert!(matches!(
188 MvRange::new(0.0, 100.0),
189 Err(RangeError::LowNotBelowHigh { .. })
190 ));
191 }
192
193 #[test]
194 fn mv_range_rejects_non_finite_high() {
195 assert!(matches!(
196 MvRange::new(f32::NAN, 0.0),
197 Err(RangeError::NotFinite {
198 field: "mv_range_high",
199 ..
200 })
201 ));
202 }
203
204 #[test]
205 fn mv_range_rejects_non_finite_low() {
206 assert!(matches!(
207 MvRange::new(100.0, f32::NAN),
208 Err(RangeError::NotFinite {
209 field: "mv_range_low",
210 ..
211 })
212 ));
213 }
214
215 #[test]
216 fn mv_range_contains_checks_inclusive_bounds() {
217 let range = MvRange::new(100.0, 0.0).unwrap();
218 assert!(range.contains(0.0));
219 assert!(range.contains(100.0));
220 assert!(range.contains(50.0));
221 assert!(!range.contains(-0.01));
222 assert!(!range.contains(100.01));
223 }
224
225 #[test]
226 fn range_error_is_a_std_error() {
227 let err = RangeError::ZeroSpan {
228 high: 1.0,
229 low: 1.0,
230 };
231 let _: Box<dyn std::error::Error> = Box::new(err);
232 }
233
234 #[test]
235 fn range_error_display_names_the_field() {
236 let err = RangeError::NotFinite {
237 field: "pv_range_high",
238 value: f32::NAN,
239 };
240 assert!(err.to_string().contains("pv_range_high"));
241 }
242
243 #[test]
244 fn range_error_display_describes_every_invalid_range_shape() {
245 assert!(
246 RangeError::ZeroSpan {
247 high: 50.0,
248 low: 50.0,
249 }
250 .to_string()
251 .contains("zero span")
252 );
253 assert!(
254 RangeError::LowNotBelowHigh {
255 low: 100.0,
256 high: 0.0,
257 }
258 .to_string()
259 .contains("strictly less")
260 );
261 }
262
263 #[test]
264 fn serde_round_trip() {
265 let range = PvRange {
266 high: 100.0,
267 low: 0.0,
268 };
269 let json = serde_json::to_string(&range).unwrap();
270 let back: PvRange = serde_json::from_str(&json).unwrap();
271 assert_eq!(range, back);
272
273 let range = MvRange {
274 high: 100.0,
275 low: 0.0,
276 };
277 let json = serde_json::to_string(&range).unwrap();
278 let back: MvRange = serde_json::from_str(&json).unwrap();
279 assert_eq!(range, back);
280 }
281}