Skip to main content

temperature_convert/
lib.rs

1#![no_std]
2//! Temperature conversions between Kelvin, Celsius and Fahrenheit.
3//!
4//! Conversions are provided per float type in their own module e.g. [`f64::kelvin_to_celsius`] and
5//! [`f32::kelvin_to_celsius`]. The free functions at the crate root are
6//! aliases for the `f64` module for backwards compatibility.
7
8#[cfg(feature = "std")]
9extern crate std;
10
11macro_rules! impl_temperature_conversions {
12    ($ty:ty, $mod_name:ident) => {
13        #[doc = concat!("Temperature conversions for [`", stringify!($ty), "`].")]
14        pub mod $mod_name {
15            #[cfg(feature = "std")]
16            #[inline]
17            const fn fma(a: $ty, b: $ty, c: $ty) -> $ty {
18                <$ty>::mul_add(a, b, c)
19            }
20
21            #[cfg(not(feature = "std"))]
22            #[inline]
23            fn fma(a: $ty, b: $ty, c: $ty) -> $ty {
24                use num_traits::real::Real;
25                Real::mul_add(a, b, c)
26            }
27
28            #[must_use]
29            pub const fn kelvin_to_celsius(val: $ty) -> $ty {
30                val - 273.15
31            }
32
33            #[must_use]
34            pub fn kelvin_to_fahrenheit(val: $ty) -> $ty {
35                fma(val - 273.15, 9. / 5., 32.)
36            }
37
38            #[must_use]
39            pub const fn celsius_to_kelvin(val: $ty) -> $ty {
40                val + 273.15
41            }
42
43            #[must_use]
44            pub fn celsius_to_fahrenheit(val: $ty) -> $ty {
45                fma(val, 9. / 5., 32.)
46            }
47
48            #[must_use]
49            pub fn fahrenheit_to_kelvin(val: $ty) -> $ty {
50                fma(val - 32., 5. / 9., 273.15)
51            }
52
53            #[must_use]
54            pub const fn fahrenheit_to_celsius(val: $ty) -> $ty {
55                (val - 32.) * (5. / 9.)
56            }
57        }
58    };
59}
60
61impl_temperature_conversions!(f64, f64);
62impl_temperature_conversions!(f32, f32);
63
64// Keep the pre-existing top-level API (defaults to f64) working unchanged.
65pub use crate::f64::*;
66
67#[cfg(test)]
68mod temperature_tests {
69    use super::*;
70    use rstest::rstest;
71
72    const PRECISION: f64 = f64::EPSILON;
73
74    /// Helper function to compare f64 values within precision
75    fn approx_equal(x: f64, y: f64) -> bool {
76        (x - y).abs() < PRECISION
77    }
78
79    #[rstest]
80    #[case(0.0, -273.15)]
81    #[case(100.0, -173.14999999999998)]
82    #[case(1000.0, 726.85)]
83    #[case(5000.0, 4726.85)]
84    fn test_kelvin_to_celsius_precision(#[case] kelvin_val: f64, #[case] celsius_val: f64) {
85        let converted = kelvin_to_celsius(kelvin_val);
86        assert!(
87            approx_equal(converted, celsius_val),
88            "High precision K->C failed - Expected: {celsius_val}, Got: {converted}"
89        );
90    }
91
92    #[rstest]
93    #[case(0.0, -459.66999999999996)]
94    #[case(100.0, -279.66999999999996)]
95    #[case(1000.0, 1340.3300000000002)]
96    #[case(5000.0, 8540.330000000002)]
97    #[case(69420.0, 124496.33000000002)]
98    fn test_kelvin_to_fahrenheit_precision(#[case] kelvin_val: f64, #[case] fahrenheit_val: f64) {
99        let converted = kelvin_to_fahrenheit(kelvin_val);
100        assert!(
101            approx_equal(converted, fahrenheit_val),
102            "High precision K->F failed - Expected: {fahrenheit_val}, Got: {converted}"
103        );
104    }
105
106    #[rstest]
107    #[case(0.0, 273.15)]
108    #[case(100.0, 373.15)]
109    #[case(1000.0, 1273.15)]
110    #[case(5000.0, 5273.15)]
111    #[case(69420.0, 69693.15)]
112    fn test_celsius_to_kelvin_precision(#[case] celsius_val: f64, #[case] kelvin_val: f64) {
113        let converted = celsius_to_kelvin(celsius_val);
114        assert!(
115            approx_equal(converted, kelvin_val),
116            "High precision C->K failed - Expected: {kelvin_val}, Got: {converted}"
117        );
118    }
119
120    #[rstest]
121    #[case(0.0, 32.0)]
122    #[case(100.0, 212.0)]
123    #[case(1000.0, 1832.0)]
124    #[case(5000.0, 9032.0)]
125    #[case(69420.0, 124988.0)]
126    fn test_celsius_to_fahrenheit_precision(#[case] celsius_val: f64, #[case] fahrenheit_val: f64) {
127        let converted = celsius_to_fahrenheit(celsius_val);
128        assert!(
129            approx_equal(converted, fahrenheit_val),
130            "High precision C->F failed - Expected: {fahrenheit_val}, Got: {converted}"
131        );
132    }
133
134    #[rstest]
135    #[case(0.0, 255.3722222222222)]
136    #[case(100.0, 310.92777777777775)]
137    #[case(1000.0, 810.9277777777778)]
138    #[case(5000.0, 3033.15)]
139    #[case(69420.0, 38822.03888888889)]
140    fn test_fahrenheit_to_kelvin_precision(#[case] fahrenheit_val: f64, #[case] kelvin_val: f64) {
141        let converted = fahrenheit_to_kelvin(fahrenheit_val);
142        assert!(
143            approx_equal(converted, kelvin_val),
144            "High precision F->K failed - Expected: {kelvin_val}, Got: {converted}"
145        );
146    }
147
148    #[rstest]
149    #[case(0.0, -17.77777777777778)]
150    #[case(100.0, 37.77777777777778)]
151    #[case(1000.0, 537.7777777777778)]
152    #[case(5000.0, 2760.0)]
153    #[case(69420.0, 38548.88888888889)]
154    fn test_fahrenheit_to_celsius_precision(#[case] fahrenheit_val: f64, #[case] celsius_val: f64) {
155        let converted = fahrenheit_to_celsius(fahrenheit_val);
156        assert!(
157            approx_equal(converted, celsius_val),
158            "High precision F->C failed - Expected: {celsius_val}, Got: {converted}"
159        );
160    }
161}
162
163#[cfg(test)]
164mod round_trip_tests {
165    macro_rules! round_trip_tests {
166        ($ty:ty, $mod_name:ident) => {
167            mod $mod_name {
168                use crate::$mod_name;
169
170                fn approx_eq(a: $ty, b: $ty) -> bool {
171                    const INTRINSIC_SCALE: $ty = 100.; //10.2 lowest margin, 10.1 fails
172                    let scale = a.abs().max(b.abs()).max(INTRINSIC_SCALE);
173                    (a - b).abs() < <$ty>::EPSILON * scale * 8.0
174                }
175
176                const SAMPLES: [$ty; 5] = [0.0, 100.0, 273.15, 1000.0, 5000.0];
177
178                #[test]
179                fn kelvin_celsius_round_trip() {
180                    for &k in &SAMPLES {
181                        let back = $mod_name::celsius_to_kelvin($mod_name::kelvin_to_celsius(k));
182                        assert!(approx_eq(back, k), "K->C->K failed for {k}: got {back}");
183                    }
184                }
185
186                #[test]
187                fn celsius_fahrenheit_round_trip() {
188                    for &c in &SAMPLES {
189                        let back =
190                            $mod_name::fahrenheit_to_celsius($mod_name::celsius_to_fahrenheit(c));
191                        assert!(approx_eq(back, c), "C->F->C failed for {c}: got {back}");
192                    }
193                }
194
195                #[test]
196                fn kelvin_fahrenheit_round_trip() {
197                    for &k in &SAMPLES {
198                        let back =
199                            $mod_name::fahrenheit_to_kelvin($mod_name::kelvin_to_fahrenheit(k));
200                        assert!(approx_eq(back, k), "K->F->K failed for {k}: got {back}");
201                    }
202                }
203            }
204        };
205    }
206
207    round_trip_tests!(f64, f64);
208    round_trip_tests!(f32, f32);
209}