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