Skip to main content

use_atmospheric_pressure/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7fn validate_pressure(value: f64) -> Result<f64, PressureValueError> {
8    if !value.is_finite() {
9        return Err(PressureValueError::NonFinitePressure(value));
10    }
11
12    if value < 0.0 {
13        return Err(PressureValueError::NegativePressure(value));
14    }
15
16    Ok(value)
17}
18
19/// Errors returned by pressure value constructors.
20#[derive(Clone, Copy, Debug, PartialEq)]
21pub enum PressureValueError {
22    /// The supplied pressure was `NaN` or infinite.
23    NonFinitePressure(f64),
24    /// The supplied pressure was negative.
25    NegativePressure(f64),
26}
27
28impl fmt::Display for PressureValueError {
29    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
30        match self {
31            Self::NonFinitePressure(value) => {
32                write!(formatter, "pressure must be finite, got {value}")
33            },
34            Self::NegativePressure(value) => {
35                write!(formatter, "pressure cannot be negative, got {value}")
36            },
37        }
38    }
39}
40
41impl Error for PressureValueError {}
42
43/// Simple atmospheric pressure unit labels for the stored `f64` values.
44#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
45pub enum PressureUnitLabel {
46    /// Hectopascals.
47    Hectopascal,
48    /// Millibars.
49    Millibar,
50    /// Unknown unit label.
51    Unknown,
52    /// Caller-defined unit label.
53    Custom(String),
54}
55
56impl fmt::Display for PressureUnitLabel {
57    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
58        match self {
59            Self::Hectopascal => formatter.write_str("hPa"),
60            Self::Millibar => formatter.write_str("mbar"),
61            Self::Unknown => formatter.write_str("unknown"),
62            Self::Custom(value) => formatter.write_str(value),
63        }
64    }
65}
66
67impl FromStr for PressureUnitLabel {
68    type Err = PressureUnitLabelParseError;
69
70    fn from_str(value: &str) -> Result<Self, Self::Err> {
71        let trimmed = value.trim();
72
73        if trimmed.is_empty() {
74            return Err(PressureUnitLabelParseError::Empty);
75        }
76
77        match trimmed.to_ascii_lowercase().as_str() {
78            "hpa" | "hectopascal" | "hectopascals" => Ok(Self::Hectopascal),
79            "mbar" | "millibar" | "millibars" => Ok(Self::Millibar),
80            "unknown" => Ok(Self::Unknown),
81            _ => Ok(Self::Custom(trimmed.to_string())),
82        }
83    }
84}
85
86/// Error returned when parsing pressure unit labels fails.
87#[derive(Clone, Copy, Debug, Eq, PartialEq)]
88pub enum PressureUnitLabelParseError {
89    /// The pressure unit label was empty after trimming whitespace.
90    Empty,
91}
92
93impl fmt::Display for PressureUnitLabelParseError {
94    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
95        match self {
96            Self::Empty => formatter.write_str("pressure unit label cannot be empty"),
97        }
98    }
99}
100
101impl Error for PressureUnitLabelParseError {}
102
103/// Stable atmospheric pressure tendency vocabulary.
104#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
105pub enum PressureTendency {
106    /// Rising pressure.
107    Rising,
108    /// Falling pressure.
109    Falling,
110    /// Steady pressure.
111    Steady,
112    /// Unknown tendency.
113    Unknown,
114    /// Caller-defined tendency text.
115    Custom(String),
116}
117
118impl fmt::Display for PressureTendency {
119    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
120        match self {
121            Self::Rising => formatter.write_str("rising"),
122            Self::Falling => formatter.write_str("falling"),
123            Self::Steady => formatter.write_str("steady"),
124            Self::Unknown => formatter.write_str("unknown"),
125            Self::Custom(value) => formatter.write_str(value),
126        }
127    }
128}
129
130impl FromStr for PressureTendency {
131    type Err = PressureTendencyParseError;
132
133    fn from_str(value: &str) -> Result<Self, Self::Err> {
134        let trimmed = value.trim();
135
136        if trimmed.is_empty() {
137            return Err(PressureTendencyParseError::Empty);
138        }
139
140        match trimmed
141            .to_ascii_lowercase()
142            .replace(['_', ' '], "-")
143            .as_str()
144        {
145            "rising" => Ok(Self::Rising),
146            "falling" => Ok(Self::Falling),
147            "steady" => Ok(Self::Steady),
148            "unknown" => Ok(Self::Unknown),
149            _ => Ok(Self::Custom(trimmed.to_string())),
150        }
151    }
152}
153
154/// Error returned when parsing pressure tendencies fails.
155#[derive(Clone, Copy, Debug, Eq, PartialEq)]
156pub enum PressureTendencyParseError {
157    /// The pressure tendency was empty after trimming whitespace.
158    Empty,
159}
160
161impl fmt::Display for PressureTendencyParseError {
162    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
163        match self {
164            Self::Empty => formatter.write_str("pressure tendency cannot be empty"),
165        }
166    }
167}
168
169impl Error for PressureTendencyParseError {}
170
171/// Atmospheric pressure stored in hPa or mbar.
172#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
173pub struct AtmosphericPressure(f64);
174
175impl AtmosphericPressure {
176    /// Creates atmospheric pressure from a finite non-negative hPa value.
177    ///
178    /// # Errors
179    ///
180    /// Returns [`PressureValueError`] when the pressure is invalid.
181    pub fn new(hectopascals: f64) -> Result<Self, PressureValueError> {
182        validate_pressure(hectopascals).map(Self)
183    }
184
185    /// Returns the stored pressure in hPa.
186    #[must_use]
187    pub fn hectopascals(&self) -> f64 {
188        self.0
189    }
190
191    /// Returns the unit label for the stored convention.
192    #[must_use]
193    pub fn unit_label(&self) -> PressureUnitLabel {
194        PressureUnitLabel::Hectopascal
195    }
196}
197
198/// Sea-level pressure stored in hPa or mbar.
199#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
200pub struct SeaLevelPressure(f64);
201
202impl SeaLevelPressure {
203    /// Creates sea-level pressure from a finite non-negative hPa value.
204    ///
205    /// # Errors
206    ///
207    /// Returns [`PressureValueError`] when the pressure is invalid.
208    pub fn new(hectopascals: f64) -> Result<Self, PressureValueError> {
209        validate_pressure(hectopascals).map(Self)
210    }
211
212    /// Returns the stored pressure in hPa.
213    #[must_use]
214    pub fn hectopascals(&self) -> f64 {
215        self.0
216    }
217
218    /// Returns the unit label for the stored convention.
219    #[must_use]
220    pub fn unit_label(&self) -> PressureUnitLabel {
221        PressureUnitLabel::Hectopascal
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::{
228        AtmosphericPressure, PressureTendency, PressureTendencyParseError, PressureValueError,
229        SeaLevelPressure,
230    };
231    use core::str::FromStr;
232
233    #[test]
234    fn valid_pressure() {
235        let pressure = AtmosphericPressure::new(1008.5).unwrap();
236
237        assert_eq!(pressure.hectopascals(), 1008.5);
238    }
239
240    #[test]
241    fn negative_pressure_rejected() {
242        assert_eq!(
243            AtmosphericPressure::new(-1.0),
244            Err(PressureValueError::NegativePressure(-1.0))
245        );
246    }
247
248    #[test]
249    fn sea_level_pressure_construction() {
250        let pressure = SeaLevelPressure::new(1016.3).unwrap();
251
252        assert_eq!(pressure.hectopascals(), 1016.3);
253    }
254
255    #[test]
256    fn tendency_display_and_parse() {
257        assert_eq!(PressureTendency::Steady.to_string(), "steady");
258        assert_eq!(
259            PressureTendency::from_str("falling").unwrap(),
260            PressureTendency::Falling
261        );
262        assert_eq!(
263            PressureTendency::from_str(" "),
264            Err(PressureTendencyParseError::Empty)
265        );
266    }
267
268    #[test]
269    fn custom_tendency() {
270        assert_eq!(
271            PressureTendency::from_str("rapid rise").unwrap(),
272            PressureTendency::Custom(String::from("rapid rise"))
273        );
274    }
275}