1use crate::inline::InlineStr;
16use core::fmt;
17
18pub type Result<T> = core::result::Result<T, KernelError>;
20
21pub const EXCERPT_BYTES: usize = 32;
26
27pub type Excerpt = InlineStr<EXCERPT_BYTES>;
29
30#[derive(Debug, Clone, PartialEq)]
35#[non_exhaustive]
36pub enum KernelError {
37 NotFinite {
39 parameter: &'static str,
41 value: f64,
43 },
44
45 OutOfRange {
47 parameter: &'static str,
49 value: f64,
51 min: f64,
53 max: f64,
55 },
56
57 InsufficientData {
60 found: usize,
62 required: usize,
64 context: &'static str,
66 },
67
68 UnknownCardinalDirection {
70 direction: Excerpt,
72 },
73
74 BufferTooSmall {
79 needed: usize,
81 found: usize,
83 },
84
85 CapacityExceeded {
90 context: &'static str,
92 needed: usize,
94 capacity: usize,
96 },
97
98 SingularSystem {
103 context: &'static str,
105 },
106
107 NotCovariance {
110 context: &'static str,
112 },
113
114 NotConverged {
120 iterations: u32,
122 residual: f64,
124 },
125
126 Parse {
128 what: &'static str,
130 input: Excerpt,
132 },
133
134 Indeterminate {
137 quantity: &'static str,
139 },
140
141 Missing {
144 what: &'static str,
146 },
147
148 Unrepresentable {
151 what: &'static str,
153 },
154
155 TimeReversed {
158 by: core::time::Duration,
160 },
161
162 VerticalDatumMismatch {
164 required: crate::geodesy::VerticalDatum,
166 found: crate::geodesy::VerticalDatum,
168 },
169
170 OutsideValidity {
173 data: &'static str,
175 },
176}
177
178pub fn ensure_finite(parameter: &'static str, value: f64) -> Result<()> {
184 if value.is_finite() {
185 Ok(())
186 } else {
187 Err(KernelError::NotFinite { parameter, value })
188 }
189}
190
191pub fn ensure_range(parameter: &'static str, value: f64, min: f64, max: f64) -> Result<()> {
198 ensure_finite(parameter, value)?;
199 if value < min || value > max {
200 return Err(KernelError::OutOfRange {
201 parameter,
202 value,
203 min,
204 max,
205 });
206 }
207 Ok(())
208}
209
210fn write_interval(f: &mut fmt::Formatter<'_>, min: f64, max: f64) -> fmt::Result {
214 let positive = min > 0.0 && min <= f64::MIN_POSITIVE;
215 let below = min > f64::MIN;
216 let above = max < f64::MAX;
217 match (positive, below, above) {
218 (true, _, true) => write!(f, "greater than 0 and at most {max}"),
219 (true, _, false) => f.write_str("greater than 0"),
220 (false, true, true) => write!(f, "between {min} and {max}"),
221 (false, true, false) => write!(f, "at least {min}"),
222 (false, false, true) => write!(f, "at most {max}"),
223 (false, false, false) => f.write_str("finite"),
224 }
225}
226
227impl fmt::Display for KernelError {
228 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229 match self {
230 Self::NotFinite { parameter, value } => {
231 write!(f, "{parameter} must be a finite number, got {value}")
232 }
233 Self::OutOfRange {
234 parameter,
235 value,
236 min,
237 max,
238 } => {
239 write!(f, "{parameter} out of range: {value}. Must be ")?;
240 write_interval(f, *min, *max)
241 }
242 Self::InsufficientData {
243 found,
244 required,
245 context,
246 } => write!(f, "{context} needs at least {required}, and has {found}"),
247 Self::UnknownCardinalDirection { direction } => write!(
248 f,
249 "unknown cardinal direction: {direction}. Expected one of N, NE, E, SE, S, SW, W, NW"
250 ),
251 Self::CapacityExceeded {
252 context,
253 needed,
254 capacity,
255 } => write!(
256 f,
257 "{context} needs room for {needed}, and the limit is {capacity}"
258 ),
259 Self::BufferTooSmall { needed, found } => write!(
260 f,
261 "output buffer holds {found} values, {needed} are needed"
262 ),
263 Self::SingularSystem { context } => {
264 write!(f, "singular system while solving {context}")
265 }
266 Self::NotCovariance { context } => {
267 write!(f, "{context} is not a covariance matrix")
268 }
269 Self::NotConverged {
270 iterations,
271 residual,
272 } => write!(
273 f,
274 "solver did not converge after {iterations} iterations, residual {residual}"
275 ),
276 Self::Parse { what, input } => {
277 write!(f, "could not read {input:?} as a {what}")
278 }
279 Self::Indeterminate { quantity } => {
280 write!(f, "{quantity} is indeterminate for these inputs")
281 }
282 Self::Missing { what } => write!(f, "{what} is not available"),
283 Self::Unrepresentable { what } => write!(f, "{what} cannot be represented"),
284 Self::TimeReversed { by } => {
285 write!(f, "time ran backwards by {} s", by.as_secs_f64())
286 }
287 Self::VerticalDatumMismatch { required, found } => write!(
288 f,
289 "a height above {found:?} was given where one above {required:?} is required"
290 ),
291 Self::OutsideValidity { data } => {
292 write!(f, "the {data} is not valid for the requested moment")
293 }
294 }
295 }
296}
297
298impl core::error::Error for KernelError {}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use alloc::string::ToString;
306
307 #[test]
308 fn every_variant_has_a_message() {
309 let errors = [
310 KernelError::NotFinite {
311 parameter: "course",
312 value: f64::NAN,
313 },
314 KernelError::OutOfRange {
315 parameter: "course",
316 value: 400.0,
317 min: 0.0,
318 max: 360.0,
319 },
320 KernelError::InsufficientData {
321 found: 1,
322 required: 2,
323 context: "interpolation",
324 },
325 KernelError::UnknownCardinalDirection {
326 direction: Excerpt::new("XYZ"),
327 },
328 KernelError::BufferTooSmall {
329 needed: 36,
330 found: 8,
331 },
332 KernelError::CapacityExceeded {
333 context: "a deviation table",
334 needed: 90,
335 capacity: 72,
336 },
337 KernelError::SingularSystem {
338 context: "parametric fit",
339 },
340 KernelError::NotCovariance {
341 context: "the observation noise",
342 },
343 KernelError::TimeReversed {
344 by: core::time::Duration::from_secs(18),
345 },
346 KernelError::OutsideValidity {
347 data: "leap second table",
348 },
349 KernelError::VerticalDatumMismatch {
350 required: crate::geodesy::VerticalDatum::Ellipsoid,
351 found: crate::geodesy::VerticalDatum::MeanSeaLevel,
352 },
353 KernelError::NotConverged {
354 iterations: 64,
355 residual: 1.0,
356 },
357 KernelError::Parse {
358 what: "latitude",
359 input: Excerpt::new("north-ish"),
360 },
361 KernelError::Indeterminate {
362 quantity: "a rhumb line through a pole",
363 },
364 KernelError::Missing {
365 what: "the vessel's position",
366 },
367 KernelError::Unrepresentable {
368 what: "a moment beyond the end of time",
369 },
370 ];
371 for error in errors {
372 assert!(!error.to_string().is_empty(), "{error:?}");
373 }
374 }
375
376 #[test]
377 fn errors_compare_by_value() {
378 let a = KernelError::OutOfRange {
379 parameter: "x",
380 value: 1.0,
381 min: 0.0,
382 max: 0.5,
383 };
384 assert_eq!(a, a.clone());
385 assert_ne!(
386 a,
387 KernelError::Missing {
388 what: "the vessel's position"
389 }
390 );
391 }
392
393 #[test]
394 fn an_unbounded_side_is_not_printed_as_a_number() {
395 let message = |min, max| {
396 KernelError::OutOfRange {
397 parameter: "x",
398 value: 0.0,
399 min,
400 max,
401 }
402 .to_string()
403 };
404 assert_eq!(
405 message(0.0, 1.0),
406 "x out of range: 0. Must be between 0 and 1"
407 );
408 assert_eq!(
409 message(f64::MIN_POSITIVE, f64::MAX),
410 "x out of range: 0. Must be greater than 0"
411 );
412 assert_eq!(
413 message(f64::MIN_POSITIVE, 1e6),
414 "x out of range: 0. Must be greater than 0 and at most 1000000"
415 );
416 assert_eq!(
417 message(1.0, f64::MAX),
418 "x out of range: 0. Must be at least 1"
419 );
420 assert_eq!(
421 message(f64::MIN, -1.0),
422 "x out of range: 0. Must be at most -1"
423 );
424 }
425
426 #[test]
427 fn the_checks_report_what_they_reject() {
428 assert!(ensure_finite("x", 1.0).is_ok());
429 assert!(matches!(
430 ensure_finite("x", f64::NAN),
431 Err(KernelError::NotFinite { parameter: "x", .. })
432 ));
433 assert!(matches!(
434 ensure_range("x", 2.0, 0.0, 1.0),
435 Err(KernelError::OutOfRange { parameter: "x", .. })
436 ));
437 assert!(matches!(
438 ensure_range("x", f64::INFINITY, 0.0, 1.0),
439 Err(KernelError::NotFinite { .. })
440 ));
441 }
442}