temperature-convert 1.1.0

Temperature unit converter between Celsius, Fahrenheit and Kelvin.
Documentation
#![no_std]
//! Temperature conversions between Kelvin, Celsius and Fahrenheit.
//!
//! Conversions are provided per float type in their own module e.g. [`f64::kelvin_to_celsius`] and
//! [`f32::kelvin_to_celsius`]. The free functions at the crate root are
//! aliases for the `f64` module for backwards compatibility.

#[cfg(feature = "std")]
extern crate std;

macro_rules! impl_temperature_conversions {
    ($ty:ty, $mod_name:ident) => {
        #[doc = concat!("Temperature conversions for [`", stringify!($ty), "`].")]
        pub mod $mod_name {
            #[cfg(feature = "std")]
            #[inline]
            const fn fma(a: $ty, b: $ty, c: $ty) -> $ty {
                <$ty>::mul_add(a, b, c)
            }

            #[cfg(not(feature = "std"))]
            #[inline]
            fn fma(a: $ty, b: $ty, c: $ty) -> $ty {
                use num_traits::real::Real;
                Real::mul_add(a, b, c)
            }

            #[must_use]
            pub const fn kelvin_to_celsius(val: $ty) -> $ty {
                val - 273.15
            }

            #[must_use]
            pub fn kelvin_to_fahrenheit(val: $ty) -> $ty {
                fma(val - 273.15, 9. / 5., 32.)
            }

            #[must_use]
            pub const fn celsius_to_kelvin(val: $ty) -> $ty {
                val + 273.15
            }

            #[must_use]
            pub fn celsius_to_fahrenheit(val: $ty) -> $ty {
                fma(val, 9. / 5., 32.)
            }

            #[must_use]
            pub fn fahrenheit_to_kelvin(val: $ty) -> $ty {
                fma(val - 32., 5. / 9., 273.15)
            }

            #[must_use]
            pub const fn fahrenheit_to_celsius(val: $ty) -> $ty {
                (val - 32.) * (5. / 9.)
            }
        }
    };
}

impl_temperature_conversions!(f64, f64);
impl_temperature_conversions!(f32, f32);

// Keep the pre-existing top-level API (defaults to f64) working unchanged.
pub use crate::f64::*;

#[cfg(test)]
mod temperature_tests {
    use super::*;
    use rstest::rstest;

    const PRECISION: f64 = f64::EPSILON;

    /// Helper function to compare f64 values within precision
    fn approx_equal(x: f64, y: f64) -> bool {
        (x - y).abs() < PRECISION
    }

    #[rstest]
    #[case(0.0, -273.15)]
    #[case(100.0, -173.14999999999998)]
    #[case(1000.0, 726.85)]
    #[case(5000.0, 4726.85)]
    fn test_kelvin_to_celsius_precision(#[case] kelvin_val: f64, #[case] celsius_val: f64) {
        let converted = kelvin_to_celsius(kelvin_val);
        assert!(
            approx_equal(converted, celsius_val),
            "High precision K->C failed - Expected: {celsius_val}, Got: {converted}"
        );
    }

    #[rstest]
    #[case(0.0, -459.66999999999996)]
    #[case(100.0, -279.66999999999996)]
    #[case(1000.0, 1340.3300000000002)]
    #[case(5000.0, 8540.330000000002)]
    #[case(69420.0, 124496.33000000002)]
    fn test_kelvin_to_fahrenheit_precision(#[case] kelvin_val: f64, #[case] fahrenheit_val: f64) {
        let converted = kelvin_to_fahrenheit(kelvin_val);
        assert!(
            approx_equal(converted, fahrenheit_val),
            "High precision K->F failed - Expected: {fahrenheit_val}, Got: {converted}"
        );
    }

    #[rstest]
    #[case(0.0, 273.15)]
    #[case(100.0, 373.15)]
    #[case(1000.0, 1273.15)]
    #[case(5000.0, 5273.15)]
    #[case(69420.0, 69693.15)]
    fn test_celsius_to_kelvin_precision(#[case] celsius_val: f64, #[case] kelvin_val: f64) {
        let converted = celsius_to_kelvin(celsius_val);
        assert!(
            approx_equal(converted, kelvin_val),
            "High precision C->K failed - Expected: {kelvin_val}, Got: {converted}"
        );
    }

    #[rstest]
    #[case(0.0, 32.0)]
    #[case(100.0, 212.0)]
    #[case(1000.0, 1832.0)]
    #[case(5000.0, 9032.0)]
    #[case(69420.0, 124988.0)]
    fn test_celsius_to_fahrenheit_precision(#[case] celsius_val: f64, #[case] fahrenheit_val: f64) {
        let converted = celsius_to_fahrenheit(celsius_val);
        assert!(
            approx_equal(converted, fahrenheit_val),
            "High precision C->F failed - Expected: {fahrenheit_val}, Got: {converted}"
        );
    }

    #[rstest]
    #[case(0.0, 255.3722222222222)]
    #[case(100.0, 310.92777777777775)]
    #[case(1000.0, 810.9277777777778)]
    #[case(5000.0, 3033.15)]
    #[case(69420.0, 38822.03888888889)]
    fn test_fahrenheit_to_kelvin_precision(#[case] fahrenheit_val: f64, #[case] kelvin_val: f64) {
        let converted = fahrenheit_to_kelvin(fahrenheit_val);
        assert!(
            approx_equal(converted, kelvin_val),
            "High precision F->K failed - Expected: {kelvin_val}, Got: {converted}"
        );
    }

    #[rstest]
    #[case(0.0, -17.77777777777778)]
    #[case(100.0, 37.77777777777778)]
    #[case(1000.0, 537.7777777777778)]
    #[case(5000.0, 2760.0)]
    #[case(69420.0, 38548.88888888889)]
    fn test_fahrenheit_to_celsius_precision(#[case] fahrenheit_val: f64, #[case] celsius_val: f64) {
        let converted = fahrenheit_to_celsius(fahrenheit_val);
        assert!(
            approx_equal(converted, celsius_val),
            "High precision F->C failed - Expected: {celsius_val}, Got: {converted}"
        );
    }
}

#[cfg(test)]
mod round_trip_tests {
    macro_rules! round_trip_tests {
        ($ty:ty, $mod_name:ident) => {
            mod $mod_name {
                use crate::$mod_name;

                fn approx_eq(a: $ty, b: $ty) -> bool {
                    const INTRINSIC_SCALE: $ty = 100.; //10.2 lowest margin, 10.1 fails
                    let scale = a.abs().max(b.abs()).max(INTRINSIC_SCALE);
                    (a - b).abs() < <$ty>::EPSILON * scale * 8.0
                }

                const SAMPLES: [$ty; 5] = [0.0, 100.0, 273.15, 1000.0, 5000.0];

                #[test]
                fn kelvin_celsius_round_trip() {
                    for &k in &SAMPLES {
                        let back = $mod_name::celsius_to_kelvin($mod_name::kelvin_to_celsius(k));
                        assert!(approx_eq(back, k), "K->C->K failed for {k}: got {back}");
                    }
                }

                #[test]
                fn celsius_fahrenheit_round_trip() {
                    for &c in &SAMPLES {
                        let back =
                            $mod_name::fahrenheit_to_celsius($mod_name::celsius_to_fahrenheit(c));
                        assert!(approx_eq(back, c), "C->F->C failed for {c}: got {back}");
                    }
                }

                #[test]
                fn kelvin_fahrenheit_round_trip() {
                    for &k in &SAMPLES {
                        let back =
                            $mod_name::fahrenheit_to_kelvin($mod_name::kelvin_to_fahrenheit(k));
                        assert!(approx_eq(back, k), "K->F->K failed for {k}: got {back}");
                    }
                }
            }
        };
    }

    round_trip_tests!(f64, f64);
    round_trip_tests!(f32, f32);
}