Ran black code formatter on everything.
[python_utils.git] / conversion_utils.py
1 #!/usr/bin/env python3
2
3 from numbers import Number
4 from typing import Callable
5
6 import constants
7
8
9 class Converter(object):
10     """A converter has a canonical name and a category.  The name defines
11     a unit of measurement in a category or class of measurements.
12     This framework will allow conversion between named units in the
13     same category.  e.g. name may be "meter", "inch", "mile" and their
14     category may be "length".
15
16     The way that conversions work is that we convert the magnitude
17     first to a canonical unit and then (if needed) from the canonical
18     unit to the desired destination unit.  e.g. if "meter" is the
19     canonical unit of measurement for the category "length", in order
20     to convert miles into inches we first convert miles into meters
21     and, from there, meters into inches.  This is potentially
22     dangerous because it requires two floating point operations which
23     each have the potential to overflow, underflow, or introduce
24     floating point errors.  Caveat emptor.
25     """
26
27     def __init__(
28         self,
29         name: str,
30         category: str,
31         to_canonical: Callable,  # convert to canonical unit
32         from_canonical: Callable,  # convert from canonical unit
33         unit: str,
34     ) -> None:
35         self.name = name
36         self.category = category
37         self.to_canonical_f = to_canonical
38         self.from_canonical_f = from_canonical
39         self.unit = unit
40
41     def to_canonical(self, n: Number) -> Number:
42         return self.to_canonical_f(n)
43
44     def from_canonical(self, n: Number) -> Number:
45         return self.from_canonical_f(n)
46
47     def unit_suffix(self) -> str:
48         return self.unit
49
50
51 # A catalog of converters.
52 conversion_catalog = {
53     "Second": Converter("Second", "time", lambda s: s, lambda s: s, "s"),
54     "Minute": Converter(
55         "Minute",
56         "time",
57         lambda m: (m * constants.SECONDS_PER_MINUTE),
58         lambda s: (s / constants.SECONDS_PER_MINUTE),
59         "m",
60     ),
61     "Hour": Converter(
62         "Hour",
63         "time",
64         lambda h: (h * constants.SECONDS_PER_HOUR),
65         lambda s: (s / constants.SECONDS_PER_HOUR),
66         "h",
67     ),
68     "Day": Converter(
69         "Day",
70         "time",
71         lambda d: (d * constants.SECONDS_PER_DAY),
72         lambda s: (s / constants.SECONDS_PER_DAY),
73         "d",
74     ),
75     "Week": Converter(
76         "Week",
77         "time",
78         lambda w: (w * constants.SECONDS_PER_WEEK),
79         lambda s: (s / constants.SECONDS_PER_WEEK),
80         "w",
81     ),
82     "Fahrenheit": Converter(
83         "Fahrenheit",
84         "temperature",
85         lambda f: (f - 32.0) * 0.55555555,
86         lambda c: c * 1.8 + 32.0,
87         "°F",
88     ),
89     "Celsius": Converter(
90         "Celsius", "temperature", lambda c: c, lambda c: c, "°C"
91     ),
92     "Kelvin": Converter(
93         "Kelvin",
94         "temperature",
95         lambda k: k - 273.15,
96         lambda c: c + 273.15,
97         "°K",
98     ),
99 }
100
101
102 def convert(magnitude: Number, from_thing: str, to_thing: str) -> float:
103     src = conversion_catalog.get(from_thing, None)
104     dst = conversion_catalog.get(to_thing, None)
105     if src is None or dst is None:
106         raise ValueError("No known conversion")
107     if src.category != dst.category:
108         raise ValueError("Incompatible conversion")
109     return _convert(magnitude, src, dst)
110
111
112 def _convert(
113     magnitude: Number, from_unit: Converter, to_unit: Converter
114 ) -> 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()