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