temp-conv 0.5.5

A simple cli-based unit converter for temperatures
Documentation
//! Data Structure for handling temperatures

use std::str::FromStr;

use clap::ValueEnum;
use serde::{Deserialize, Serialize};

use crate::error::TemperatureParseError;

/// A list of different units of temperature.
#[derive(Clone, Copy, Eq, PartialEq, PartialOrd, Ord, ValueEnum, Debug, Serialize, Deserialize)]
pub enum Temperature {
    /// SI Temperature Unit commonly used nearly all over the world.
    Celsius,
    /// Temperature Unit of the Imperial System.
    Fahrenheit,
    /// SI Temperature Unit commonly used in a scientific context.
    Kelvin,
}

impl Temperature {
    const KELVIN_BASELINE: f64 = 273.15;
    const FAHRENHEIT_COEF: f64 = 5.0 / 9.0;
    const REVERSE_FAHRENHEIT_COEF: f64 = 9.0 / 5.0;
    const FAHRENHEIT_BASELINE: f64 = 32.0;

    /// Converts from one temperature unit to another.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use crate::temperature::Temperature;
    /// let temp_unit = Temperature::Celsius;
    /// let temp = 0.0;
    /// let converted_temp = temp_unit.convert(Temperature::Kelvin, temp);
    ///
    /// assert_eq!(273.15, converted_temp);
    /// ```
    #[inline]
    pub fn convert(&self, target_temperature: Temperature, value: f64) -> f64 {
        match self {
            Temperature::Celsius => match target_temperature {
                Temperature::Celsius => value,
                Temperature::Kelvin => value + Temperature::KELVIN_BASELINE,
                Temperature::Fahrenheit => {
                    value * Temperature::REVERSE_FAHRENHEIT_COEF + Temperature::FAHRENHEIT_BASELINE
                }
            },
            Temperature::Kelvin => match target_temperature {
                Temperature::Celsius => value - Temperature::KELVIN_BASELINE,
                Temperature::Kelvin => value,
                Temperature::Fahrenheit => {
                    (value - Temperature::KELVIN_BASELINE) * Temperature::REVERSE_FAHRENHEIT_COEF
                        + Temperature::FAHRENHEIT_BASELINE
                }
            },
            Temperature::Fahrenheit => match target_temperature {
                Temperature::Celsius => {
                    (value - Temperature::FAHRENHEIT_BASELINE) * Temperature::FAHRENHEIT_COEF
                }
                Temperature::Kelvin => {
                    (value - Temperature::FAHRENHEIT_BASELINE) * Temperature::FAHRENHEIT_COEF
                        + Temperature::KELVIN_BASELINE
                }
                Temperature::Fahrenheit => value,
            },
        }
    }
}

impl FromStr for Temperature {
    type Err = crate::error::TemperatureParseError;

    #[inline]
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "Kelvin" | "kelvin" | "k" => Ok(Temperature::Kelvin),
            "Celsius" | "celsius" | "c" => Ok(Temperature::Celsius),
            "Fahrenheit" | "fahrenheit" | "f" => Ok(Temperature::Fahrenheit),
            _ => Err(TemperatureParseError::InvalidTemperature(s.to_string())),
        }
    }
}

impl std::fmt::Display for Temperature {
    #[inline]
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Temperature::Celsius => write!(f, "Celsius"),
            Temperature::Fahrenheit => write!(f, "Fahrenheit"),
            Temperature::Kelvin => write!(f, "Kelvin"),
        }
    }
}

#[cfg(test)]
mod tests {
    use std::str::FromStr;

    use super::{
        super::error::TemperatureParseError,
        Temperature::{self, *},
    };

    use assert_float_eq::*;
    use pretty_assertions::assert_eq;
    use rstest::rstest;

    #[rstest]
    #[case(Celsius, Kelvin, 0.0, 273.15)]
    #[case(Celsius, Kelvin, 27.0, 300.15)]
    #[case(Celsius, Kelvin, -200.0, 73.15)]
    #[case(Celsius, Kelvin, 9031.99, 9305.14)]
    #[case(Kelvin, Celsius, 273.15, 0.0)]
    #[case(Kelvin, Celsius, 300.15, 27.0)]
    #[case(Kelvin, Celsius, 9031.99, 8758.84)]
    #[case(Kelvin, Celsius, -9031.99, -9305.14)]
    #[case(Fahrenheit, Celsius, 32.0, 0.0)]
    #[case(Fahrenheit, Celsius, 50.0, 10.0)]
    #[case(Fahrenheit, Celsius, 14.0, -10.0)]
    #[case(Fahrenheit, Celsius, -4.0, -20.0)]
    #[case(Celsius, Fahrenheit, 5.0, 41.0)]
    #[case(Celsius, Fahrenheit, -5.0, 23.0)]
    #[case(Celsius, Fahrenheit, 27.0, 80.6)]
    #[case(Celsius, Fahrenheit, 0.0, 32.0)]
    #[case(Fahrenheit, Kelvin, 32.0, 273.15)]
    #[case(Fahrenheit, Kelvin, 50.0, 283.15)]
    #[case(Fahrenheit, Kelvin, 14.0, 263.15)]
    #[case(Fahrenheit, Kelvin, -4.0, 253.15)]
    #[case(Kelvin, Fahrenheit, 0.0, -459.67)]
    #[case(Kelvin, Fahrenheit, 100.0, -279.67)]
    #[case(Kelvin, Fahrenheit, 155.5, -179.77)]
    #[case(Kelvin, Fahrenheit, 2500.0, 4040.33)]
    fn check_conversion(
        #[case] original_temperature: Temperature,
        #[case] target_temperature: Temperature,
        #[case] original_value: f64,
        #[case] expected_value: f64,
    ) {
        let result = original_temperature.convert(target_temperature, original_value);

        assert_f64_near!(result, expected_value);
    }

    #[rstest]
    #[case("Kelvin", Ok(Temperature::Kelvin))]
    #[case("kelvin", Ok(Temperature::Kelvin))]
    #[case("k", Ok(Temperature::Kelvin))]
    #[case("Celsius", Ok(Temperature::Celsius))]
    #[case("celsius", Ok(Temperature::Celsius))]
    #[case("c", Ok(Temperature::Celsius))]
    #[case("Fahrenheit", Ok(Temperature::Fahrenheit))]
    #[case("fahrenheit", Ok(Temperature::Fahrenheit))]
    #[case("f", Ok(Temperature::Fahrenheit))]
    fn check_parsing(
        #[case] input: &str,
        #[case] expected: Result<Temperature, TemperatureParseError>,
    ) {
        assert_eq!(Temperature::from_str(input), expected);
    }
}