use_air_temperature/
lib.rs1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7fn validate_celsius(value: f64) -> Result<f64, TemperatureValueError> {
8 if !value.is_finite() {
9 Err(TemperatureValueError::NonFiniteCelsius(value))
10 } else {
11 Ok(value)
12 }
13}
14
15#[derive(Clone, Copy, Debug, PartialEq)]
17pub enum TemperatureValueError {
18 NonFiniteCelsius(f64),
20}
21
22impl fmt::Display for TemperatureValueError {
23 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
24 match self {
25 Self::NonFiniteCelsius(value) => {
26 write!(formatter, "temperature value must be finite, got {value}")
27 },
28 }
29 }
30}
31
32impl Error for TemperatureValueError {}
33
34#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
36pub enum TemperatureKind {
37 Air,
39 DewPoint,
41 WetBulb,
43 HeatIndex,
45 WindChill,
47 Apparent,
49 Unknown,
51 Custom(String),
53}
54
55impl fmt::Display for TemperatureKind {
56 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
57 match self {
58 Self::Air => formatter.write_str("air"),
59 Self::DewPoint => formatter.write_str("dew-point"),
60 Self::WetBulb => formatter.write_str("wet-bulb"),
61 Self::HeatIndex => formatter.write_str("heat-index"),
62 Self::WindChill => formatter.write_str("wind-chill"),
63 Self::Apparent => formatter.write_str("apparent"),
64 Self::Unknown => formatter.write_str("unknown"),
65 Self::Custom(value) => formatter.write_str(value),
66 }
67 }
68}
69
70impl FromStr for TemperatureKind {
71 type Err = TemperatureKindParseError;
72
73 fn from_str(value: &str) -> Result<Self, Self::Err> {
74 let trimmed = value.trim();
75
76 if trimmed.is_empty() {
77 return Err(TemperatureKindParseError::Empty);
78 }
79
80 match trimmed
81 .to_ascii_lowercase()
82 .replace(['_', ' '], "-")
83 .as_str()
84 {
85 "air" => Ok(Self::Air),
86 "dew-point" | "dewpoint" => Ok(Self::DewPoint),
87 "wet-bulb" | "wetbulb" => Ok(Self::WetBulb),
88 "heat-index" | "heatindex" => Ok(Self::HeatIndex),
89 "wind-chill" | "windchill" => Ok(Self::WindChill),
90 "apparent" => Ok(Self::Apparent),
91 "unknown" => Ok(Self::Unknown),
92 _ => Ok(Self::Custom(trimmed.to_string())),
93 }
94 }
95}
96
97#[derive(Clone, Copy, Debug, Eq, PartialEq)]
99pub enum TemperatureKindParseError {
100 Empty,
102}
103
104impl fmt::Display for TemperatureKindParseError {
105 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
106 match self {
107 Self::Empty => formatter.write_str("temperature kind cannot be empty"),
108 }
109 }
110}
111
112impl Error for TemperatureKindParseError {}
113
114#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
116pub struct AirTemperature(f64);
117
118impl AirTemperature {
119 pub fn new(celsius: f64) -> Result<Self, TemperatureValueError> {
125 validate_celsius(celsius).map(Self)
126 }
127
128 #[must_use]
130 pub fn celsius(&self) -> f64 {
131 self.0
132 }
133}
134
135#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
137pub struct DewPoint(f64);
138
139impl DewPoint {
140 pub fn new(celsius: f64) -> Result<Self, TemperatureValueError> {
146 validate_celsius(celsius).map(Self)
147 }
148
149 #[must_use]
151 pub fn celsius(&self) -> f64 {
152 self.0
153 }
154}
155
156#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
158pub struct HeatIndex(f64);
159
160impl HeatIndex {
161 pub fn new(celsius: f64) -> Result<Self, TemperatureValueError> {
167 validate_celsius(celsius).map(Self)
168 }
169
170 #[must_use]
172 pub fn celsius(&self) -> f64 {
173 self.0
174 }
175}
176
177#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
179pub struct WindChill(f64);
180
181impl WindChill {
182 pub fn new(celsius: f64) -> Result<Self, TemperatureValueError> {
188 validate_celsius(celsius).map(Self)
189 }
190
191 #[must_use]
193 pub fn celsius(&self) -> f64 {
194 self.0
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::{
201 AirTemperature, DewPoint, TemperatureKind, TemperatureKindParseError, TemperatureValueError,
202 };
203 use core::str::FromStr;
204
205 #[test]
206 fn valid_positive_temperature() {
207 let value = AirTemperature::new(24.5).unwrap();
208
209 assert_eq!(value.celsius(), 24.5);
210 }
211
212 #[test]
213 fn valid_negative_temperature() {
214 let value = AirTemperature::new(-18.0).unwrap();
215
216 assert_eq!(value.celsius(), -18.0);
217 }
218
219 #[test]
220 fn dew_point_construction() {
221 let value = DewPoint::new(9.25).unwrap();
222
223 assert_eq!(value.celsius(), 9.25);
224 }
225
226 #[test]
227 fn temperature_kind_display_and_parse() {
228 assert_eq!(TemperatureKind::WindChill.to_string(), "wind-chill");
229 assert_eq!(
230 TemperatureKind::from_str("dew point").unwrap(),
231 TemperatureKind::DewPoint
232 );
233 assert_eq!(
234 TemperatureKind::from_str(" "),
235 Err(TemperatureKindParseError::Empty)
236 );
237 }
238
239 #[test]
240 fn custom_temperature_kind() {
241 assert_eq!(
242 TemperatureKind::from_str("frost point").unwrap(),
243 TemperatureKind::Custom(String::from("frost point"))
244 );
245 }
246
247 #[test]
248 fn rejects_non_finite_temperature() {
249 assert_eq!(
250 AirTemperature::new(f64::INFINITY),
251 Err(TemperatureValueError::NonFiniteCelsius(f64::INFINITY))
252 );
253 }
254}