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