Skip to main content

aviso_validators/
float.rs

1// (C) Copyright 2024- ECMWF and individual contributors.
2//
3// This software is licensed under the terms of the Apache Licence Version 2.0
4// which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
5// In applying this licence, ECMWF does not waive the privileges and immunities
6// granted to it by virtue of its status as an intergovernmental organisation nor
7// does it submit to any jurisdiction.
8
9//! Floating-point validation and canonicalization handler.
10
11use anyhow::{Context, Result, bail};
12
13/// Floating-point validation handler.
14pub struct FloatHandler;
15
16impl FloatHandler {
17    /// Validates a floating-point field, applies optional inclusive range checks,
18    /// and returns a canonical string representation.
19    ///
20    /// Valid example: `"12.5"` -> `"12.5"`.
21    /// Invalid example: `"NaN"` -> error.
22    ///
23    /// Canonicalization uses Rust's shortest round-trippable decimal formatting.
24    /// Non-finite values (`NaN`, `inf`, `-inf`) are rejected.
25    pub fn validate_and_canonicalize(
26        value: &str,
27        range: Option<&[f64; 2]>,
28        field_name: &str,
29    ) -> Result<String> {
30        let parsed_value: f64 = value.parse().context(format!(
31            "Field '{}' must be a valid number, got: '{}'",
32            field_name, value
33        ))?;
34
35        if !parsed_value.is_finite() {
36            bail!(
37                "Field '{}' must be a finite number, got: '{}'",
38                field_name,
39                value
40            );
41        }
42
43        if let Some([min, max]) = range {
44            if parsed_value < *min || parsed_value > *max {
45                bail!(
46                    "Field '{}' value {} is outside allowed range [{}, {}]",
47                    field_name,
48                    parsed_value,
49                    min,
50                    max
51                );
52            }
53        }
54
55        Ok(parsed_value.to_string())
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::FloatHandler;
62
63    #[test]
64    fn valid_float_without_range() {
65        let result = FloatHandler::validate_and_canonicalize("12.5", None, "severity");
66        assert!(result.is_ok());
67        assert_eq!(result.expect("value should be valid"), "12.5");
68    }
69
70    #[test]
71    fn valid_float_with_range() {
72        let result = FloatHandler::validate_and_canonicalize("3.4", Some(&[1.0, 7.0]), "level");
73        assert!(result.is_ok());
74    }
75
76    #[test]
77    fn invalid_float_format() {
78        let result = FloatHandler::validate_and_canonicalize("abc", None, "level");
79        assert!(result.is_err());
80    }
81
82    #[test]
83    fn float_outside_range() {
84        let result = FloatHandler::validate_and_canonicalize("9.1", Some(&[1.0, 7.0]), "level");
85        assert!(result.is_err());
86    }
87
88    #[test]
89    fn rejects_nan() {
90        let result = FloatHandler::validate_and_canonicalize("NaN", None, "level");
91        assert!(result.is_err());
92    }
93
94    #[test]
95    fn rejects_positive_infinity() {
96        let result = FloatHandler::validate_and_canonicalize("inf", None, "level");
97        assert!(result.is_err());
98    }
99
100    #[test]
101    fn rejects_negative_infinity() {
102        let result = FloatHandler::validate_and_canonicalize("-inf", None, "level");
103        assert!(result.is_err());
104    }
105}