Easier and more self documenting patterns for loading/saving Persistent
[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         suffix: str,
38     ) -> None:
39         """Construct a converter.
40
41         Args:
42             name: the unit name
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.
49         """
50
51         self.name = name
52         self.category = category
53         self.to_canonical_f = to_canonical
54         self.from_canonical_f = from_canonical
55         self.suffix = suffix
56
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)
61
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)
66
67     def unit_suffix(self) -> str:
68         """Get this unit's suffix abbreviation."""
69         return self.suffix
70
71
72 # A catalog of converters.
73 conversion_catalog = {
74     "Second": Converter("Second", "time", lambda s: s, lambda s: s, "s"),
75     "Minute": Converter(
76         "Minute",
77         "time",
78         lambda m: (m * constants.SECONDS_PER_MINUTE),
79         lambda s: (s / constants.SECONDS_PER_MINUTE),
80         "m",
81     ),
82     "Hour": Converter(
83         "Hour",
84         "time",
85         lambda h: (h * constants.SECONDS_PER_HOUR),
86         lambda s: (s / constants.SECONDS_PER_HOUR),
87         "h",
88     ),
89     "Day": Converter(
90         "Day",
91         "time",
92         lambda d: (d * constants.SECONDS_PER_DAY),
93         lambda s: (s / constants.SECONDS_PER_DAY),
94         "d",
95     ),
96     "Week": Converter(
97         "Week",
98         "time",
99         lambda w: (w * constants.SECONDS_PER_WEEK),
100         lambda s: (s / constants.SECONDS_PER_WEEK),
101         "w",
102     ),
103     "Fahrenheit": Converter(
104         "Fahrenheit",
105         "temperature",
106         lambda f: (f - 32.0) * 0.55555555,
107         lambda c: c * 1.8 + 32.0,
108         "°F",
109     ),
110     "Celsius": Converter("Celsius", "temperature", lambda c: c, lambda c: c, "°C"),
111     "Kelvin": Converter(
112         "Kelvin",
113         "temperature",
114         lambda k: k - 273.15,
115         lambda c: c + 273.15,
116         "°K",
117     ),
118 }
119
120
121 def convert(magnitude: SupportsFloat, from_thing: str, to_thing: str) -> float:
122     """Convert between units using the internal catalog.
123
124     Args:
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
128
129     Returns:
130         The converted magnitude.  Raises on error.
131     """
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)
139
140
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)
146
147
148 def sec_to_min(s: float) -> float:
149     """
150     Convert seconds into minutes.
151
152     >>> sec_to_min(120)
153     2.0
154     """
155     return convert(s, "Second", "Minute")
156
157
158 def sec_to_hour(s: float) -> float:
159     """
160     Convert seconds into hours.
161
162     >>> sec_to_hour(1800)
163     0.5
164     """
165     return convert(s, "Second", "Hour")
166
167
168 def sec_to_day(s: float) -> float:
169     """
170     Convert seconds into days.
171
172     >>> sec_to_day(1800)
173     0.020833333333333332
174     """
175     return convert(s, "Second", "Day")
176
177
178 def sec_to_week(s: float) -> float:
179     """
180     Convert seconds into weeks.
181
182     >>> sec_to_week(1800)
183     0.002976190476190476
184     """
185     return convert(s, "Second", "Week")
186
187
188 def min_to_sec(m: float) -> float:
189     """
190     Convert minutes into seconds.
191
192     >>> min_to_sec(5)
193     300.0
194     """
195     return convert(m, "Minute", "Second")
196
197
198 def min_to_hour(m: float) -> float:
199     """
200     Convert minutes into hours.
201
202     >>> min_to_hour(120)
203     2.0
204     """
205     return convert(m, "Minute", "Hour")
206
207
208 def min_to_day(m: float) -> float:
209     """
210     Convert minutes into days.
211
212     >>> min_to_day(60 * 12)
213     0.5
214     """
215     return convert(m, "Minute", "Day")
216
217
218 def min_to_week(m: float) -> float:
219     """
220     Convert minutes into weeks.
221
222     >>> min_to_week(60 * 24 * 3)
223     0.42857142857142855
224     """
225     return convert(m, "Minute", "Week")
226
227
228 def hour_to_sec(h: float) -> float:
229     """
230     Convert hours into seconds.
231
232     >>> hour_to_sec(1)
233     3600.0
234     """
235     return convert(h, "Hour", "Second")
236
237
238 def hour_to_min(h: float) -> float:
239     """
240     Convert hours into minutes.
241
242     >>> hour_to_min(2)
243     120.0
244     """
245     return convert(h, "Hour", "Minute")
246
247
248 def hour_to_day(h: float) -> float:
249     """
250     Convert hours into days.
251
252     >>> hour_to_day(36)
253     1.5
254     """
255     return convert(h, "Hour", "Day")
256
257
258 def hour_to_week(h: float) -> float:
259     """
260     Convert hours into weeks.
261
262     >>> hour_to_week(24)
263     0.14285714285714285
264     """
265     return convert(h, "Hour", "Week")
266
267
268 def day_to_sec(d: float) -> float:
269     """
270     Convert days into seconds.
271
272     >>> day_to_sec(1)
273     86400.0
274     """
275     return convert(d, "Day", "Second")
276
277
278 def day_to_min(d: float) -> float:
279     """
280     Convert days into minutes.
281
282     >>> day_to_min(0.1)
283     144.0
284     """
285     return convert(d, "Day", "Minute")
286
287
288 def day_to_hour(d: float) -> float:
289     """
290     Convert days into hours.
291
292     >>> day_to_hour(1)
293     24.0
294     """
295     return convert(d, "Day", "Hour")
296
297
298 def day_to_week(d: float) -> float:
299     """
300     Convert days into weeks.
301
302     >>> day_to_week(14)
303     2.0
304     """
305     return convert(d, "Day", "Week")
306
307
308 def week_to_sec(w: float) -> float:
309     """
310     Convert weeks into seconds.
311
312     >>> week_to_sec(10)
313     6048000.0
314     """
315     return convert(w, "Week", "Second")
316
317
318 def week_to_min(w: float) -> float:
319     """
320     Convert weeks into minutes.
321
322     >>> week_to_min(1)
323     10080.0
324     """
325     return convert(w, "Week", "Minute")
326
327
328 def week_to_hour(w: float) -> float:
329     """
330     Convert weeks into hours.
331
332     >>> week_to_hour(1)
333     168.0
334     """
335     return convert(w, "Week", "Hour")
336
337
338 def week_to_day(w: float) -> float:
339     """
340     Convert weeks into days.
341
342     >>> week_to_day(1)
343     7.0
344     """
345     return convert(w, "Week", "Day")
346
347
348 def f_to_c(temp_f: float) -> float:
349     """
350     Convert Fahrenheit into Celsius.
351
352     >>> f_to_c(32.0)
353     0.0
354     """
355     return convert(temp_f, "Fahrenheit", "Celsius")
356
357
358 def c_to_f(temp_c: float) -> float:
359     """
360     Convert Celsius to Fahrenheit.
361
362     >>> c_to_f(0.0)
363     32.0
364     """
365     return convert(temp_c, "Celsius", "Fahrenheit")
366
367
368 if __name__ == '__main__':
369     import doctest
370
371     doctest.testmod()