Module firkin
[hide private]
[frames] | no frames]

Source Code for Module firkin

  1  # -*- coding: utf-8 -*- 
  2  ## 
  3  ##    firkin - a Python module to convert between units 
  4  ##             <http://www.florian-diesch.de/software/firkin/> 
  5  ##    Copyright (C) 2008 Florian Diesch <devel@florian-diesch.de> 
  6   
  7  ##    This program is free software; you can redistribute it and/or modify 
  8  ##    it under the terms of the GNU General Public License as published by 
  9  ##    the Free Software Foundation; either version 2 of the License, or 
 10  ##    (at your option) any later version. 
 11   
 12  ##    This program is distributed in the hope that it will be useful, 
 13  ##    but WITHOUT ANY WARRANTY; without even the implied warranty of 
 14  ##    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 15  ##    GNU General Public License for more details. 
 16   
 17  ##    You should have received a copy of the GNU General Public License along 
 18  ##    with this program; if not, write to the Free Software Foundation, Inc., 
 19  ##    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 
 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   
129 -def to_decimal(value):
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
143 -class Family(object):
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 """
157 - def __init__(self, base, name=None):
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
212 - def autoconvert(self, amount, unit):
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
236 - def __contains__(self, item):
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
245 - def __getitem__(self, index):
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
257 - def __setitem__(self, index, value):
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
270 -class SIFamily(Family):
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
327 -class Item(object):
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
358 - def __repr__(self):
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
370 -class UnitManager(object):
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
404 - def __init__(self):
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
478 - def shortest_path(self, start, end, path=None):
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] # make a copy 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
512 - def convert_to_family(self, amount, unit, family):
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
550 - def convert_to_unit(self, amount, unit, dest):
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
584 - def convert_to_group(self, amount, unit, group):
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