2 # -*- coding: utf-8 -*-
4 # © Copyright 2021-2022, Scott Gasch
6 """Utilities involving converting between different units."""
8 from typing import Callable, SupportsFloat
13 class Converter(object):
14 """A converter has a canonical name and a category. The name defines
15 a unit of measurement in a category or class of measurements.
16 This framework will allow conversion between named units in the
17 same category. e.g. name may be "meter", "inch", "mile" and their
18 category may be "length".
20 The way that conversions work is that we convert the magnitude
21 first to a canonical unit and then (if needed) from the canonical
22 unit to the desired destination unit. e.g. if "meter" is the
23 canonical unit of measurement for the category "length", in order
24 to convert miles into inches we first convert miles into meters
25 and, from there, meters into inches. This is potentially
26 dangerous because it requires two floating point operations which
27 each have the potential to overflow, underflow, or introduce
28 floating point errors. Caveat emptor.
35 to_canonical: Callable, # convert to canonical unit
36 from_canonical: Callable, # convert from canonical unit
39 """Construct a converter.
43 category: the converter category
44 to_canonical: a Callable to convert this unit into the
45 canonical unit of the category.
46 from_canonical: a Callable to convert from the canonical
47 unit of this category into this unit.
48 suffix: the abbreviation of the unit name.
52 self.category = category
53 self.to_canonical_f = to_canonical
54 self.from_canonical_f = from_canonical
57 def to_canonical(self, n: SupportsFloat) -> SupportsFloat:
58 """Convert into the canonical unit of this caregory by using the
59 Callable provided during construction."""
60 return self.to_canonical_f(n)
62 def from_canonical(self, n: SupportsFloat) -> SupportsFloat:
63 """Convert from the canonical unit of this category by using the
64 Callable provided during construction."""
65 return self.from_canonical_f(n)
67 def unit_suffix(self) -> str:
68 """Get this unit's suffix abbreviation."""
72 # A catalog of converters.
73 conversion_catalog = {
74 "Second": Converter("Second", "time", lambda s: s, lambda s: s, "s"),
78 lambda m: (m * constants.SECONDS_PER_MINUTE),
79 lambda s: (s / constants.SECONDS_PER_MINUTE),
85 lambda h: (h * constants.SECONDS_PER_HOUR),
86 lambda s: (s / constants.SECONDS_PER_HOUR),
92 lambda d: (d * constants.SECONDS_PER_DAY),
93 lambda s: (s / constants.SECONDS_PER_DAY),
99 lambda w: (w * constants.SECONDS_PER_WEEK),
100 lambda s: (s / constants.SECONDS_PER_WEEK),
103 "Fahrenheit": Converter(
106 lambda f: (f - 32.0) * 0.55555555,
107 lambda c: c * 1.8 + 32.0,
110 "Celsius": Converter("Celsius", "temperature", lambda c: c, lambda c: c, "°C"),
114 lambda k: k - 273.15,
115 lambda c: c + 273.15,
121 def convert(magnitude: SupportsFloat, from_thing: str, to_thing: str) -> float:
122 """Convert between units using the internal catalog.
125 magnitude: the quantity from which to convert
126 from_thing: the quantity's source unit we're coverting from
127 to_thing: the unit we are coverting to
130 The converted magnitude. Raises on error.
132 src = conversion_catalog.get(from_thing, None)
133 dst = conversion_catalog.get(to_thing, None)
134 if src is None or dst is None:
135 raise ValueError("No known conversion")
136 if src.category != dst.category:
137 raise ValueError("Incompatible conversion")
138 return _convert(magnitude, src, dst)
141 def _convert(magnitude: SupportsFloat, from_unit: Converter, to_unit: Converter) -> float:
142 """Internal conversion code."""
143 canonical = from_unit.to_canonical(magnitude)
144 converted = to_unit.from_canonical(canonical)
145 return float(converted)
148 def sec_to_min(s: float) -> float:
150 Convert seconds into minutes.
155 return convert(s, "Second", "Minute")
158 def sec_to_hour(s: float) -> float:
160 Convert seconds into hours.
162 >>> sec_to_hour(1800)
165 return convert(s, "Second", "Hour")
168 def sec_to_day(s: float) -> float:
170 Convert seconds into days.
175 return convert(s, "Second", "Day")
178 def sec_to_week(s: float) -> float:
180 Convert seconds into weeks.
182 >>> sec_to_week(1800)
185 return convert(s, "Second", "Week")
188 def min_to_sec(m: float) -> float:
190 Convert minutes into seconds.
195 return convert(m, "Minute", "Second")
198 def min_to_hour(m: float) -> float:
200 Convert minutes into hours.
205 return convert(m, "Minute", "Hour")
208 def min_to_day(m: float) -> float:
210 Convert minutes into days.
212 >>> min_to_day(60 * 12)
215 return convert(m, "Minute", "Day")
218 def min_to_week(m: float) -> float:
220 Convert minutes into weeks.
222 >>> min_to_week(60 * 24 * 3)
225 return convert(m, "Minute", "Week")
228 def hour_to_sec(h: float) -> float:
230 Convert hours into seconds.
235 return convert(h, "Hour", "Second")
238 def hour_to_min(h: float) -> float:
240 Convert hours into minutes.
245 return convert(h, "Hour", "Minute")
248 def hour_to_day(h: float) -> float:
250 Convert hours into days.
255 return convert(h, "Hour", "Day")
258 def hour_to_week(h: float) -> float:
260 Convert hours into weeks.
265 return convert(h, "Hour", "Week")
268 def day_to_sec(d: float) -> float:
270 Convert days into seconds.
275 return convert(d, "Day", "Second")
278 def day_to_min(d: float) -> float:
280 Convert days into minutes.
285 return convert(d, "Day", "Minute")
288 def day_to_hour(d: float) -> float:
290 Convert days into hours.
295 return convert(d, "Day", "Hour")
298 def day_to_week(d: float) -> float:
300 Convert days into weeks.
305 return convert(d, "Day", "Week")
308 def week_to_sec(w: float) -> float:
310 Convert weeks into seconds.
315 return convert(w, "Week", "Second")
318 def week_to_min(w: float) -> float:
320 Convert weeks into minutes.
325 return convert(w, "Week", "Minute")
328 def week_to_hour(w: float) -> float:
330 Convert weeks into hours.
335 return convert(w, "Week", "Hour")
338 def week_to_day(w: float) -> float:
340 Convert weeks into days.
345 return convert(w, "Week", "Day")
348 def f_to_c(temp_f: float) -> float:
350 Convert Fahrenheit into Celsius.
355 return convert(temp_f, "Fahrenheit", "Celsius")
358 def c_to_f(temp_c: float) -> float:
360 Convert Celsius to Fahrenheit.
365 return convert(temp_c, "Celsius", "Fahrenheit")
368 if __name__ == '__main__':