X-Git-Url: https://wannabe.guru.org/gitweb/?a=blobdiff_plain;f=conversion_utils.py;h=57902a79754e2b7950f800cf82d3bdabc74114a9;hb=e46158e49121b8a955bb07b73f5bcf9928b79c90;hp=bab27e1215dc0a67389e2353ba77b9b53c4ff7a1;hpb=497fb9e21f45ec08e1486abaee6dfa7b20b8a691;p=python_utils.git diff --git a/conversion_utils.py b/conversion_utils.py index bab27e1..57902a7 100644 --- a/conversion_utils.py +++ b/conversion_utils.py @@ -1,74 +1,371 @@ #!/usr/bin/env python3 +# -*- coding: utf-8 -*- -from numbers import Number -from typing import Callable +# © Copyright 2021-2022, Scott Gasch + +"""Utilities involving converting between different units.""" + +from typing import Callable, SupportsFloat + +import constants class Converter(object): - def __init__(self, - name: str, - category: str, - to_canonical: Callable, - from_canonical: Callable, - unit: str) -> None: + """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 = to_canonical - self.from_canonical = from_canonical - self.unit = unit + self.to_canonical_f = to_canonical + self.from_canonical_f = from_canonical + self.suffix = suffix - def to_canonical(self, n: Number) -> Number: - return self.to_canonical(n) + 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: Number) -> Number: - return self.from_canonical(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: - return self.unit + """Get this unit's suffix abbreviation.""" + return self.suffix +# A catalog of converters. conversion_catalog = { - "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"), + "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: Number, - from_thing: str, - to_thing: str) -> Number: +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: Number, - from_unit: Converter, - to_unit: Converter) -> Number: +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 converted + 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: - """Fahrenheit to Celsius.""" + """ + 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: - """Celsius to Fahrenheit.""" + """ + Convert Celsius to Fahrenheit. + + >>> c_to_f(0.0) + 32.0 + """ return convert(temp_c, "Celsius", "Fahrenheit") + + +if __name__ == '__main__': + import doctest + + doctest.testmod()