diff --git a/homeassistant/util/distance.py b/homeassistant/util/distance.py index 4a7445c46ed..7eaa1d23fd9 100644 --- a/homeassistant/util/distance.py +++ b/homeassistant/util/distance.py @@ -1,9 +1,7 @@ """Distance util functions.""" from __future__ import annotations -from numbers import Number - -from homeassistant.const import ( +from homeassistant.const import ( # pylint: disable=unused-import # noqa: F401 LENGTH, LENGTH_CENTIMETERS, LENGTH_FEET, @@ -16,53 +14,11 @@ from homeassistant.const import ( UNIT_NOT_RECOGNIZED_TEMPLATE, ) -VALID_UNITS: tuple[str, ...] = ( - LENGTH_KILOMETERS, - LENGTH_MILES, - LENGTH_FEET, - LENGTH_METERS, - LENGTH_CENTIMETERS, - LENGTH_MILLIMETERS, - LENGTH_INCHES, - LENGTH_YARD, -) +from .unit_conversion import DistanceConverter -MM_TO_M = 0.001 # 1 mm = 0.001 m -CM_TO_M = 0.01 # 1 cm = 0.01 m -KM_TO_M = 1000 # 1 km = 1000 m - -IN_TO_M = 0.0254 # 1 inch = 0.0254 m -FOOT_TO_M = IN_TO_M * 12 # 12 inches = 1 foot (0.3048 m) -YARD_TO_M = FOOT_TO_M * 3 # 3 feet = 1 yard (0.9144 m) -MILE_TO_M = YARD_TO_M * 1760 # 1760 yard = 1 mile (1609.344 m) - -NAUTICAL_MILE_TO_M = 1852 # 1 nautical mile = 1852 m - -UNIT_CONVERSION: dict[str, float] = { - LENGTH_METERS: 1, - LENGTH_MILLIMETERS: 1 / MM_TO_M, - LENGTH_CENTIMETERS: 1 / CM_TO_M, - LENGTH_KILOMETERS: 1 / KM_TO_M, - LENGTH_INCHES: 1 / IN_TO_M, - LENGTH_FEET: 1 / FOOT_TO_M, - LENGTH_YARD: 1 / YARD_TO_M, - LENGTH_MILES: 1 / MILE_TO_M, -} +VALID_UNITS = DistanceConverter.VALID_UNITS -def convert(value: float, unit_1: str, unit_2: str) -> float: +def convert(value: float, from_unit: str, to_unit: str) -> float: """Convert one unit of measurement to another.""" - if unit_1 not in VALID_UNITS: - raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_1, LENGTH)) - if unit_2 not in VALID_UNITS: - raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_2, LENGTH)) - - if not isinstance(value, Number): - raise TypeError(f"{value} is not of numeric type") - - if unit_1 == unit_2 or unit_1 not in VALID_UNITS: - return value - - meters: float = value / UNIT_CONVERSION[unit_1] - - return meters * UNIT_CONVERSION[unit_2] + return DistanceConverter.convert(value, from_unit, to_unit) diff --git a/homeassistant/util/speed.py b/homeassistant/util/speed.py index 823c65b59b0..31993549586 100644 --- a/homeassistant/util/speed.py +++ b/homeassistant/util/speed.py @@ -1,9 +1,7 @@ """Distance util functions.""" from __future__ import annotations -from numbers import Number - -from homeassistant.const import ( +from homeassistant.const import ( # pylint: disable=unused-import # noqa: F401 SPEED, SPEED_FEET_PER_SECOND, SPEED_INCHES_PER_DAY, @@ -16,54 +14,20 @@ from homeassistant.const import ( UNIT_NOT_RECOGNIZED_TEMPLATE, ) -from .distance import ( - FOOT_TO_M, - IN_TO_M, - KM_TO_M, - MILE_TO_M, - MM_TO_M, - NAUTICAL_MILE_TO_M, +from .unit_conversion import ( # pylint: disable=unused-import # noqa: F401 + _FOOT_TO_M as FOOT_TO_M, + _HRS_TO_SECS as HRS_TO_SECS, + _IN_TO_M as IN_TO_M, + _KM_TO_M as KM_TO_M, + _MILE_TO_M as MILE_TO_M, + _NAUTICAL_MILE_TO_M as NAUTICAL_MILE_TO_M, + SpeedConverter, ) -VALID_UNITS: tuple[str, ...] = ( - SPEED_FEET_PER_SECOND, - SPEED_INCHES_PER_DAY, - SPEED_INCHES_PER_HOUR, - SPEED_KILOMETERS_PER_HOUR, - SPEED_KNOTS, - SPEED_METERS_PER_SECOND, - SPEED_MILES_PER_HOUR, - SPEED_MILLIMETERS_PER_DAY, -) - -HRS_TO_SECS = 60 * 60 # 1 hr = 3600 seconds -DAYS_TO_SECS = 24 * HRS_TO_SECS # 1 day = 24 hours = 86400 seconds - -# Units in terms of m/s -UNIT_CONVERSION: dict[str, float] = { - SPEED_FEET_PER_SECOND: 1 / FOOT_TO_M, - SPEED_INCHES_PER_DAY: DAYS_TO_SECS / IN_TO_M, - SPEED_INCHES_PER_HOUR: HRS_TO_SECS / IN_TO_M, - SPEED_KILOMETERS_PER_HOUR: HRS_TO_SECS / KM_TO_M, - SPEED_KNOTS: HRS_TO_SECS / NAUTICAL_MILE_TO_M, - SPEED_METERS_PER_SECOND: 1, - SPEED_MILES_PER_HOUR: HRS_TO_SECS / MILE_TO_M, - SPEED_MILLIMETERS_PER_DAY: DAYS_TO_SECS / MM_TO_M, -} +UNIT_CONVERSION = SpeedConverter.UNIT_CONVERSION +VALID_UNITS = SpeedConverter.VALID_UNITS -def convert(value: float, unit_1: str, unit_2: str) -> float: +def convert(value: float, from_unit: str, to_unit: str) -> float: """Convert one unit of measurement to another.""" - if unit_1 not in VALID_UNITS: - raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_1, SPEED)) - if unit_2 not in VALID_UNITS: - raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_2, SPEED)) - - if not isinstance(value, Number): - raise TypeError(f"{value} is not of numeric type") - - if unit_1 == unit_2: - return value - - meters_per_second = value / UNIT_CONVERSION[unit_1] - return meters_per_second * UNIT_CONVERSION[unit_2] + return SpeedConverter.convert(value, from_unit, to_unit) diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index fbc5c05b706..363c1bf5f3c 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -8,6 +8,14 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR, ENERGY_WATT_HOUR, + LENGTH_CENTIMETERS, + LENGTH_FEET, + LENGTH_INCHES, + LENGTH_KILOMETERS, + LENGTH_METERS, + LENGTH_MILES, + LENGTH_MILLIMETERS, + LENGTH_YARD, POWER_KILO_WATT, POWER_WATT, PRESSURE_BAR, @@ -19,6 +27,14 @@ from homeassistant.const import ( PRESSURE_MMHG, PRESSURE_PA, PRESSURE_PSI, + SPEED_FEET_PER_SECOND, + SPEED_INCHES_PER_DAY, + SPEED_INCHES_PER_HOUR, + SPEED_KILOMETERS_PER_HOUR, + SPEED_KNOTS, + SPEED_METERS_PER_SECOND, + SPEED_MILES_PER_HOUR, + SPEED_MILLIMETERS_PER_DAY, TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN, @@ -31,14 +47,28 @@ from homeassistant.const import ( VOLUME_MILLILITERS, ) -from .distance import FOOT_TO_M, IN_TO_M +# Distance conversion constants +_MM_TO_M = 0.001 # 1 mm = 0.001 m +_CM_TO_M = 0.01 # 1 cm = 0.01 m +_KM_TO_M = 1000 # 1 km = 1000 m + +_IN_TO_M = 0.0254 # 1 inch = 0.0254 m +_FOOT_TO_M = _IN_TO_M * 12 # 12 inches = 1 foot (0.3048 m) +_YARD_TO_M = _FOOT_TO_M * 3 # 3 feet = 1 yard (0.9144 m) +_MILE_TO_M = _YARD_TO_M * 1760 # 1760 yard = 1 mile (1609.344 m) + +_NAUTICAL_MILE_TO_M = 1852 # 1 nautical mile = 1852 m + +# Duration conversion constants +_HRS_TO_SECS = 60 * 60 # 1 hr = 3600 seconds +_DAYS_TO_SECS = 24 * _HRS_TO_SECS # 1 day = 24 hours = 86400 seconds # Volume conversion constants _L_TO_CUBIC_METER = 0.001 # 1 L = 0.001 m³ _ML_TO_CUBIC_METER = 0.001 * _L_TO_CUBIC_METER # 1 mL = 0.001 L -_GALLON_TO_CUBIC_METER = 231 * pow(IN_TO_M, 3) # US gallon is 231 cubic inches +_GALLON_TO_CUBIC_METER = 231 * pow(_IN_TO_M, 3) # US gallon is 231 cubic inches _FLUID_OUNCE_TO_CUBIC_METER = _GALLON_TO_CUBIC_METER / 128 # 128 fl. oz. in a US gallon -_CUBIC_FOOT_TO_CUBIC_METER = pow(FOOT_TO_M, 3) +_CUBIC_FOOT_TO_CUBIC_METER = pow(_FOOT_TO_M, 3) class BaseUnitConverter: @@ -86,6 +116,33 @@ class BaseUnitConverterWithUnitConversion(BaseUnitConverter): return new_value * cls.UNIT_CONVERSION[to_unit] +class DistanceConverter(BaseUnitConverterWithUnitConversion): + """Utility to convert distance values.""" + + UNIT_CLASS = "distance" + NORMALIZED_UNIT = LENGTH_METERS + UNIT_CONVERSION: dict[str, float] = { + LENGTH_METERS: 1, + LENGTH_MILLIMETERS: 1 / _MM_TO_M, + LENGTH_CENTIMETERS: 1 / _CM_TO_M, + LENGTH_KILOMETERS: 1 / _KM_TO_M, + LENGTH_INCHES: 1 / _IN_TO_M, + LENGTH_FEET: 1 / _FOOT_TO_M, + LENGTH_YARD: 1 / _YARD_TO_M, + LENGTH_MILES: 1 / _MILE_TO_M, + } + VALID_UNITS: tuple[str, ...] = ( + LENGTH_KILOMETERS, + LENGTH_MILES, + LENGTH_FEET, + LENGTH_METERS, + LENGTH_CENTIMETERS, + LENGTH_MILLIMETERS, + LENGTH_INCHES, + LENGTH_YARD, + ) + + class EnergyConverter(BaseUnitConverterWithUnitConversion): """Utility to convert energy values.""" @@ -147,6 +204,33 @@ class PressureConverter(BaseUnitConverterWithUnitConversion): ) +class SpeedConverter(BaseUnitConverterWithUnitConversion): + """Utility to convert speed values.""" + + UNIT_CLASS = "speed" + NORMALIZED_UNIT = SPEED_METERS_PER_SECOND + UNIT_CONVERSION: dict[str, float] = { + SPEED_FEET_PER_SECOND: 1 / _FOOT_TO_M, + SPEED_INCHES_PER_DAY: _DAYS_TO_SECS / _IN_TO_M, + SPEED_INCHES_PER_HOUR: _HRS_TO_SECS / _IN_TO_M, + SPEED_KILOMETERS_PER_HOUR: _HRS_TO_SECS / _KM_TO_M, + SPEED_KNOTS: _HRS_TO_SECS / _NAUTICAL_MILE_TO_M, + SPEED_METERS_PER_SECOND: 1, + SPEED_MILES_PER_HOUR: _HRS_TO_SECS / _MILE_TO_M, + SPEED_MILLIMETERS_PER_DAY: _DAYS_TO_SECS / _MM_TO_M, + } + VALID_UNITS: tuple[str, ...] = ( + SPEED_FEET_PER_SECOND, + SPEED_INCHES_PER_DAY, + SPEED_INCHES_PER_HOUR, + SPEED_KILOMETERS_PER_HOUR, + SPEED_KNOTS, + SPEED_METERS_PER_SECOND, + SPEED_MILES_PER_HOUR, + SPEED_MILLIMETERS_PER_DAY, + ) + + class TemperatureConverter(BaseUnitConverter): """Utility to convert temperature values.""" diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index 3f06393be3e..fb31624d255 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -5,6 +5,14 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR, ENERGY_WATT_HOUR, + LENGTH_CENTIMETERS, + LENGTH_FEET, + LENGTH_INCHES, + LENGTH_KILOMETERS, + LENGTH_METERS, + LENGTH_MILES, + LENGTH_MILLIMETERS, + LENGTH_YARD, POWER_KILO_WATT, POWER_WATT, PRESSURE_CBAR, @@ -15,6 +23,14 @@ from homeassistant.const import ( PRESSURE_MMHG, PRESSURE_PA, PRESSURE_PSI, + SPEED_FEET_PER_SECOND, + SPEED_INCHES_PER_DAY, + SPEED_INCHES_PER_HOUR, + SPEED_KILOMETERS_PER_HOUR, + SPEED_KNOTS, + SPEED_METERS_PER_SECOND, + SPEED_MILES_PER_HOUR, + SPEED_MILLIMETERS_PER_DAY, TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN, @@ -27,9 +43,11 @@ from homeassistant.const import ( ) from homeassistant.util.unit_conversion import ( BaseUnitConverter, + DistanceConverter, EnergyConverter, PowerConverter, PressureConverter, + SpeedConverter, TemperatureConverter, VolumeConverter, ) @@ -40,6 +58,14 @@ INVALID_SYMBOL = "bob" @pytest.mark.parametrize( "converter,valid_unit", [ + (DistanceConverter, LENGTH_KILOMETERS), + (DistanceConverter, LENGTH_METERS), + (DistanceConverter, LENGTH_CENTIMETERS), + (DistanceConverter, LENGTH_MILLIMETERS), + (DistanceConverter, LENGTH_MILES), + (DistanceConverter, LENGTH_YARD), + (DistanceConverter, LENGTH_FEET), + (DistanceConverter, LENGTH_INCHES), (EnergyConverter, ENERGY_WATT_HOUR), (EnergyConverter, ENERGY_KILO_WATT_HOUR), (EnergyConverter, ENERGY_MEGA_WATT_HOUR), @@ -53,6 +79,14 @@ INVALID_SYMBOL = "bob" (PressureConverter, PRESSURE_CBAR), (PressureConverter, PRESSURE_MMHG), (PressureConverter, PRESSURE_PSI), + (SpeedConverter, SPEED_FEET_PER_SECOND), + (SpeedConverter, SPEED_INCHES_PER_DAY), + (SpeedConverter, SPEED_INCHES_PER_HOUR), + (SpeedConverter, SPEED_KILOMETERS_PER_HOUR), + (SpeedConverter, SPEED_KNOTS), + (SpeedConverter, SPEED_METERS_PER_SECOND), + (SpeedConverter, SPEED_MILES_PER_HOUR), + (SpeedConverter, SPEED_MILLIMETERS_PER_DAY), (TemperatureConverter, TEMP_CELSIUS), (TemperatureConverter, TEMP_FAHRENHEIT), (TemperatureConverter, TEMP_KELVIN), @@ -70,9 +104,11 @@ def test_convert_same_unit(converter: type[BaseUnitConverter], valid_unit: str) @pytest.mark.parametrize( "converter,valid_unit", [ + (DistanceConverter, LENGTH_KILOMETERS), (EnergyConverter, ENERGY_KILO_WATT_HOUR), (PowerConverter, POWER_WATT), (PressureConverter, PRESSURE_PA), + (SpeedConverter, SPEED_KILOMETERS_PER_HOUR), (TemperatureConverter, TEMP_CELSIUS), (VolumeConverter, VOLUME_LITERS), ], @@ -91,9 +127,11 @@ def test_convert_invalid_unit( @pytest.mark.parametrize( "converter,from_unit,to_unit", [ + (DistanceConverter, LENGTH_KILOMETERS, LENGTH_METERS), (EnergyConverter, ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR), (PowerConverter, POWER_WATT, POWER_KILO_WATT), (PressureConverter, PRESSURE_HPA, PRESSURE_INHG), + (SpeedConverter, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR), (TemperatureConverter, TEMP_CELSIUS, TEMP_FAHRENHEIT), (VolumeConverter, VOLUME_GALLONS, VOLUME_LITERS), ], @@ -106,6 +144,77 @@ def test_convert_nonnumeric_value( converter.convert("a", from_unit, to_unit) +@pytest.mark.parametrize( + "value,from_unit,expected,to_unit", + [ + (5, LENGTH_MILES, pytest.approx(8.04672), LENGTH_KILOMETERS), + (5, LENGTH_MILES, pytest.approx(8046.72), LENGTH_METERS), + (5, LENGTH_MILES, pytest.approx(804672.0), LENGTH_CENTIMETERS), + (5, LENGTH_MILES, pytest.approx(8046720.0), LENGTH_MILLIMETERS), + (5, LENGTH_MILES, pytest.approx(8800.0), LENGTH_YARD), + (5, LENGTH_MILES, pytest.approx(26400.0008448), LENGTH_FEET), + (5, LENGTH_MILES, pytest.approx(316800.171072), LENGTH_INCHES), + (5, LENGTH_YARD, pytest.approx(0.0045720000000000005), LENGTH_KILOMETERS), + (5, LENGTH_YARD, pytest.approx(4.572), LENGTH_METERS), + (5, LENGTH_YARD, pytest.approx(457.2), LENGTH_CENTIMETERS), + (5, LENGTH_YARD, pytest.approx(4572), LENGTH_MILLIMETERS), + (5, LENGTH_YARD, pytest.approx(0.002840908212), LENGTH_MILES), + (5, LENGTH_YARD, pytest.approx(15.00000048), LENGTH_FEET), + (5, LENGTH_YARD, pytest.approx(180.0000972), LENGTH_INCHES), + (5000, LENGTH_FEET, pytest.approx(1.524), LENGTH_KILOMETERS), + (5000, LENGTH_FEET, pytest.approx(1524), LENGTH_METERS), + (5000, LENGTH_FEET, pytest.approx(152400.0), LENGTH_CENTIMETERS), + (5000, LENGTH_FEET, pytest.approx(1524000.0), LENGTH_MILLIMETERS), + (5000, LENGTH_FEET, pytest.approx(0.9469694040000001), LENGTH_MILES), + (5000, LENGTH_FEET, pytest.approx(1666.66667), LENGTH_YARD), + (5000, LENGTH_FEET, pytest.approx(60000.032400000004), LENGTH_INCHES), + (5000, LENGTH_INCHES, pytest.approx(0.127), LENGTH_KILOMETERS), + (5000, LENGTH_INCHES, pytest.approx(127.0), LENGTH_METERS), + (5000, LENGTH_INCHES, pytest.approx(12700.0), LENGTH_CENTIMETERS), + (5000, LENGTH_INCHES, pytest.approx(127000.0), LENGTH_MILLIMETERS), + (5000, LENGTH_INCHES, pytest.approx(0.078914117), LENGTH_MILES), + (5000, LENGTH_INCHES, pytest.approx(138.88889), LENGTH_YARD), + (5000, LENGTH_INCHES, pytest.approx(416.66668), LENGTH_FEET), + (5, LENGTH_KILOMETERS, pytest.approx(5000), LENGTH_METERS), + (5, LENGTH_KILOMETERS, pytest.approx(500000), LENGTH_CENTIMETERS), + (5, LENGTH_KILOMETERS, pytest.approx(5000000), LENGTH_MILLIMETERS), + (5, LENGTH_KILOMETERS, pytest.approx(3.106855), LENGTH_MILES), + (5, LENGTH_KILOMETERS, pytest.approx(5468.066), LENGTH_YARD), + (5, LENGTH_KILOMETERS, pytest.approx(16404.2), LENGTH_FEET), + (5, LENGTH_KILOMETERS, pytest.approx(196850.5), LENGTH_INCHES), + (5000, LENGTH_METERS, pytest.approx(5), LENGTH_KILOMETERS), + (5000, LENGTH_METERS, pytest.approx(500000), LENGTH_CENTIMETERS), + (5000, LENGTH_METERS, pytest.approx(5000000), LENGTH_MILLIMETERS), + (5000, LENGTH_METERS, pytest.approx(3.106855), LENGTH_MILES), + (5000, LENGTH_METERS, pytest.approx(5468.066), LENGTH_YARD), + (5000, LENGTH_METERS, pytest.approx(16404.2), LENGTH_FEET), + (5000, LENGTH_METERS, pytest.approx(196850.5), LENGTH_INCHES), + (500000, LENGTH_CENTIMETERS, pytest.approx(5), LENGTH_KILOMETERS), + (500000, LENGTH_CENTIMETERS, pytest.approx(5000), LENGTH_METERS), + (500000, LENGTH_CENTIMETERS, pytest.approx(5000000), LENGTH_MILLIMETERS), + (500000, LENGTH_CENTIMETERS, pytest.approx(3.106855), LENGTH_MILES), + (500000, LENGTH_CENTIMETERS, pytest.approx(5468.066), LENGTH_YARD), + (500000, LENGTH_CENTIMETERS, pytest.approx(16404.2), LENGTH_FEET), + (500000, LENGTH_CENTIMETERS, pytest.approx(196850.5), LENGTH_INCHES), + (5000000, LENGTH_MILLIMETERS, pytest.approx(5), LENGTH_KILOMETERS), + (5000000, LENGTH_MILLIMETERS, pytest.approx(5000), LENGTH_METERS), + (5000000, LENGTH_MILLIMETERS, pytest.approx(500000), LENGTH_CENTIMETERS), + (5000000, LENGTH_MILLIMETERS, pytest.approx(3.106855), LENGTH_MILES), + (5000000, LENGTH_MILLIMETERS, pytest.approx(5468.066), LENGTH_YARD), + (5000000, LENGTH_MILLIMETERS, pytest.approx(16404.2), LENGTH_FEET), + (5000000, LENGTH_MILLIMETERS, pytest.approx(196850.5), LENGTH_INCHES), + ], +) +def test_distance_convert( + value: float, + from_unit: str, + expected: float, + to_unit: str, +) -> None: + """Test conversion to other units.""" + assert DistanceConverter.convert(value, from_unit, to_unit) == expected + + @pytest.mark.parametrize( "value,from_unit,expected,to_unit", [ @@ -185,6 +294,44 @@ def test_pressure_convert( assert PressureConverter.convert(value, from_unit, to_unit) == expected +@pytest.mark.parametrize( + "value,from_unit,expected,to_unit", + [ + # 5 km/h / 1.609 km/mi = 3.10686 mi/h + (5, SPEED_KILOMETERS_PER_HOUR, pytest.approx(3.106856), SPEED_MILES_PER_HOUR), + # 5 mi/h * 1.609 km/mi = 8.04672 km/h + (5, SPEED_MILES_PER_HOUR, 8.04672, SPEED_KILOMETERS_PER_HOUR), + # 5 in/day * 25.4 mm/in = 127 mm/day + (5, SPEED_INCHES_PER_DAY, 127, SPEED_MILLIMETERS_PER_DAY), + # 5 mm/day / 25.4 mm/in = 0.19685 in/day + (5, SPEED_MILLIMETERS_PER_DAY, pytest.approx(0.1968504), SPEED_INCHES_PER_DAY), + # 5 in/hr * 24 hr/day = 3048 mm/day + (5, SPEED_INCHES_PER_HOUR, 3048, SPEED_MILLIMETERS_PER_DAY), + # 5 m/s * 39.3701 in/m * 3600 s/hr = 708661 + (5, SPEED_METERS_PER_SECOND, pytest.approx(708661.42), SPEED_INCHES_PER_HOUR), + # 5000 in/h / 39.3701 in/m / 3600 s/h = 0.03528 m/s + ( + 5000, + SPEED_INCHES_PER_HOUR, + pytest.approx(0.0352778), + SPEED_METERS_PER_SECOND, + ), + # 5 kt * 1852 m/nmi / 3600 s/h = 2.5722 m/s + (5, SPEED_KNOTS, pytest.approx(2.57222), SPEED_METERS_PER_SECOND), + # 5 ft/s * 0.3048 m/ft = 1.524 m/s + (5, SPEED_FEET_PER_SECOND, pytest.approx(1.524), SPEED_METERS_PER_SECOND), + ], +) +def test_speed_convert( + value: float, + from_unit: str, + expected: float, + to_unit: str, +) -> None: + """Test conversion to other units.""" + assert SpeedConverter.convert(value, from_unit, to_unit) == expected + + @pytest.mark.parametrize( "value,from_unit,expected,to_unit", [