temp_conv/
temperature.rs

1//! Data Structure for handling temperatures
2
3use std::str::FromStr;
4
5use clap::ValueEnum;
6use serde::{Deserialize, Serialize};
7
8use crate::error::TemperatureParseError;
9
10/// A list of different units of temperature.
11#[derive(Clone, Copy, Eq, PartialEq, PartialOrd, Ord, ValueEnum, Debug, Serialize, Deserialize)]
12pub enum Temperature {
13    /// SI Temperature Unit commonly used nearly all over the world.
14    Celsius,
15    /// Temperature Unit of the Imperial System.
16    Fahrenheit,
17    /// SI Temperature Unit commonly used in a scientific context.
18    Kelvin,
19}
20
21impl Temperature {
22    const KELVIN_BASELINE: f64 = 273.15;
23    const FAHRENHEIT_COEF: f64 = 5.0 / 9.0;
24    const REVERSE_FAHRENHEIT_COEF: f64 = 9.0 / 5.0;
25    const FAHRENHEIT_BASELINE: f64 = 32.0;
26
27    /// Converts from one temperature unit to another.
28    ///
29    /// # Examples
30    ///
31    /// ```rust
32    /// use crate::temperature::Temperature;
33    /// let temp_unit = Temperature::Celsius;
34    /// let temp = 0.0;
35    /// let converted_temp = temp_unit.convert(Temperature::Kelvin, temp);
36    ///
37    /// assert_eq!(273.15, converted_temp);
38    /// ```
39    #[inline]
40    pub fn convert(&self, target_temperature: Temperature, value: f64) -> f64 {
41        match self {
42            Temperature::Celsius => match target_temperature {
43                Temperature::Celsius => value,
44                Temperature::Kelvin => value + Temperature::KELVIN_BASELINE,
45                Temperature::Fahrenheit => {
46                    value * Temperature::REVERSE_FAHRENHEIT_COEF + Temperature::FAHRENHEIT_BASELINE
47                }
48            },
49            Temperature::Kelvin => match target_temperature {
50                Temperature::Celsius => value - Temperature::KELVIN_BASELINE,
51                Temperature::Kelvin => value,
52                Temperature::Fahrenheit => {
53                    (value - Temperature::KELVIN_BASELINE) * Temperature::REVERSE_FAHRENHEIT_COEF
54                        + Temperature::FAHRENHEIT_BASELINE
55                }
56            },
57            Temperature::Fahrenheit => match target_temperature {
58                Temperature::Celsius => {
59                    (value - Temperature::FAHRENHEIT_BASELINE) * Temperature::FAHRENHEIT_COEF
60                }
61                Temperature::Kelvin => {
62                    (value - Temperature::FAHRENHEIT_BASELINE) * Temperature::FAHRENHEIT_COEF
63                        + Temperature::KELVIN_BASELINE
64                }
65                Temperature::Fahrenheit => value,
66            },
67        }
68    }
69}
70
71impl FromStr for Temperature {
72    type Err = crate::error::TemperatureParseError;
73
74    #[inline]
75    fn from_str(s: &str) -> Result<Self, Self::Err> {
76        match s {
77            "Kelvin" | "kelvin" | "k" => Ok(Temperature::Kelvin),
78            "Celsius" | "celsius" | "c" => Ok(Temperature::Celsius),
79            "Fahrenheit" | "fahrenheit" | "f" => Ok(Temperature::Fahrenheit),
80            _ => Err(TemperatureParseError::InvalidTemperature(s.to_string())),
81        }
82    }
83}
84
85impl std::fmt::Display for Temperature {
86    #[inline]
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        match self {
89            Temperature::Celsius => write!(f, "Celsius"),
90            Temperature::Fahrenheit => write!(f, "Fahrenheit"),
91            Temperature::Kelvin => write!(f, "Kelvin"),
92        }
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use std::str::FromStr;
99
100    use super::{
101        super::error::TemperatureParseError,
102        Temperature::{self, *},
103    };
104
105    use assert_float_eq::*;
106    use pretty_assertions::assert_eq;
107    use rstest::rstest;
108
109    #[rstest]
110    #[case(Celsius, Kelvin, 0.0, 273.15)]
111    #[case(Celsius, Kelvin, 27.0, 300.15)]
112    #[case(Celsius, Kelvin, -200.0, 73.15)]
113    #[case(Celsius, Kelvin, 9031.99, 9305.14)]
114    #[case(Kelvin, Celsius, 273.15, 0.0)]
115    #[case(Kelvin, Celsius, 300.15, 27.0)]
116    #[case(Kelvin, Celsius, 9031.99, 8758.84)]
117    #[case(Kelvin, Celsius, -9031.99, -9305.14)]
118    #[case(Fahrenheit, Celsius, 32.0, 0.0)]
119    #[case(Fahrenheit, Celsius, 50.0, 10.0)]
120    #[case(Fahrenheit, Celsius, 14.0, -10.0)]
121    #[case(Fahrenheit, Celsius, -4.0, -20.0)]
122    #[case(Celsius, Fahrenheit, 5.0, 41.0)]
123    #[case(Celsius, Fahrenheit, -5.0, 23.0)]
124    #[case(Celsius, Fahrenheit, 27.0, 80.6)]
125    #[case(Celsius, Fahrenheit, 0.0, 32.0)]
126    #[case(Fahrenheit, Kelvin, 32.0, 273.15)]
127    #[case(Fahrenheit, Kelvin, 50.0, 283.15)]
128    #[case(Fahrenheit, Kelvin, 14.0, 263.15)]
129    #[case(Fahrenheit, Kelvin, -4.0, 253.15)]
130    #[case(Kelvin, Fahrenheit, 0.0, -459.67)]
131    #[case(Kelvin, Fahrenheit, 100.0, -279.67)]
132    #[case(Kelvin, Fahrenheit, 155.5, -179.77)]
133    #[case(Kelvin, Fahrenheit, 2500.0, 4040.33)]
134    fn check_conversion(
135        #[case] original_temperature: Temperature,
136        #[case] target_temperature: Temperature,
137        #[case] original_value: f64,
138        #[case] expected_value: f64,
139    ) {
140        let result = original_temperature.convert(target_temperature, original_value);
141
142        assert_f64_near!(result, expected_value);
143    }
144
145    #[rstest]
146    #[case("Kelvin", Ok(Temperature::Kelvin))]
147    #[case("kelvin", Ok(Temperature::Kelvin))]
148    #[case("k", Ok(Temperature::Kelvin))]
149    #[case("Celsius", Ok(Temperature::Celsius))]
150    #[case("celsius", Ok(Temperature::Celsius))]
151    #[case("c", Ok(Temperature::Celsius))]
152    #[case("Fahrenheit", Ok(Temperature::Fahrenheit))]
153    #[case("fahrenheit", Ok(Temperature::Fahrenheit))]
154    #[case("f", Ok(Temperature::Fahrenheit))]
155    fn check_parsing(
156        #[case] input: &str,
157        #[case] expected: Result<Temperature, TemperatureParseError>,
158    ) {
159        assert_eq!(Temperature::from_str(input), expected);
160    }
161}