Change settings in flake8 and black.
[python_utils.git] / conversion_utils.py
1 #!/usr/bin/env python3
2
3 from typing import Callable, SupportsFloat
4
5 import constants
6
7
8 class Converter(object):
9     """A converter has a canonical name and a category.  The name defines
10     a unit of measurement in a category or class of measurements.
11     This framework will allow conversion between named units in the
12     same category.  e.g. name may be "meter", "inch", "mile" and their
13     category may be "length".
14
15     The way that conversions work is that we convert the magnitude
16     first to a canonical unit and then (if needed) from the canonical
17     unit to the desired destination unit.  e.g. if "meter" is the
18     canonical unit of measurement for the category "length", in order
19     to convert miles into inches we first convert miles into meters
20     and, from there, meters into inches.  This is potentially
21     dangerous because it requires two floating point operations which
22     each have the potential to overflow, underflow, or introduce
23     floating point errors.  Caveat emptor.
24     """
25
26     def __init__(
27         self,
28         name: str,
29         category: str,
30         to_canonical: Callable,  # convert to canonical unit
31         from_canonical: Callable,  # convert from canonical unit
32         unit: str,
33     ) -> None:
34         self.name = name
35         self.category = category
36         self.to_canonical_f = to_canonical
37         self.from_canonical_f = from_canonical
38         self.unit = unit
39
40     def to_canonical(self, n: SupportsFloat) -> SupportsFloat:
41         return self.to_canonical_f(n)
42
43     def from_canonical(self, n: SupportsFloat) -> SupportsFloat:
44         return self.from_canonical_f(n)
45
46     def unit_suffix(self) -> str:
47         return self.unit
48
49
50 # A catalog of converters.
51 conversion_catalog = {
52     "Second": Converter("Second", "time", lambda s: s, lambda s: s, "s"),
53     "Minute": Converter(
54         "Minute",
55         "time",
56         lambda m: (m * constants.SECONDS_PER_MINUTE),
57         lambda s: (s / constants.SECONDS_PER_MINUTE),
58         "m",
59     ),
60     "Hour": Converter(
61         "Hour",
62         "time",
63         lambda h: (h * constants.SECONDS_PER_HOUR),
64         lambda s: (s / constants.SECONDS_PER_HOUR),
65         "h",
66     ),
67     "Day": Converter(
68         "Day",
69         "time",
70         lambda d: (d * constants.SECONDS_PER_DAY),
71         lambda s: (s / constants.SECONDS_PER_DAY),
72         "d",
73     ),
74     "Week": Converter(
75         "Week",
76         "time",
77         lambda w: (w * constants.SECONDS_PER_WEEK),
78         lambda s: (s / constants.SECONDS_PER_WEEK),
79         "w",
80     ),
81     "Fahrenheit": Converter(
82         "Fahrenheit",
83         "temperature",
84         lambda f: (f - 32.0) * 0.55555555,
85         lambda c: c * 1.8 + 32.0,
86         "°F",
87     ),
88     "Celsius": Converter("Celsius", "temperature", lambda c: c, lambda c: c, "°C"),
89     "Kelvin": Converter(
90         "Kelvin",
91         "temperature",
92         lambda k: k - 273.15,
93         lambda c: c + 273.15,
94         "°K",
95     ),
96 }
97
98
99 def convert(magnitude: SupportsFloat, from_thing: str, to_thing: str) -> float:
100     src = conversion_catalog.get(from_thing, None)
101     dst = conversion_catalog.get(to_thing, None)
102     if src is None or dst is None:
103         raise ValueError("No known conversion")
104     if src.category != dst.category:
105         raise ValueError("Incompatible conversion")
106     return _convert(magnitude, src, dst)
107
108
109 def _convert(magnitude: SupportsFloat, from_unit: Converter, to_unit: Converter) -> float:
110     canonical = from_unit.to_canonical(magnitude)
111     converted = to_unit.from_canonical(canonical)
112     return float(converted)
113
114
115 def sec_to_min(s: float) -> float:
116     """
117     Convert seconds into minutes.
118
119     >>> sec_to_min(120)
120     2.0
121     """
122     return convert(s, "Second", "Minute")
123
124
125 def sec_to_hour(s: float) -> float:
126     """
127     Convert seconds into hours.
128
129     >>> sec_to_hour(1800)
130     0.5
131     """
132     return convert(s, "Second", "Hour")
133
134
135 def sec_to_day(s: float) -> float:
136     """
137     Convert seconds into days.
138
139     >>> sec_to_day(1800)
140     0.020833333333333332
141     """
142     return convert(s, "Second", "Day")
143
144
145 def sec_to_week(s: float) -> float:
146     """
147     Convert seconds into weeks.
148
149     >>> sec_to_week(1800)
150     0.002976190476190476
151     """
152     return convert(s, "Second", "Week")
153
154
155 def min_to_sec(m: float) -> float:
156     """
157     Convert minutes into seconds.
158
159     >>> min_to_sec(5)
160     300.0
161     """
162     return convert(m, "Minute", "Second")
163
164
165 def min_to_hour(m: float) -> float:
166     """
167     Convert minutes into hours.
168
169     >>> min_to_hour(120)
170     2.0
171     """
172     return convert(m, "Minute", "Hour")
173
174
175 def min_to_day(m: float) -> float:
176     """
177     Convert minutes into days.
178
179     >>> min_to_day(60 * 12)
180     0.5
181     """
182     return convert(m, "Minute", "Day")
183
184
185 def min_to_week(m: float) -> float:
186     """
187     Convert minutes into weeks.
188
189     >>> min_to_week(60 * 24 * 3)
190     0.42857142857142855
191     """
192     return convert(m, "Minute", "Week")
193
194
195 def hour_to_sec(h: float) -> float:
196     """
197     Convert hours into seconds.
198
199     >>> hour_to_sec(1)
200     3600.0
201     """
202     return convert(h, "Hour", "Second")
203
204
205 def hour_to_min(h: float) -> float:
206     """
207     Convert hours into minutes.
208
209     >>> hour_to_min(2)
210     120.0
211     """
212     return convert(h, "Hour", "Minute")
213
214
215 def hour_to_day(h: float) -> float:
216     """
217     Convert hours into days.
218
219     >>> hour_to_day(36)
220     1.5
221     """
222     return convert(h, "Hour", "Day")
223
224
225 def hour_to_week(h: float) -> float:
226     """
227     Convert hours into weeks.
228
229     >>> hour_to_week(24)
230     0.14285714285714285
231     """
232     return convert(h, "Hour", "Week")
233
234
235 def day_to_sec(d: float) -> float:
236     """
237     Convert days into seconds.
238
239     >>> day_to_sec(1)
240     86400.0
241     """
242     return convert(d, "Day", "Second")
243
244
245 def day_to_min(d: float) -> float:
246     """
247     Convert days into minutes.
248
249     >>> day_to_min(0.1)
250     144.0
251     """
252     return convert(d, "Day", "Minute")
253
254
255 def day_to_hour(d: float) -> float:
256     """
257     Convert days into hours.
258
259     >>> day_to_hour(1)
260     24.0
261     """
262     return convert(d, "Day", "Hour")
263
264
265 def day_to_week(d: float) -> float:
266     """
267     Convert days into weeks.
268
269     >>> day_to_week(14)
270     2.0
271     """
272     return convert(d, "Day", "Week")
273
274
275 def week_to_sec(w: float) -> float:
276     """
277     Convert weeks into seconds.
278
279     >>> week_to_sec(10)
280     6048000.0
281     """
282     return convert(w, "Week", "Second")
283
284
285 def week_to_min(w: float) -> float:
286     """
287     Convert weeks into minutes.
288
289     >>> week_to_min(1)
290     10080.0
291     """
292     return convert(w, "Week", "Minute")
293
294
295 def week_to_hour(w: float) -> float:
296     """
297     Convert weeks into hours.
298
299     >>> week_to_hour(1)
300     168.0
301     """
302     return convert(w, "Week", "Hour")
303
304
305 def week_to_day(w: float) -> float:
306     """
307     Convert weeks into days.
308
309     >>> week_to_day(1)
310     7.0
311     """
312     return convert(w, "Week", "Day")
313
314
315 def f_to_c(temp_f: float) -> float:
316     """
317     Convert Fahrenheit into Celsius.
318
319     >>> f_to_c(32.0)
320     0.0
321     """
322     return convert(temp_f, "Fahrenheit", "Celsius")
323
324
325 def c_to_f(temp_c: float) -> float:
326     """
327     Convert Celsius to Fahrenheit.
328
329     >>> c_to_f(0.0)
330     32.0
331     """
332     return convert(temp_c, "Celsius", "Fahrenheit")
333
334
335 if __name__ == '__main__':
336     import doctest
337
338     doctest.testmod()