Limnoria/others/convertcore.py
2004-01-21 15:52:01 +00:00

488 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.
"""
global fromUnit
global toUnit
global types
global unitsByType
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.
"""
global types
global unitsByType
if type in types:
return '%s units: %s' % (type, ', '.join(unitsByType[type]))
else:
return 'valid types: ' + ', '.join(types)