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