Handle census site unavailability w/o throwing.
[python_utils.git] / conversion_utils.py
1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*-
3
4 # © Copyright 2021-2022, Scott Gasch
5
6 """Utilities involving converting between different units."""
7
8 from typing import Callable, SupportsFloat
9
10 import constants
11
12
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".
19
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.
29     """
30
31     def __init__(
32         self,
33         name: str,
34         category: str,
35         to_canonical: Callable,  # convert to canonical unit
36         from_canonical: Callable,  # convert from canonical unit
37         unit: str,
38     ) -> None:
39         self.name = name
40         self.category = category
41         self.to_canonical_f = to_canonical
42         self.from_canonical_f = from_canonical
43         self.unit = unit
44
45     def to_canonical(self, n: SupportsFloat) -> SupportsFloat:
46         return self.to_canonical_f(n)
47
48     def from_canonical(self, n: SupportsFloat) -> SupportsFloat:
49         return self.from_canonical_f(n)
50
51     def unit_suffix(self) -> str:
52         return self.unit
53
54
55 # A catalog of converters.
56 conversion_catalog = {
57     "Second": Converter("Second", "time", lambda s: s, lambda s: s, "s"),
58     "Minute": Converter(
59         "Minute",
60         "time",
61         lambda m: (m * constants.SECONDS_PER_MINUTE),
62         lambda s: (s / constants.SECONDS_PER_MINUTE),
63         "m",
64     ),
65     "Hour": Converter(
66         "Hour",
67         "time",
68         lambda h: (h * constants.SECONDS_PER_HOUR),
69         lambda s: (s / constants.SECONDS_PER_HOUR),
70         "h",
71     ),
72     "Day": Converter(
73         "Day",
74         "time",
75         lambda d: (d * constants.SECONDS_PER_DAY),
76         lambda s: (s / constants.SECONDS_PER_DAY),
77         "d",
78     ),
79     "Week": Converter(
80         "Week",
81         "time",
82         lambda w: (w * constants.SECONDS_PER_WEEK),
83         lambda s: (s / constants.SECONDS_PER_WEEK),
84         "w",
85     ),
86     "Fahrenheit": Converter(
87         "Fahrenheit",
88         "temperature",
89         lambda f: (f - 32.0) * 0.55555555,
90         lambda c: c * 1.8 + 32.0,
91         "°F",
92     ),
93     "Celsius": Converter("Celsius", "temperature", lambda c: c, lambda c: c, "°C"),
94     "Kelvin": Converter(
95         "Kelvin",
96         "temperature",
97         lambda k: k - 273.15,
98         lambda c: c + 273.15,
99         "°K",
100     ),
101 }
102
103
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)
112
113
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)
118
119
120 def sec_to_min(s: float) -> float:
121     """
122     Convert seconds into minutes.
123
124     >>> sec_to_min(120)
125     2.0
126     """
127     return convert(s, "Second", "Minute")
128
129
130 def sec_to_hour(s: float) -> float:
131     """
132     Convert seconds into hours.
133
134     >>> sec_to_hour(1800)
135     0.5
136     """
137     return convert(s, "Second", "Hour")
138
139
140 def sec_to_day(s: float) -> float:
141     """
142     Convert seconds into days.
143
144     >>> sec_to_day(1800)
145     0.020833333333333332
146     """
147     return convert(s, "Second", "Day")
148
149
150 def sec_to_week(s: float) -> float:
151     """
152     Convert seconds into weeks.
153
154     >>> sec_to_week(1800)
155     0.002976190476190476
156     """
157     return convert(s, "Second", "Week")
158
159
160 def min_to_sec(m: float) -> float:
161     """
162     Convert minutes into seconds.
163
164     >>> min_to_sec(5)
165     300.0
166     """
167     return convert(m, "Minute", "Second")
168
169
170 def min_to_hour(m: float) -> float:
171     """
172     Convert minutes into hours.
173
174     >>> min_to_hour(120)
175     2.0
176     """
177     return convert(m, "Minute", "Hour")
178
179
180 def min_to_day(m: float) -> float:
181     """
182     Convert minutes into days.
183
184     >>> min_to_day(60 * 12)
185     0.5
186     """
187     return convert(m, "Minute", "Day")
188
189
190 def min_to_week(m: float) -> float:
191     """
192     Convert minutes into weeks.
193
194     >>> min_to_week(60 * 24 * 3)
195     0.42857142857142855
196     """
197     return convert(m, "Minute", "Week")
198
199
200 def hour_to_sec(h: float) -> float:
201     """
202     Convert hours into seconds.
203
204     >>> hour_to_sec(1)
205     3600.0
206     """
207     return convert(h, "Hour", "Second")
208
209
210 def hour_to_min(h: float) -> float:
211     """
212     Convert hours into minutes.
213
214     >>> hour_to_min(2)
215     120.0
216     """
217     return convert(h, "Hour", "Minute")
218
219
220 def hour_to_day(h: float) -> float:
221     """
222     Convert hours into days.
223
224     >>> hour_to_day(36)
225     1.5
226     """
227     return convert(h, "Hour", "Day")
228
229
230 def hour_to_week(h: float) -> float:
231     """
232     Convert hours into weeks.
233
234     >>> hour_to_week(24)
235     0.14285714285714285
236     """
237     return convert(h, "Hour", "Week")
238
239
240 def day_to_sec(d: float) -> float:
241     """
242     Convert days into seconds.
243
244     >>> day_to_sec(1)
245     86400.0
246     """
247     return convert(d, "Day", "Second")
248
249
250 def day_to_min(d: float) -> float:
251     """
252     Convert days into minutes.
253
254     >>> day_to_min(0.1)
255     144.0
256     """
257     return convert(d, "Day", "Minute")
258
259
260 def day_to_hour(d: float) -> float:
261     """
262     Convert days into hours.
263
264     >>> day_to_hour(1)
265     24.0
266     """
267     return convert(d, "Day", "Hour")
268
269
270 def day_to_week(d: float) -> float:
271     """
272     Convert days into weeks.
273
274     >>> day_to_week(14)
275     2.0
276     """
277     return convert(d, "Day", "Week")
278
279
280 def week_to_sec(w: float) -> float:
281     """
282     Convert weeks into seconds.
283
284     >>> week_to_sec(10)
285     6048000.0
286     """
287     return convert(w, "Week", "Second")
288
289
290 def week_to_min(w: float) -> float:
291     """
292     Convert weeks into minutes.
293
294     >>> week_to_min(1)
295     10080.0
296     """
297     return convert(w, "Week", "Minute")
298
299
300 def week_to_hour(w: float) -> float:
301     """
302     Convert weeks into hours.
303
304     >>> week_to_hour(1)
305     168.0
306     """
307     return convert(w, "Week", "Hour")
308
309
310 def week_to_day(w: float) -> float:
311     """
312     Convert weeks into days.
313
314     >>> week_to_day(1)
315     7.0
316     """
317     return convert(w, "Week", "Day")
318
319
320 def f_to_c(temp_f: float) -> float:
321     """
322     Convert Fahrenheit into Celsius.
323
324     >>> f_to_c(32.0)
325     0.0
326     """
327     return convert(temp_f, "Fahrenheit", "Celsius")
328
329
330 def c_to_f(temp_c: float) -> float:
331     """
332     Convert Celsius to Fahrenheit.
333
334     >>> c_to_f(0.0)
335     32.0
336     """
337     return convert(temp_c, "Celsius", "Fahrenheit")
338
339
340 if __name__ == '__main__':
341     import doctest
342
343     doctest.testmod()