#!/usr/bin/env python3 # -*- coding: utf-8 -*- # © Copyright 2021-2022, Scott Gasch """Utilities involving converting between different units.""" from typing import Callable, SupportsFloat import constants class Converter(object): """A converter has a canonical name and a category. The name defines a unit of measurement in a category or class of measurements. This framework will allow conversion between named units in the same category. e.g. name may be "meter", "inch", "mile" and their category may be "length". The way that conversions work is that we convert the magnitude first to a canonical unit and then (if needed) from the canonical unit to the desired destination unit. e.g. if "meter" is the canonical unit of measurement for the category "length", in order to convert miles into inches we first convert miles into meters and, from there, meters into inches. This is potentially dangerous because it requires two floating point operations which each have the potential to overflow, underflow, or introduce floating point errors. Caveat emptor. """ def __init__( self, name: str, category: str, to_canonical: Callable, # convert to canonical unit from_canonical: Callable, # convert from canonical unit suffix: str, ) -> None: """Construct a converter. Args: name: the unit name category: the converter category to_canonical: a Callable to convert this unit into the canonical unit of the category. from_canonical: a Callable to convert from the canonical unit of this category into this unit. suffix: the abbreviation of the unit name. """ self.name = name self.category = category self.to_canonical_f = to_canonical self.from_canonical_f = from_canonical self.suffix = suffix def to_canonical(self, n: SupportsFloat) -> SupportsFloat: """Convert into the canonical unit of this caregory by using the Callable provided during construction.""" return self.to_canonical_f(n) def from_canonical(self, n: SupportsFloat) -> SupportsFloat: """Convert from the canonical unit of this category by using the Callable provided during construction.""" return self.from_canonical_f(n) def unit_suffix(self) -> str: """Get this unit's suffix abbreviation.""" return self.suffix # A catalog of converters. conversion_catalog = { "Second": Converter("Second", "time", lambda s: s, lambda s: s, "s"), "Minute": Converter( "Minute", "time", lambda m: (m * constants.SECONDS_PER_MINUTE), lambda s: (s / constants.SECONDS_PER_MINUTE), "m", ), "Hour": Converter( "Hour", "time", lambda h: (h * constants.SECONDS_PER_HOUR), lambda s: (s / constants.SECONDS_PER_HOUR), "h", ), "Day": Converter( "Day", "time", lambda d: (d * constants.SECONDS_PER_DAY), lambda s: (s / constants.SECONDS_PER_DAY), "d", ), "Week": Converter( "Week", "time", lambda w: (w * constants.SECONDS_PER_WEEK), lambda s: (s / constants.SECONDS_PER_WEEK), "w", ), "Fahrenheit": Converter( "Fahrenheit", "temperature", lambda f: (f - 32.0) * 0.55555555, lambda c: c * 1.8 + 32.0, "°F", ), "Celsius": Converter("Celsius", "temperature", lambda c: c, lambda c: c, "°C"), "Kelvin": Converter( "Kelvin", "temperature", lambda k: k - 273.15, lambda c: c + 273.15, "°K", ), } def convert(magnitude: SupportsFloat, from_thing: str, to_thing: str) -> float: """Convert between units using the internal catalog. Args: magnitude: the quantity from which to convert from_thing: the quantity's source unit we're coverting from to_thing: the unit we are coverting to Returns: The converted magnitude. Raises on error. """ src = conversion_catalog.get(from_thing, None) dst = conversion_catalog.get(to_thing, None) if src is None or dst is None: raise ValueError("No known conversion") if src.category != dst.category: raise ValueError("Incompatible conversion") return _convert(magnitude, src, dst) def _convert(magnitude: SupportsFloat, from_unit: Converter, to_unit: Converter) -> float: """Internal conversion code.""" canonical = from_unit.to_canonical(magnitude) converted = to_unit.from_canonical(canonical) return float(converted) def sec_to_min(s: float) -> float: """ Convert seconds into minutes. >>> sec_to_min(120) 2.0 """ return convert(s, "Second", "Minute") def sec_to_hour(s: float) -> float: """ Convert seconds into hours. >>> sec_to_hour(1800) 0.5 """ return convert(s, "Second", "Hour") def sec_to_day(s: float) -> float: """ Convert seconds into days. >>> sec_to_day(1800) 0.020833333333333332 """ return convert(s, "Second", "Day") def sec_to_week(s: float) -> float: """ Convert seconds into weeks. >>> sec_to_week(1800) 0.002976190476190476 """ return convert(s, "Second", "Week") def min_to_sec(m: float) -> float: """ Convert minutes into seconds. >>> min_to_sec(5) 300.0 """ return convert(m, "Minute", "Second") def min_to_hour(m: float) -> float: """ Convert minutes into hours. >>> min_to_hour(120) 2.0 """ return convert(m, "Minute", "Hour") def min_to_day(m: float) -> float: """ Convert minutes into days. >>> min_to_day(60 * 12) 0.5 """ return convert(m, "Minute", "Day") def min_to_week(m: float) -> float: """ Convert minutes into weeks. >>> min_to_week(60 * 24 * 3) 0.42857142857142855 """ return convert(m, "Minute", "Week") def hour_to_sec(h: float) -> float: """ Convert hours into seconds. >>> hour_to_sec(1) 3600.0 """ return convert(h, "Hour", "Second") def hour_to_min(h: float) -> float: """ Convert hours into minutes. >>> hour_to_min(2) 120.0 """ return convert(h, "Hour", "Minute") def hour_to_day(h: float) -> float: """ Convert hours into days. >>> hour_to_day(36) 1.5 """ return convert(h, "Hour", "Day") def hour_to_week(h: float) -> float: """ Convert hours into weeks. >>> hour_to_week(24) 0.14285714285714285 """ return convert(h, "Hour", "Week") def day_to_sec(d: float) -> float: """ Convert days into seconds. >>> day_to_sec(1) 86400.0 """ return convert(d, "Day", "Second") def day_to_min(d: float) -> float: """ Convert days into minutes. >>> day_to_min(0.1) 144.0 """ return convert(d, "Day", "Minute") def day_to_hour(d: float) -> float: """ Convert days into hours. >>> day_to_hour(1) 24.0 """ return convert(d, "Day", "Hour") def day_to_week(d: float) -> float: """ Convert days into weeks. >>> day_to_week(14) 2.0 """ return convert(d, "Day", "Week") def week_to_sec(w: float) -> float: """ Convert weeks into seconds. >>> week_to_sec(10) 6048000.0 """ return convert(w, "Week", "Second") def week_to_min(w: float) -> float: """ Convert weeks into minutes. >>> week_to_min(1) 10080.0 """ return convert(w, "Week", "Minute") def week_to_hour(w: float) -> float: """ Convert weeks into hours. >>> week_to_hour(1) 168.0 """ return convert(w, "Week", "Hour") def week_to_day(w: float) -> float: """ Convert weeks into days. >>> week_to_day(1) 7.0 """ return convert(w, "Week", "Day") def f_to_c(temp_f: float) -> float: """ Convert Fahrenheit into Celsius. >>> f_to_c(32.0) 0.0 """ return convert(temp_f, "Fahrenheit", "Celsius") def c_to_f(temp_c: float) -> float: """ Convert Celsius to Fahrenheit. >>> c_to_f(0.0) 32.0 """ return convert(temp_c, "Celsius", "Fahrenheit") if __name__ == '__main__': import doctest doctest.testmod()