#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
-from numbers import Number
-from typing import Callable
+"""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
+ unit: str,
+ ) -> None:
self.name = name
self.category = category
self.to_canonical_f = to_canonical
self.from_canonical_f = from_canonical
self.unit = unit
- def to_canonical(self, n: Number) -> Number:
+ def to_canonical(self, n: SupportsFloat) -> SupportsFloat:
return self.to_canonical_f(n)
- def from_canonical(self, n: Number) -> Number:
+ def from_canonical(self, n: SupportsFloat) -> SupportsFloat:
return self.from_canonical_f(n)
def unit_suffix(self) -> str:
return self.unit
+# 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:
src = conversion_catalog.get(from_thing, None)
dst = conversion_catalog.get(to_thing, None)
if src is None or dst is None:
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:
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:
- """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()