mirror of
https://github.com/Mikaela/Limnoria.git
synced 2024-11-19 17:09:27 +01:00
478 lines
17 KiB
Python
Executable File
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)
|
|
|
|
|