mypy clean!
[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(
110     magnitude: SupportsFloat, from_unit: Converter, to_unit: Converter
111 ) -> float:
112     canonical = from_unit.to_canonical(magnitude)
113     converted = to_unit.from_canonical(canonical)
114     return float(converted)
115
116
117 def sec_to_min(s: float) -> float:
118     """
119     Convert seconds into minutes.
120
121     >>> sec_to_min(120)
122     2.0
123     """
124     return convert(s, "Second", "Minute")
125
126
127 def sec_to_hour(s: float) -> float:
128     """
129     Convert seconds into hours.
130
131     >>> sec_to_hour(1800)
132     0.5
133     """
134     return convert(s, "Second", "Hour")
135
136
137 def sec_to_day(s: float) -> float:
138     """
139     Convert seconds into days.
140
141     >>> sec_to_day(1800)
142     0.020833333333333332
143     """
144     return convert(s, "Second", "Day")
145
146
147 def sec_to_week(s: float) -> float:
148     """
149     Convert seconds into weeks.
150
151     >>> sec_to_week(1800)
152     0.002976190476190476
153     """
154     return convert(s, "Second", "Week")
155
156
157 def min_to_sec(m: float) -> float:
158     """
159     Convert minutes into seconds.
160
161     >>> min_to_sec(5)
162     300.0
163     """
164     return convert(m, "Minute", "Second")
165
166
167 def min_to_hour(m: float) -> float:
168     """
169     Convert minutes into hours.
170
171     >>> min_to_hour(120)
172     2.0
173     """
174     return convert(m, "Minute", "Hour")
175
176
177 def min_to_day(m: float) -> float:
178     """
179     Convert minutes into days.
180
181     >>> min_to_day(60 * 12)
182     0.5
183     """
184     return convert(m, "Minute", "Day")
185
186
187 def min_to_week(m: float) -> float:
188     """
189     Convert minutes into weeks.
190
191     >>> min_to_week(60 * 24 * 3)
192     0.42857142857142855
193     """
194     return convert(m, "Minute", "Week")
195
196
197 def hour_to_sec(h: float) -> float:
198     """
199     Convert hours into seconds.
200
201     >>> hour_to_sec(1)
202     3600.0
203     """
204     return convert(h, "Hour", "Second")
205
206
207 def hour_to_min(h: float) -> float:
208     """
209     Convert hours into minutes.
210
211     >>> hour_to_min(2)
212     120.0
213     """
214     return convert(h, "Hour", "Minute")
215
216
217 def hour_to_day(h: float) -> float:
218     """
219     Convert hours into days.
220
221     >>> hour_to_day(36)
222     1.5
223     """
224     return convert(h, "Hour", "Day")
225
226
227 def hour_to_week(h: float) -> float:
228     """
229     Convert hours into weeks.
230
231     >>> hour_to_week(24)
232     0.14285714285714285
233     """
234     return convert(h, "Hour", "Week")
235
236
237 def day_to_sec(d: float) -> float:
238     """
239     Convert days into seconds.
240
241     >>> day_to_sec(1)
242     86400.0
243     """
244     return convert(d, "Day", "Second")
245
246
247 def day_to_min(d: float) -> float:
248     """
249     Convert days into minutes.
250
251     >>> day_to_min(0.1)
252     144.0
253     """
254     return convert(d, "Day", "Minute")
255
256
257 def day_to_hour(d: float) -> float:
258     """
259     Convert days into hours.
260
261     >>> day_to_hour(1)
262     24.0
263     """
264     return convert(d, "Day", "Hour")
265
266
267 def day_to_week(d: float) -> float:
268     """
269     Convert days into weeks.
270
271     >>> day_to_week(14)
272     2.0
273     """
274     return convert(d, "Day", "Week")
275
276
277 def week_to_sec(w: float) -> float:
278     """
279     Convert weeks into seconds.
280
281     >>> week_to_sec(10)
282     6048000.0
283     """
284     return convert(w, "Week", "Second")
285
286
287 def week_to_min(w: float) -> float:
288     """
289     Convert weeks into minutes.
290
291     >>> week_to_min(1)
292     10080.0
293     """
294     return convert(w, "Week", "Minute")
295
296
297 def week_to_hour(w: float) -> float:
298     """
299     Convert weeks into hours.
300
301     >>> week_to_hour(1)
302     168.0
303     """
304     return convert(w, "Week", "Hour")
305
306
307 def week_to_day(w: float) -> float:
308     """
309     Convert weeks into days.
310
311     >>> week_to_day(1)
312     7.0
313     """
314     return convert(w, "Week", "Day")
315
316
317 def f_to_c(temp_f: float) -> float:
318     """
319     Convert Fahrenheit into Celsius.
320
321     >>> f_to_c(32.0)
322     0.0
323     """
324     return convert(temp_f, "Fahrenheit", "Celsius")
325
326
327 def c_to_f(temp_c: float) -> float:
328     """
329     Convert Celsius to Fahrenheit.
330
331     >>> c_to_f(0.0)
332     32.0
333     """
334     return convert(temp_c, "Celsius", "Fahrenheit")
335
336
337 if __name__ == '__main__':
338     import doctest
339
340     doctest.testmod()