Limnoria/others/convertcore.py

478 lines
17 KiB
Python
Executable File

#!/usr/bin/env python
#****************************************************************************
# This file has been modified from its original version. It has been
# formatted to fit your irc bot.
#
# Below is the original copyright. Doug Bell rocks.
# The hijacker is Keith Jones, and he has no bomb in his shoe.
#
#****************************************************************************
#****************************************************************************
# convertcore.py, provides non-GUI base classes for data
#
# ConvertAll, a units conversion program
# Copyright (C) 2002, Douglas W. Bell
#
# This is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License, Version 2. This program is
# distributed in the hope that it will be useful, but WITTHOUT ANY WARRANTY.
#*****************************************************************************
#from option import Option
import re, copy, sys, os.path
import registry
import conf
#from math import *
class UnitGroup:
"Stores, updates and converts a group of units"
maxDecPlcs = 12
def __init__(self, unitData, option):
self.unitData = unitData
self.option = option
self.unitList = []
self.currentNum = 0
self.factor = 1.0
self.reducedList = []
self.linear = 1
def update(self, text, cursorPos=None):
"Decode user entered text into units"
self.unitList = self.parseGroup(text)
if cursorPos != None:
self.updateCurrentUnit(text, cursorPos)
else:
self.currentNum = len(self.unitList) - 1
def updateCurrentUnit(self, text, cursorPos):
"Set current unit number"
self.currentNum = len(re.findall('[\*/]', text[:cursorPos]))
def currentUnit(self):
"Return current unit if its a full match, o/w None"
if self.unitList and self.unitList[self.currentNum].equiv:
return self.unitList[self.currentNum]
return None
def currentPartialUnit(self):
"Return unit with at least a partial match, o/w None"
if not self.unitList:
return None
return self.unitData.findPartialMatch(self.unitList[self.currentNum]\
.name)
def currentSortPos(self):
"Return unit near current unit for sorting"
if not self.unitList:
return self.unitData[self.unitData.sortedKeys[0]]
return self.unitData.findSortPos(self.unitList[self.currentNum]\
.name)
def replaceCurrent(self, unit):
"Replace the current unit with unit"
if self.unitList:
exp = self.unitList[self.currentNum].exp
self.unitList[self.currentNum] = copy.copy(unit)
self.unitList[self.currentNum].exp = exp
else:
self.unitList.append(copy.copy(unit))
def completePartial(self):
"Replace a partial unit with a full one"
if self.unitList and not self.unitList[self.currentNum].equiv:
text = self.unitList[self.currentNum].name
unit = self.unitData.findPartialMatch(text)
if unit:
exp = self.unitList[self.currentNum].exp
self.unitList[self.currentNum] = copy.copy(unit)
self.unitList[self.currentNum].exp = exp
def moveToNext(self, upward):
"Replace unit with adjacent one based on match or sort position"
unit = self.currentSortPos()
num = self.unitData.sortedKeys.index(unit.name.\
replace(' ', '')) \
+ (upward and -1 or 1)
if 0 <= num < len(self.unitData.sortedKeys):
self.replaceCurrent(self.unitData[self.unitData.sortedKeys[num]])
def addOper(self, mult):
"Add new operator & blank unit after current, * if mult is true"
if self.unitList:
self.completePartial()
prevExp = self.unitList[self.currentNum].exp
self.currentNum += 1
self.unitList.insert(self.currentNum, Unit(''))
if (not mult and prevExp > 0) or (mult and prevExp < 0):
self.unitList[self.currentNum].exp = -1
def changeExp(self, newExp):
"Change the current unit's exponent"
if self.unitList:
self.completePartial()
if self.unitList[self.currentNum].exp > 0:
self.unitList[self.currentNum].exp = newExp
else:
self.unitList[self.currentNum].exp = -newExp
def clearUnit(self):
"Remove units"
self.unitList = []
def parseGroup(self, text):
"Return list of units from text string"
unitList = []
parts = [part.strip() for part in re.split('([\*/])', text)]
numerator = 1
while parts:
unit = self.parseUnit(parts.pop(0))
if not numerator:
unit.exp = -unit.exp
if parts and parts.pop(0) == '/':
numerator = not numerator
unitList.append(unit)
return unitList
def parseUnit(self, text):
"Return a valid or invalid unit with exponent from a text string"
parts = text.split('^', 1)
exp = 1
if len(parts) > 1: # has exponent
try:
exp = int(parts[1])
except ValueError:
if parts[1].lstrip().startswith('-'):
exp = -Unit.partialExp # tmp invalid exp
else:
exp = Unit.partialExp
unitText = parts[0].strip().replace(' ', '')
unit = copy.copy(self.unitData.get(unitText, None))
if not unit and unitText and unitText[-1] == 's' and not \
self.unitData.findPartialMatch(unitText): # check for plural
unit = copy.copy(self.unitData.get(unitText[:-1], None))
if not unit:
#unit = Unit(parts[0].strip()) # tmp invalid unit
raise UnitDataError('%s is not a valid unit.' % (unitText))
unit.exp = exp
return unit
def unitString(self, unitList=None):
"Return the full string for this group or a given group"
if unitList == None:
unitList = self.unitList[:]
fullText = ''
if unitList:
fullText = unitList[0].unitText(0)
numerator = 1
for unit in unitList[1:]:
if (numerator and unit.exp > 0) \
or (not numerator and unit.exp < 0):
fullText = '%s * %s' % (fullText, unit.unitText(1))
else:
fullText = '%s / %s' % (fullText, unit.unitText(1))
numerator = not numerator
return fullText
def groupValid(self):
"Return 1 if all unitself.reducedLists are valid"
if not self.unitList:
return 0
for unit in self.unitList:
if not unit.unitValid():
return 0
return 1
def reduceGroup(self):
"Update reduced list of units and factor"
self.linear = 1
self.reducedList = []
self.factor = 1.0
if not self.groupValid():
return
count = 0
tmpList = self.unitList[:]
while tmpList:
count += 1
if count > 5000:
raise UnitDataError, 'Circular unit definition'
unit = tmpList.pop(0)
if unit.equiv == '!':
self.reducedList.append(copy.copy(unit))
elif not unit.equiv:
raise UnitDataError, 'Invalid conversion for "%s"' % unit.name
else:
if unit.fromEqn:
self.linear = 0
newList = self.parseGroup(unit.equiv)
for newUnit in newList:
newUnit.exp *= unit.exp
tmpList.extend(newList)
self.factor *= unit.factor**unit.exp
self.reducedList.sort()
tmpList = self.reducedList[:]
self.reducedList = []
for unit in tmpList:
if self.reducedList and unit == self.reducedList[-1]:
self.reducedList[-1].exp += unit.exp
else:
self.reducedList.append(unit)
self.reducedList = [unit for unit in self.reducedList if \
unit.name != 'unit' and unit.exp != 0]
def categoryMatch(self, otherGroup):
"Return 1 if unit types are equivalent"
if not self.checkLinear() or not otherGroup.checkLinear():
return 0
return self.reducedList == otherGroup.reducedList and \
[unit.exp for unit in self.reducedList] \
== [unit.exp for unit in otherGroup.reducedList]
def checkLinear(self):
"Return 1 if linear or acceptable non-linear"
if not self.linear:
if len(self.unitList) > 1 or self.unitList[0].exp != 1:
return 0
return 1
def compatStr(self):
"Return string with reduced unit or linear compatability problem"
if self.checkLinear():
return self.unitString(self.reducedList)
return 'Cannot combine non-linear units'
def convert(self, num, toGroup):
"Return num of this group converted to toGroup"
if self.linear:
num *= self.factor
else:
num = self.nonLinearCalc(num, 1) * self.factor
if toGroup.linear:
return num / toGroup.factor
return toGroup.nonLinearCalc(num / toGroup.factor, 0)
def nonLinearCalc(self, num, isFrom):
"Return result of non-linear calculation"
x = num
try:
if self.unitList[0].toEqn: # regular equations
if isFrom:
return float(eval(self.unitList[0].fromEqn))
return float(eval(self.unitList[0].toEqn))
data = list(eval(self.unitList[0].fromEqn)) # extrapolation list
if isFrom:
data = [(float(group[0]), float(group[1])) for group in data]
else:
data = [(float(group[1]), float(group[0])) for group in data]
data.sort()
pos = len(data) - 1
for i in range(len(data)):
if num <= data[i][0]:
pos = i
break
if pos == 0:
pos = 1
return (num-data[pos-1][0]) / float(data[pos][0]-data[pos-1][0]) \
* (data[pos][1]-data[pos-1][1]) + data[pos-1][1]
except OverflowError:
return 1e9999
except:
raise UnitDataError, 'Bad equation for %s' % self.unitList[0].name
def convertStr(self, num, toGroup):
"Return formatted string of converted number"
return self.formatNumStr(self.convert(num, toGroup))
def formatNumStr(self, num):
"Return num string formatted per options"
decPlcs = self.option.intData('DecimalPlaces', 0, UnitGroup.maxDecPlcs)
if self.option.boolData('SciNotation'):
return ('%%0.%dE' % decPlcs) % num
if self.option.boolData('FixedDecimals'):
return ('%%0.%df' % decPlcs) % num
return ('%%0.%dG' % decPlcs) % num
class UnitDataError(Exception):
pass
class UnitData(dict):
def __init__(self):
dict.__init__(self)
self.sortedKeys = []
def readData(self):
"Read all unit data from file"
types = []
typeUnits = {}
try:
f = file(os.path.join(conf.supybot.directories.data(), \
'units.dat'), 'r')
lines = f.readlines()
f.close()
except IOError:
raise UnitDataError, 'Can not read "units.dat" file'
for i in range(len(lines)): # join continuation lines
delta = 1
while lines[i].rstrip().endswith('\\'):
lines[i] = ''.join([lines[i].rstrip()[:-1], lines[i+delta]])
lines[i+delta] = ''
delta += 1
units = [Unit(line) for line in lines if \
line.split('#', 1)[0].strip()] # remove comment lines
typeText = ''
for unit in units: # find & set headings
if unit.name.startswith('['):
typeText = unit.name[1:-1].strip()
types.append(typeText)
typeUnits[typeText] = []
unit.typeName = typeText
units = [unit for unit in units if unit.equiv] # keep valid units
for unit in units:
self[unit.name.replace(' ', '')] = unit
typeUnits[unit.typeName].append(unit.name)
self.sortedKeys = self.keys()
self.sortedKeys.sort()
print len(units), 'units loaded'
if len(self.sortedKeys) < len(units):
raise UnitDataError, 'Duplicate unit names found'
return (types, typeUnits)
def findPartialMatch(self, text):
"Return first partially matching unit or None"
text = text.replace(' ', '')
if not text:
return None
for name in self.sortedKeys:
if name.startswith(text):
return self[name]
return None
def findSortPos(self, text):
"Return unit whose abbrev comes immediately after text"
text = text.replace(' ', '')
for name in self.sortedKeys:
if text <= name:
return self[name]
return self[self.sortedKeys[-1]]
class Unit:
"Reads and stores a single unit conversion"
partialExp = 1000
def __init__(self, dataStr):
dataList = dataStr.split('#')
unitList = dataList.pop(0).split('=', 1)
self.name = unitList.pop(0).strip()
self.equiv = ''
self.factor = 1.0
self.fromEqn = '' # used only for non-linear units
self.toEqn = '' # used only for non-linear units
if unitList:
self.equiv = unitList[0].strip()
if self.equiv[0] == '[': # used only for non-linear units
try:
self.equiv, self.fromEqn = re.match('\[(.*?)\](.*)', \
self.equiv).groups()
if ';' in self.fromEqn:
self.fromEqn, self.toEqn = self.fromEqn.split(';', 1)
self.toEqn = self.toEqn.strip()
self.fromEqn = self.fromEqn.strip()
except AttributeError:
raise UnitDataError, 'Bad equation for "%s"' % self.name
else: # split factor and equiv unit for linear
parts = self.equiv.split(None, 1)
if len(parts) > 1 and re.search('[^\d\.eE\+\-\*/]', parts[0]) \
== None: # only allowed digits and operators
try:
self.factor = float(eval(parts[0]))
self.equiv = parts[1]
except:
pass
self.comments = [comm.strip() for comm in dataList]
self.comments.extend([''] * (2 - len(self.comments)))
self.exp = 1
self.viewLink = [None, None]
self.typeName = ''
def description(self):
"Return name and 1st comment (usu. full name) if applicable"
if self.comments[0]:
return '%s (%s)' % (self.name, self.comments[0])
return self.name
def unitValid(self):
"Return 1 if unit and exponent are valid"
if self.equiv and -Unit.partialExp < self.exp < Unit.partialExp:
return 1
return 0
def unitText(self, absExp=0):
"Return text for unit name with exponent or absolute value of exp"
exp = self.exp
if absExp:
exp = abs(self.exp)
if exp == 1:
return self.name
if -Unit.partialExp < exp < Unit.partialExp:
return '%s^%d' % (self.name, exp)
if exp > 1:
return '%s^' % self.name
else:
return '%s^-' % self.name
def __cmp__(self, other):
return cmp(self.name, other.name)
############################################################################
# Wrapper functionality
#
############################################################################
# Parse the data file, and set everything up for conversion
data = UnitData()
(types, unitsByType) = data.readData()
# At the moment, we're not handling options
option = None
# set up the objects for unit conversion
fromUnit = UnitGroup(data, option)
toUnit = UnitGroup(data, option)
def convert(num, unit1, unit2):
""" Convert from one unit to another
num is the factor for the first unit. Raises UnitDataError for
various errors.
"""
fromUnit.update(unit1)
toUnit.update(unit2)
fromUnit.reduceGroup()
toUnit.reduceGroup()
# Match up unit categories
if not fromUnit.categoryMatch(toUnit):
raise UnitDataError('unit categories did not match')
return fromUnit.convert(num, toUnit)
def units(type):
""" Return comma separated string list of units of given type, or
a list of types if the argument is not valid.
"""
if type in types:
return '%s units: %s' % (type, ', '.join(unitsByType[type]))
else:
return 'valid types: ' + ', '.join(types)