158 lines
4.8 KiB
Python
158 lines
4.8 KiB
Python
"""Temperature conversion utilities for SNS Network Solutions."""
|
||
|
||
from enum import Enum
|
||
|
||
|
||
class TemperatureUnit(str, Enum):
|
||
"""Supported temperature units."""
|
||
|
||
CELSIUS = "Celsius"
|
||
FAHRENHEIT = "Fahrenheit"
|
||
KELVIN = "Kelvin"
|
||
|
||
|
||
# Absolute zero in Celsius — used for Kelvin validation
|
||
_ABSOLUTE_ZERO_CELSIUS = -273.15
|
||
_ABSOLUTE_ZERO_FAHRENHEIT = -459.67
|
||
|
||
|
||
def convert_temperature(
|
||
value: float,
|
||
from_unit: str,
|
||
to_unit: str,
|
||
) -> float:
|
||
"""Convert a temperature value between Celsius, Fahrenheit, and Kelvin.
|
||
|
||
Validates that the input value is physically possible (i.e., not below
|
||
absolute zero for the given unit) and that both unit strings are
|
||
recognised before performing the conversion.
|
||
|
||
Args:
|
||
value: The numeric temperature to convert.
|
||
from_unit: The unit of the supplied value. Must be one of
|
||
``'Celsius'``, ``'Fahrenheit'``, or ``'Kelvin'``.
|
||
to_unit: The target unit for the result. Same allowed values as
|
||
*from_unit*.
|
||
|
||
Returns:
|
||
The converted temperature as a ``float``, rounded to six decimal
|
||
places to avoid floating-point noise.
|
||
|
||
Raises:
|
||
TypeError: If *value* is not an ``int`` or ``float``.
|
||
ValueError: If either unit string is not recognised, or if *value*
|
||
is below absolute zero for the given *from_unit*.
|
||
|
||
Examples:
|
||
>>> convert_temperature(100, 'Celsius', 'Fahrenheit')
|
||
212.0
|
||
>>> convert_temperature(32, 'Fahrenheit', 'Celsius')
|
||
0.0
|
||
>>> convert_temperature(0, 'Kelvin', 'Celsius')
|
||
-273.15
|
||
>>> convert_temperature(300, 'Kelvin', 'Fahrenheit')
|
||
80.33
|
||
"""
|
||
# --- type guard ---
|
||
if not isinstance(value, (int, float)):
|
||
raise TypeError(
|
||
f"'value' must be a numeric type (int or float), got {type(value).__name__!r}."
|
||
)
|
||
|
||
# --- unit validation (guard clauses) ---
|
||
valid_units = {u.value for u in TemperatureUnit}
|
||
|
||
if from_unit not in valid_units:
|
||
raise ValueError(
|
||
f"Unrecognised from_unit {from_unit!r}. "
|
||
f"Allowed values: {sorted(valid_units)}."
|
||
)
|
||
if to_unit not in valid_units:
|
||
raise ValueError(
|
||
f"Unrecognised to_unit {to_unit!r}. "
|
||
f"Allowed values: {sorted(valid_units)}."
|
||
)
|
||
|
||
# --- physical validity guard ---
|
||
_validate_above_absolute_zero(value, from_unit)
|
||
|
||
# --- trivial case ---
|
||
if from_unit == to_unit:
|
||
return float(value)
|
||
|
||
# --- convert to Celsius as the common intermediate ---
|
||
celsius = _to_celsius(value, from_unit)
|
||
|
||
# --- convert from Celsius to the target unit ---
|
||
result = _from_celsius(celsius, to_unit)
|
||
|
||
return round(result, 6)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Private helpers
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def _validate_above_absolute_zero(value: float, unit: str) -> None:
|
||
"""Raise ValueError if *value* is below absolute zero for *unit*.
|
||
|
||
Args:
|
||
value: The temperature value to check.
|
||
unit: The unit string (already validated as a recognised unit).
|
||
|
||
Raises:
|
||
ValueError: When *value* is physically impossible.
|
||
"""
|
||
if unit == TemperatureUnit.CELSIUS and value < _ABSOLUTE_ZERO_CELSIUS:
|
||
raise ValueError(
|
||
f"Temperature {value} °C is below absolute zero "
|
||
f"({_ABSOLUTE_ZERO_CELSIUS} °C)."
|
||
)
|
||
if unit == TemperatureUnit.FAHRENHEIT and value < _ABSOLUTE_ZERO_FAHRENHEIT:
|
||
raise ValueError(
|
||
f"Temperature {value} °F is below absolute zero "
|
||
f"({_ABSOLUTE_ZERO_FAHRENHEIT} °F)."
|
||
)
|
||
if unit == TemperatureUnit.KELVIN and value < 0:
|
||
raise ValueError(
|
||
f"Temperature {value} K is below absolute zero (0 K)."
|
||
)
|
||
|
||
|
||
def _to_celsius(value: float, from_unit: str) -> float:
|
||
"""Convert *value* (in *from_unit*) to degrees Celsius.
|
||
|
||
Args:
|
||
value: Source temperature.
|
||
from_unit: Source unit (pre-validated).
|
||
|
||
Returns:
|
||
Equivalent temperature in Celsius.
|
||
"""
|
||
if from_unit == TemperatureUnit.CELSIUS:
|
||
return float(value)
|
||
if from_unit == TemperatureUnit.FAHRENHEIT:
|
||
# °C = (°F − 32) × 5/9
|
||
return (value - 32) * 5 / 9
|
||
# Kelvin → °C = K − 273.15
|
||
return value - 273.15
|
||
|
||
|
||
def _from_celsius(celsius: float, to_unit: str) -> float:
|
||
"""Convert a Celsius value to *to_unit*.
|
||
|
||
Args:
|
||
celsius: Temperature in degrees Celsius.
|
||
to_unit: Target unit (pre-validated).
|
||
|
||
Returns:
|
||
Equivalent temperature in *to_unit*.
|
||
"""
|
||
if to_unit == TemperatureUnit.CELSIUS:
|
||
return celsius
|
||
if to_unit == TemperatureUnit.FAHRENHEIT:
|
||
# °F = °C × 9/5 + 32
|
||
return celsius * 9 / 5 + 32
|
||
# °C → K = °C + 273.15
|
||
return celsius + 273.15
|