Easier and more self documenting patterns for loading/saving Persistent
[python_utils.git] / conversion_utils.py
index d2225fdb2b2f37b78320ee03b07a27be6ade45ac..57902a79754e2b7950f800cf82d3bdabc74114a9 100644 (file)
@@ -1,7 +1,11 @@
 #!/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
 
@@ -23,76 +27,108 @@ class Converter(object):
     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
-                 unit: str) -> None:
+
+    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.unit = unit
+        self.suffix = suffix
 
-    def to_canonical(self, n: Number) -> Number:
+    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:
+    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 = {
-    "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"),
+    "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) -> float:
+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:
@@ -102,9 +138,8 @@ def convert(magnitude: Number,
     return _convert(magnitude, src, dst)
 
 
-def _convert(magnitude: Number,
-             from_unit: Converter,
-             to_unit: Converter) -> float:
+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)
@@ -332,4 +367,5 @@ def c_to_f(temp_c: float) -> float:
 
 if __name__ == '__main__':
     import doctest
+
     doctest.testmod()