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
40 self.category = category
41 self.to_canonical_f = to_canonical
42 self.from_canonical_f = from_canonical
45 def to_canonical(self, n: SupportsFloat) -> SupportsFloat:
46 return self.to_canonical_f(n)
48 def from_canonical(self, n: SupportsFloat) -> SupportsFloat:
49 return self.from_canonical_f(n)
51 def unit_suffix(self) -> str:
55 # A catalog of converters.
56 conversion_catalog = {
57 "Second": Converter("Second", "time", lambda s: s, lambda s: s, "s"),
61 lambda m: (m * constants.SECONDS_PER_MINUTE),
62 lambda s: (s / constants.SECONDS_PER_MINUTE),
68 lambda h: (h * constants.SECONDS_PER_HOUR),
69 lambda s: (s / constants.SECONDS_PER_HOUR),
75 lambda d: (d * constants.SECONDS_PER_DAY),
76 lambda s: (s / constants.SECONDS_PER_DAY),
82 lambda w: (w * constants.SECONDS_PER_WEEK),
83 lambda s: (s / constants.SECONDS_PER_WEEK),
86 "Fahrenheit": Converter(
89 lambda f: (f - 32.0) * 0.55555555,
90 lambda c: c * 1.8 + 32.0,
93 "Celsius": Converter("Celsius", "temperature", lambda c: c, lambda c: c, "°C"),
104 def convert(magnitude: SupportsFloat, from_thing: str, to_thing: str) -> float:
105 src = conversion_catalog.get(from_thing, None)
106 dst = conversion_catalog.get(to_thing, None)
107 if src is None or dst is None:
108 raise ValueError("No known conversion")
109 if src.category != dst.category:
110 raise ValueError("Incompatible conversion")
111 return _convert(magnitude, src, dst)
114 def _convert(magnitude: SupportsFloat, from_unit: Converter, to_unit: Converter) -> float:
115 canonical = from_unit.to_canonical(magnitude)
116 converted = to_unit.from_canonical(canonical)
117 return float(converted)
120 def sec_to_min(s: float) -> float:
122 Convert seconds into minutes.
127 return convert(s, "Second", "Minute")
130 def sec_to_hour(s: float) -> float:
132 Convert seconds into hours.
134 >>> sec_to_hour(1800)
137 return convert(s, "Second", "Hour")
140 def sec_to_day(s: float) -> float:
142 Convert seconds into days.
147 return convert(s, "Second", "Day")
150 def sec_to_week(s: float) -> float:
152 Convert seconds into weeks.
154 >>> sec_to_week(1800)
157 return convert(s, "Second", "Week")
160 def min_to_sec(m: float) -> float:
162 Convert minutes into seconds.
167 return convert(m, "Minute", "Second")
170 def min_to_hour(m: float) -> float:
172 Convert minutes into hours.
177 return convert(m, "Minute", "Hour")
180 def min_to_day(m: float) -> float:
182 Convert minutes into days.
184 >>> min_to_day(60 * 12)
187 return convert(m, "Minute", "Day")
190 def min_to_week(m: float) -> float:
192 Convert minutes into weeks.
194 >>> min_to_week(60 * 24 * 3)
197 return convert(m, "Minute", "Week")
200 def hour_to_sec(h: float) -> float:
202 Convert hours into seconds.
207 return convert(h, "Hour", "Second")
210 def hour_to_min(h: float) -> float:
212 Convert hours into minutes.
217 return convert(h, "Hour", "Minute")
220 def hour_to_day(h: float) -> float:
222 Convert hours into days.
227 return convert(h, "Hour", "Day")
230 def hour_to_week(h: float) -> float:
232 Convert hours into weeks.
237 return convert(h, "Hour", "Week")
240 def day_to_sec(d: float) -> float:
242 Convert days into seconds.
247 return convert(d, "Day", "Second")
250 def day_to_min(d: float) -> float:
252 Convert days into minutes.
257 return convert(d, "Day", "Minute")
260 def day_to_hour(d: float) -> float:
262 Convert days into hours.
267 return convert(d, "Day", "Hour")
270 def day_to_week(d: float) -> float:
272 Convert days into weeks.
277 return convert(d, "Day", "Week")
280 def week_to_sec(w: float) -> float:
282 Convert weeks into seconds.
287 return convert(w, "Week", "Second")
290 def week_to_min(w: float) -> float:
292 Convert weeks into minutes.
297 return convert(w, "Week", "Minute")
300 def week_to_hour(w: float) -> float:
302 Convert weeks into hours.
307 return convert(w, "Week", "Hour")
310 def week_to_day(w: float) -> float:
312 Convert weeks into days.
317 return convert(w, "Week", "Day")
320 def f_to_c(temp_f: float) -> float:
322 Convert Fahrenheit into Celsius.
327 return convert(temp_f, "Fahrenheit", "Celsius")
330 def c_to_f(temp_c: float) -> float:
332 Convert Celsius to Fahrenheit.
337 return convert(temp_c, "Celsius", "Fahrenheit")
340 if __name__ == '__main__':