1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 """
23 firkin is a python module to convert between different measurement
24 units.
25
26
27 Usage
28 =====
29
30 First we create an instance of L{UnitManager}:
31
32 >>> um=UnitManager()
33
34 Next we create two families of units. The first one ist C{liter} and
35 uses L{SIFamily} to automatically create units with the SI prefixes:
36
37 >>> um.add(SIFamily(base='l', name='liter'))
38
39 Now our L{UnitManager} knows about fl, pl, nl, ..., Ml, Gl, Tl.
40
41 How many liters are 10000 ml?
42
43 >>> um.convert_to_unit(1e4, 'ml', 'l')
44 (Decimal("10.0000"), 'l')
45
46 Next we create a family by hand:
47
48 >>> f=Family(name='f', base='gallon')
49 >>> f.add('barrel', 36, 'gallon')
50 >>> f.add('kilderkin', 0.5, 'barrel')
51 >>> f.add('firkin', 0.5, 'kilderkin')
52
53 Now we have a family called C{f} that uses gallon as its base and knows about
54 barrel, kilderkin and firkin, too.
55
56 How much gallons is one firkin?
57
58 >>> f.convert(1, 'firkin', 'gallon')
59 (Decimal("9.00"), 'gallon')
60
61 What's the best way to express 3 kilderkin?
62
63 >>> f.autoconvert(3, 'kilderkin')
64 (Decimal("1.50"), 'barrel')
65
66 To convert between family C{f} and family C{liter} we need to add C{f} to
67 our L{UnitManager} and tell how much liters (base unit of family C{liter}) a
68 gallon (base unit of family C{f}) is:
69
70 >>> um.add(f, other='liter', factor=4.54609)
71
72 Of course the L{UnitManager} can convert firkin to gallon, too:
73
74 >>> um.convert_to_unit(1, 'firkin', 'gallon')
75 (Decimal("9.00"), 'gallon')
76
77 But it also can convert firkin to liters:
78
79 >>> um.convert_to_unit(1, 'firkin', 'l')
80 (Decimal("40.9148100"), 'l')
81
82 Or find the best way to express one liter in one of the units from
83 family C{f}:
84
85 >>> um.convert_to_family(1, 'l', 'f')
86 (Decimal("0.219969248299"), 'gallon')
87
88 That works with barrels, too:
89
90 >>> um.convert_to_family(1, 'barrel', 'f')
91 (Decimal("1.00"), 'barrel')
92
93 A classical example: How to convert between °C and °F?
94
95 We need a family for °C:
96
97 >>> um.add(Family(base='°C'))
98
99 and for °F.
100
101 °F is (°C-32)/1.8:
102
103 >>> um.add(Family(base='°F'), other='°C', offset=(-32/1.8), factor=5.0/9)
104
105 Now we can convert:
106
107 >>> um.convert_to_unit(32, '°F', '°C')
108 (Decimal("-8E-12"), '°C')
109 >>> um.convert_to_unit(100, '°C', '°F')
110 (Decimal("212.0"), '°F')
111
112 Converting between °C and ml isn't useful:
113
114 >>> um.convert_to_unit(1, '°C', 'ml')
115 (None, None)
116
117 @warning: firkin is alpha software. So far it seems to work for me but it may
118 have severe bugs I didn't noticed yet. Use it at your own risk.
119
120 Firkin is still under development and the API may change in the future.
121
122
123
124 """
125
126
127 from decimal import Decimal
128
130 """
131 Convert I{value} to decimal.Decimal
132
133 @type value: float or anything that can be used as argument for
134 Decimal()
135 """
136 if isinstance(value, float):
137 value=Decimal(str(value))
138 elif not isinstance(value, Decimal):
139 value=Decimal(value)
140 return value
141
142
144 """
145 A I{Family} is a collection of units which are derived
146 from a common base unit.
147
148 >>> f=Family(base='gallon')
149 >>> f.add('barrel', 36, 'gallon')
150 >>> f.add('kilderkin', 0.5, 'barrel')
151 >>> f.add('firkin', 0.5, 'kilderkin')
152 >>> f.convert(1, 'firkin', 'gallon')
153 (Decimal("9.00"), 'gallon')
154 >>> f.autoconvert(3, 'kilderkin')
155 (Decimal("1.50"), 'barrel')
156 """
158 """
159 Constructor
160
161 @param base: common base unit
162 @type base: string
163 @param name: name for this family. That name can be used with
164 L{UnitManager} to refer to this family.
165 If C{None} I{base} is used as name.
166 @type name: string
167 """
168 self.base=base
169 if name is None:
170 name=base
171 self.name=name
172 self.units={base: Decimal(1)}
173
174
175 - def add(self, name, factor, other=None):
176 """
177 Add another unit to this family
178
179 @param name: The unit's name
180 @type name: string
181 @param factor: factor to multiply I{other} to get this unit
182 @type factor: anything that L{to_decimal} can use
183 @param other: Unit this one is based on. If C{None} the base
184 unit is used
185 @type other: string
186
187 @raises KeyError: if I{other} isn't known
188 @raises TypeError: if I{factor} has a wrong type
189 """
190 if other is None:
191 other=self.base
192 self[name]=self[other]*to_decimal(factor)
193
194
195 - def convert(self, amount, unit, dest):
196 """
197 Convert I{amount} of I{unit} to unit I{dest}
198
199 @type amount: anything that L{to_decimal} can use
200 @type unit: string
201 @type dest: string
202
203 @raises KeyError: if I{unit} or I{other} isn't known
204 @raises TypeError: if I{amount} has a wrong type
205
206 @return: a tupel (new amount, new unit)
207 """
208 amount=to_decimal(amount)
209 return amount*self[unit]/self[dest], dest
210
211
213 """
214 Convert I{amount} of I{unit} to the unit that fits best.
215
216 @type amount: anything that L{to_decimal} can use
217 @type unit: string
218
219 @raises KeyError: if I{unit} isn't known
220 @raises TypeError: if I{amount} has a wrong type
221
222 @return: a tupel (new amount, new unit)
223
224 """
225 amount=to_decimal(amount)
226 units=sorted(self.units, key=lambda x: self.units[x])
227 for i in range(0, len(units)-1):
228 amount, unit=self.convert(amount, unit, units[i])
229 if amount < self[units[i+1]] / self[units[i]]:
230 break
231 else:
232 amount, unit=self.convert(amount, unit, units[-1])
233 return amount, unit
234
235
237 """
238 C{item in self}
239
240 True if item is a known unit
241 @type item: string
242 """
243 return item in self.units
244
246 """
247 C{self[index]}
248
249 Get the factor you need to mutiply the base unit with to get
250 unit I{index}
251 @type index: string
252
253 @raise KeyError: if unit I{index} is not known
254 """
255 return self.units[index]
256
258 """
259 C{self[index]=foo}
260
261 Set I{value} as the factor you need to mutiply the base unit
262 with to get unit I{index}
263 @type index: string
264 @type value: anything that L{to_decimal} can use
265 """
266 self.units[index]=value
267
268
269
271 """
272 I{Family} that uses SI prefixes:
273
274 - I{y} (yokto) = 10e-24
275 - I{z} (zepto) = 10e-21
276 - I{a} (atto) = 10e-18
277 - I{f} (femto) = 10e-15
278 - I{p} (pico) = 10e-12
279 - I{n} (nano) = 10e-9
280 - I{µ} (micro) = 10e-6
281 - I{m} (milli) = 10e-3
282 - I{k} (kilo) = 10e3
283 - I{M} (mega) = 10e6
284 - I{G} (giga) = 10e9
285 - I{T} (tera) = 10e12
286 - I{P} (peta) = 10e15
287 - I{E} (exa) = 10e18
288 - I{Z} (zeta) = 10e21
289 - I{Y} (yotta) = 10e24
290
291 If I{extended} is C{True} the folowing prefixes are added:
292 - I{c} (centi) = 10e-2
293 - I{d} (dezi) = 10e-1
294 - I{da} (deka) = 10e1
295 - I{h} (hekto) = 10e2
296
297 For every prefix a unit is created.
298
299 >>> fam=SIFamily('m')
300 >>> fam.convert(1e8, 'mm', 'km')
301 (Decimal("100.000"), 'km')
302 >>> fam.autoconvert(3.65e-4, 'km')
303 (Decimal("365.0000"), 'mm')
304
305
306 """
307 - def __init__(self, base, name=None, extended=False):
308 super(SIFamily, self).__init__(base, name)
309 factor=1000.0
310 for i in ('k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'):
311 self.add('%s%s'%(i, base), factor)
312 factor=factor*1000
313 factor=1/1000.0
314 for i in ('m', 'µ', 'n', 'p', 'f', 'a', 'z', 'y'):
315 self.add('%s%s'%(i, base), factor)
316 factor=factor/1000
317 if extended:
318 self.add('h%s'%base, 100)
319 self.add('da%s'%base, 10)
320 self.add('d%s'%base, 1/10.0)
321 self.add('c%s'%base, 1/100.0)
322
323
324
325
326
328 """
329 Holds an item in L{UnitManager}
330
331 See L{UnitManager.add}
332 """
333 - def __init__(self, family, other, factor=1.0, offset=0.0, groups=None):
334 """
335 I{family}=I{other}*factor+offset
336
337 @type family: L{Family}
338 @type other: L{Family}
339 @type factor: anything that L{to_decimal} can use
340 @type offset: anything that L{to_decimal} can use
341 """
342 self.family=family
343 self.other=other
344 self.factor=to_decimal(factor)
345 self.offset=to_decimal(offset)
346 if groups is None:
347 self.groups = set()
348 elif isinstance(groups, basestring):
349 self.groups = set((groups,))
350 else:
351 try:
352 self.groups = set(groups)
353 except TypeError:
354 self.groups = set((groups, ))
355
356
357
359 if isinstance(self.other, Family):
360 other=self.other.name
361 else:
362 other=self.other
363 return '<Item %s %s %s %s>'%(self.family.name,
364 self.factor,
365 self.offset,
366 other)
367
368
369
371 """
372 A I{UnitManager} hold a collection of L{Family} objects and allows
373 to convert between their units.
374
375 >>> um=UnitManager()
376 >>> um.add(SIFamily(base='l', name='liter'))
377 >>>
378 >>> f=Family(name='f', base='gallon')
379 >>> f.add('barrel', 36, 'gallon')
380 >>> f.add('kilderkin', 0.5, 'barrel')
381 >>> f.add('firkin', 0.5, 'kilderkin')
382 >>> um.add(f, other='liter', factor=4.54609)
383 >>>
384 >>> um.add(Family(base='°C'))
385 >>> um.add(Family(base='°F'), other='°C', offset=(-32/1.8), factor=5.0/9)
386 >>> um.convert_to_unit(1e4, 'ml', 'l')
387 (Decimal("10.0000"), 'l')
388 >>> um.convert_to_unit(1, 'firkin', 'gallon')
389 (Decimal("9.00"), 'gallon')
390 >>> um.convert_to_family(1, 'firkin', 'liter')
391 (Decimal("40.9148100"), 'l')
392 >>> um.convert_to_family(1, 'l', 'f')
393 (Decimal("0.219969248299"), 'gallon')
394 >>> um.convert_to_family(1, 'barrel', 'f')
395 (Decimal("1.00"), 'barrel')
396 >>> um.convert_to_unit(32, '°F', '°C')
397 (Decimal("-8E-12"), '\\xc2\\xb0C')
398 >>> um.convert_to_unit(100, '°C', '°F')
399 (Decimal("212.0"), '\\xc2\\xb0F')
400 >>> um.convert_to_unit(1, '°C', 'ml')
401 (None, None)
402 """
403
405 """
406 Constructor
407 """
408 self.units = {}
409 self.families = {}
410 self.items = {}
411 self.groups = {}
412
413 - def add(self, family, other=None, factor=1, offset=0, groups=None):
414 """
415 Add another L{Family} object.
416
417 If I{other} is not C{None} a conversion path
418
419 I{family} = I{other} * I{factor} + I{offset}
420
421 and its reverse path are added.
422
423 @type family: L{Family}
424 @type other: L{Family} or C{string} (name of an existing family)
425 @type factor: anything that L{to_decimal} can use
426 @type offset: anything that L{to_decimal} can use
427 @type groups: C{string} or C{iterable} of C{strings}
428
429 @raise KeyError: if I{other} is a string and there's no known
430 unit with this name
431
432 @raise TypeError: if I{factor} or I{offset} has a wrong type
433 @raise AttributeError: if I{family} has a wrong type
434 """
435 self.families[family.name]=family
436
437 if other is not None and not isinstance(other, Family):
438 other=self.families[other]
439
440 if not family.name in self.items:
441 self.items[family.name]=[]
442
443 if other is not None:
444 self.items[family.name].append(Item(family=family,
445 other=other,
446 factor=factor,
447 offset=offset,
448 groups=groups)
449 )
450
451 if other.name in self.items:
452 self.items[other.name].append(Item(family=other,
453 other=family,
454 factor=1/factor,
455 offset=-offset/factor,
456 groups=groups)
457 )
458
459 for u in family.units:
460 self.units[u]=family
461
462 if groups is not None:
463 if isinstance(groups, basestring):
464 groups = (groups,)
465 try:
466 for c in groups:
467 try:
468 self.groups[c].append(family)
469 except KeyError:
470 self.groups[c] = [family]
471 except TypeError:
472 try:
473 self.groups[groups].append(family)
474 except KeyError:
475 self.groups[groups] = [family]
476
477
479 """
480 Finds the shortest conversion path between I{start} and I{end}.
481
482 @param start: family to start with
483 @type start: name of a L{Family} that has been added by L{add}
484 @param end: family to end with
485 @type end: name of a L{Family} that has been added by L{add}
486
487 @returns: list of L{Item}s to convert I{start} to I{end} or
488 C{None} if no such path can be found
489 """
490 if start not in self.items or end not in self.items:
491 if start in self.units:
492 start=self.units[start]
493 else:
494 return None
495 if path is None:
496 path=[]
497 else:
498 path=[x for x in path]
499 if start==end:
500 return path
501 shortest=None
502 for node in self.items[start]:
503 if node in path:
504 continue
505 newpath=self.shortest_path(node.other.name, end, path+[node])
506 if newpath is not None:
507 if shortest is None or len(newpath) < len(shortest):
508 shortest=newpath
509 return shortest
510
511
513 """
514 Convert I{amount} of I{unit} to the unit in I{family} that
515 fits best.
516
517 See L{Family.autoconvert}.
518
519 @type amount: anything that L{to_decimal} can use
520 @type unit: C{string} (name of a known unit)
521 @type family: L{Family} or C{string} (name of a known family)
522
523 @return: Tupel (new amount, new unit) or (None, None) if
524 conversion is not possible
525
526 @raise KeyError: if I{unit} is not known
527 @raise KeyError: if I{family} is a string an no family with
528 that name is known
529 @raise TypeError: if I{amount} has a wrong type
530 """
531 sfam=self.units[unit]
532 if isinstance(family, Family):
533 dfam=family
534 else:
535 dfam=self.families[family]
536
537 amount, unit=sfam.convert(amount, unit, sfam.base)
538
539 path=self.shortest_path(sfam.name, dfam.name)
540 if path is None:
541 return None, None
542 elif len(path)==0:
543 return sfam.autoconvert(amount, unit)
544 else:
545 for p in path:
546 amount=amount*p.factor+p.offset
547 return dfam.autoconvert(amount, dfam.base)
548
549
551 """
552 Convert I{amount} of I{unit} to unit I{dest}.
553
554 See L{Family.convert}.
555
556 @type amount: anything that L{to_decimal} can use
557 @type unit: C{string} (name of a known unit)
558 @type dest: C{string} (name of a known unit)
559
560 @return: Tupel (new amount, new unit) or (None, None) if conversion
561 is not possible.
562
563 @raise KeyError: if I{unit} or I{dest} is not known
564 @raise TypeError: if I{amount} has a wrong type
565 """
566
567 sfam=self.units[unit]
568 dfam=self.units[dest]
569
570 amount, unit=sfam.convert(amount, unit, sfam.base)
571
572 path=self.shortest_path(sfam.name, dfam.name)
573 if path is None:
574 return None, None
575 elif len(path)==0:
576 return sfam.convert(amount, unit, dest)
577 else:
578 for p in path:
579 amount=amount*p.factor+p.offset
580 return dfam.convert(amount, dfam.base, dest)
581
582
583
585 """
586 Convert I{amount} of I{unit} to the unit in I{group} that
587 fits best.
588
589 See L{convert_to_family}.
590
591 @type amount: anything that L{to_decimal} can use
592 @type unit: C{string} (name of a known unit)
593 @type group: C{string} (name of a known group)
594
595 @return: Tupel (new amount, new unit) or (None, None) if
596 conversion is not possible
597
598 @raise KeyError: if I{unit} is not known
599 @raise KeyError: if I{group} is not known
600 @raise TypeError: if I{amount} has a wrong type
601 """
602 _amount = _unit = None
603 for fam in self.families.values():
604 if fam in self.groups[group]:
605 a, u = self.convert_to_family(amount, unit, fam)
606 if a is not None:
607 if ((_amount is None) or
608 (abs(a) >= 1 and abs(1-_amount) > abs(1-a)) or
609 (abs(a) < 1 and abs(1-_amount) < abs(1-a))
610 ):
611 _amount, _unit = a, u
612 return _amount, _unit
613