def delUnit(self, symToDelete): if symToDelete in self.units: unitsToDelete = {symToDelete} # Find all units to delete foundDependentUnit = True while foundDependentUnit: foundDependentUnit = False for sym, unit in self.units.items(): if sym in unitsToDelete: continue for dependencySym in unit.baseUnits.keys(): prefix, baseSym = UC_Utils.stripPrefix( self.units, dependencySym) if baseSym in unitsToDelete: unitsToDelete.add(sym) foundDependentUnit = True break # Delete all units which need to be deleted for sym in unitsToDelete: del self.units[sym] if sym in self.conversions: del self.conversions[sym] return unitsToDelete else: try: unitDefStr = self.getUnitDefinitionStr(symToDelete) except: raise UC_Common.UnitError( f"Cannot delete '{symToDelete}' - unit does not exist") raise UC_Common.UnitError( f"Cannot delete '{symToDelete}' - unit contains a prefix: {unitDefStr}" )
def parseUnit(units, conversions, tokens, overwrite=False): """ Convert the next series of tokens into a unit @param units: a map of unit symbols to unit objects to be modified @param conversions: a map of unit symbols to scale factors to be modified @param tokens: a list of tokens """ baseUnitMap = {} # Handle base unit sym = UC_Utils.parseSymbol(tokens) if not overwrite and (sym in units): raise UC_Common.FileFormatError( f"Duplicate definition of unit '{sym}'") # Handle derived unit nextToken = UC_Utils.getNextToken(tokens) if nextToken == UC_Common.MAP_DELIMITER: scaleFactor = UC_Utils.parseFloat(tokens) UC_Utils.getNextToken(tokens, UC_Common.SEP_DELIMITER) baseUnitMap = parseBaseUnitMap(tokens) conversions[sym] = scaleFactor # Handle other tokens elif nextToken != UC_Common.END_DELIMITER: raise UC_Common.FileFormatError( f"Expected delimiter; received '{nextToken}'") # Create unit units[sym] = UC_Unit.Unit(sym, baseUnitMap)
def getNextToken(tokens: list = [], expectedToken = None): """ Get the next token and remove it from the queue @param tokens: a list of tokens @param expectedToken: the expected token - an error is thrown if the next token does not equal the expected token @return the next token """ if not tokens: raise UC_Common.UnitError(f"Expected token; none received") token = tokens.pop(0) if expectedToken and token != expectedToken: raise UC_Common.UnitError(f"Expected '{expectedToken}'; received '{token}'") return token
def validate(units, conversions, prefixes): # Ensure that all units and dependencies are defined for unit in units.values(): for baseUnit in unit.baseUnits.keys(): prefix, sym = stripPrefix(units, baseUnit) if sym not in units: raise UC_Common.UnitError(f"Invalid unit symbol: '{sym}'") if prefix and (prefix not in prefixes): raise UC_Common.UnitError(f"Invalid prefix: '{prefix}'") # Ensure that all conversion units are defined for unit in conversions.keys(): if unit not in units: raise UC_Common.UnitError(f"No unit defined for conversion from: '{unit}'") # Ensure that there are no cycles in the dependency graph topologicalSort(units)
def delPrefix(self, symToDelete): if symToDelete in self.prefixes: unitsToDelete = set() # Find all units to delete foundDependentUnit = True while foundDependentUnit: foundDependentUnit = False for sym, unit in self.units.items(): if sym in unitsToDelete: continue for dependencySym in unit.baseUnits.keys(): prefix, baseSym = UC_Utils.stripPrefix( self.units, dependencySym) if prefix == symToDelete or baseSym in unitsToDelete: unitsToDelete.add(sym) foundDependentUnit = True break # Delete all units which need to be deleted for sym in unitsToDelete: del self.units[sym] if sym in self.conversions: del self.conversions[sym] # Delete from prefix map del self.prefixes[symToDelete] return unitsToDelete else: raise UC_Common.UnitError( f"Cannot delete '{symToDelete}' - prefix does not exist")
def addPrefix(self, sym, base, exp): if not UC_Utils.isValidSymbol(sym): raise UC_Common.UnitError( f"Invalid prefix '{sym}': valid prefix symbols are composed of alphabetical characters and underscores" ) if sym in self.prefixes: base, exp = self.prefixes[sym] raise UC_Common.UnitError( f"Prefix '{sym}' already exists: '{sym}' = {base}^{exp} = {base**exp}" ) else: # Try adding prefix prefixes = self.prefixes.copy() prefixes[sym] = (base, exp) # Check that all dependencies exist and check for an acyclic dependency graph UC_Utils.validate(self.units, self.conversions, prefixes) self.prefixes = prefixes
def peekNextToken(tokens: list = []): """ Get the next token without removing it from the queue @param tokens: a list of tokens @return the next token """ if not tokens: raise UC_Common.UnitError(f"Expected token; none received") return tokens[0]
def parseInt(tokens): """ Get the next token as an int and remove it from the queue @param tokens: a list of tokens @return the next token as an int """ scaleFactorStr = getNextToken(tokens) try: scaleFactor = int(scaleFactorStr) except: raise UC_Common.UnitError(f"Expected integer; received '{scaleFactorStr}'") return scaleFactor
def parseSymbol(tokens): """ Get the next token as a unit symbol and remove it from the queue @param tokens: a list of tokens @return the next token as a unit symbol """ sym = getNextToken(tokens) if not isValidSymbol(sym): raise UC_Common.UnitError(f"Expected alphabetical symbol; received '{sym}'") return sym
def stripPrefix(units, string): # Find longest matching suffix longestSuffix = "" for sym in units.keys(): if string.endswith(sym) and len(sym) > len(longestSuffix): longestSuffix = sym if len(longestSuffix) == len(string): break if len(longestSuffix) == 0: raise UC_Common.UnitError(f"Invalid unit: received '{string}'") return string[0:len(string)-len(longestSuffix)], longestSuffix
def parseFloat(tokens): """ Get the next token as a float and remove it from the queue @param tokens: a list of tokens @return the next token as a float """ scaleFactorStr = getNextToken(tokens) try: scaleFactor = Decimal(scaleFactorStr) except: raise UC_Common.UnitError(f"Expected float; received '{scaleFactorStr}'") return scaleFactor
def aggregateQuantities(tokens): """ Combine tokens which constitute a quantity @param tokens: a list of tokens """ aggregatedTokens = [] needsValue = True while tokens: if UC_Utils.isOperator(tokens[0]): if needsValue: raise UC_Common.UnitError( f"Expected float; received '{tokens[0]}'") aggregatedTokens.append(tokens.pop(0)) needsValue = True elif UC_Utils.isSpecialChar(tokens[0]): aggregatedTokens.append(tokens.pop(0)) else: needsValue = False # Get value quantity = '1' try: float(tokens[0]) quantity = tokens.pop(0) except: # Inject multiplication where needed if aggregatedTokens and aggregatedTokens[ -1] == UC_Common.BRACKET_SHUT: aggregatedTokens.append(UC_Common.OPERATOR_MUL) # Get unit unit = [] if tokens and isinstance(tokens[0], list): unit = tokens.pop(0) aggregatedTokens.append((quantity, unit)) if needsValue and aggregatedTokens: raise UC_Common.UnitError(f"Expected float; no tokens received") return aggregatedTokens
def addUnit(self, sym, scaleFactor, unit): if not UC_Utils.isValidSymbol(sym): raise UC_Common.UnitError( f"Invalid symbol '{sym}': valid unit symbols are composed of alphabetical characters and underscores" ) if sym in self.units: quantity = self.conversions[sym] if sym in self.conversions else 1 raise UC_Common.UnitError( f"Unit '{sym}' already exists: {self.getUnitDefinitionStr(sym)}" ) else: # Try adding unit units = self.units.copy() units[sym] = UC_Unit.Unit(sym, unit.reduce()) conversions = self.conversions.copy() conversions[sym] = scaleFactor # Check that all dependencies exist and check for an acyclic dependency graph UC_Utils.validate(units, conversions, self.prefixes) self.units = units self.conversions = conversions
def parsePrefixMapping(prefixes, tokens, base, overwrite=False): """ Convert the next series of tokens into a prefix-exponent pair @param prefixes: the prefix-exponent map to modify @param tokens: a list of tokens @param base: the base for the exponent """ prefix = UC_Utils.getNextToken(tokens) if not overwrite and (prefix in prefixes): raise UC_Common.FileFormatError( f"Duplicate definition of prefix '{prefix}'") prefixes[prefix] = (base, Decimal(UC_Utils.parseInt(tokens)))
def topologicalSortVisit(units: dict, unit: str, sortedValues: list, permVisited: set, tempVisited: set): if unit in permVisited: return if unit in tempVisited: raise UC_Common.UnitError(f"Dependency cycle detected for unit '{unit}'") tempVisited[unit] = True for baseUnit in units[unit].baseUnits.keys(): prefix, sym = stripPrefix(units, baseUnit) topologicalSortVisit(units, sym, sortedValues, permVisited, tempVisited) del tempVisited[unit] permVisited[unit] = True sortedValues.insert(0, unit)
def processPrefixes(self, units): scaleFactor = Decimal(1) unitsToUpdate = {} for prefixedSym, exp in units.items(): # Find prefix and base unit prefix, baseUnit = UC_Utils.stripPrefix(self.units, prefixedSym) if len(prefix) == 0: continue if prefix not in self.prefixes: raise UC_Common.UnitError(f"Unknown unit: '{prefixedSym}'") unitsToUpdate[prefixedSym] = baseUnit # Calculate scale factor scaleFactor *= self.getPrefixScaleFactor(prefix)**(exp) # Update unit map for fromSym, toSym in unitsToUpdate.items(): if not (toSym in units): units[toSym] = 0 units[toSym] += units[fromSym] units.pop(fromSym) return scaleFactor
def aggregateSign(tokens, updatedTokens=[]): """ Replace the negation operator '-' with a multiplication by -1 @param tokens: a list of tokens """ def aggregateSignHelper(tokens, updatedTokens): while tokens: token = tokens.pop(0) if token == UC_Common.BRACKET_OPEN: updatedTokens.append(token) aggregateSignHelper(tokens, updatedTokens) elif token == UC_Common.BRACKET_SHUT: updatedTokens.append(token) return updatedTokens elif ((token == UC_Common.OPERATOR_ADD or token == UC_Common.OPERATOR_SUB) and (not updatedTokens or UC_Utils.isSpecialChar(updatedTokens[-1]))): if tokens and UC_Utils.isFloat(tokens[0]): updatedTokens.append(f"{token}{tokens.pop(0)}") elif not updatedTokens or updatedTokens[ -1] != UC_Common.BRACKET_SHUT: updatedTokens.extend([ UC_Common.BRACKET_OPEN, f"{token}1", UC_Common.OPERATOR_MUL ]) aggregateSignHelper(tokens, updatedTokens) updatedTokens.append(UC_Common.BRACKET_SHUT) else: updatedTokens.append(token) else: updatedTokens.append(token) updatedTokens = [] aggregateSignHelper(tokens, updatedTokens) if tokens: raise UC_Common.UnitError( f"Detected mismatched parentheses: '{UC_Common.BRACKET_SHUT}'") return updatedTokens
def parseExpr(tokens): stack = [] for token in tokens: if token == UC_Common.OPERATOR_ADD: a = stack.pop() b = stack.pop() stack.append(UC_AST.AST_Add(b, a)) elif token == UC_Common.OPERATOR_SUB: a = stack.pop() b = stack.pop() stack.append(UC_AST.AST_Sub(b, a)) elif token == UC_Common.OPERATOR_MUL: a = stack.pop() b = stack.pop() stack.append(UC_AST.AST_Mul(b, a)) elif token == UC_Common.OPERATOR_DIV: a = stack.pop() b = stack.pop() stack.append(UC_AST.AST_Div(b, a)) elif token == UC_Common.OPERATOR_EXP: a = stack.pop() b = stack.pop() stack.append(UC_AST.AST_Exp(b, a)) elif token == UC_Common.OPERATOR_EQL: a = stack.pop() b = stack.pop() stack.append(UC_AST.AST_Eql(b, a)) else: valStr, unitTokens = token val = Decimal(valStr) baseUnits = UC_Unit.Unit(baseUnits=parseUnit(unitTokens)).reduce() unit = UC_Unit.Unit(baseUnits=baseUnits) stack.append(UC_Unit.Quantity(val, unit)) if not stack: return UC_Unit.Quantity(1, UC_Unit.Unit()) if len(stack) != 1: raise UC_Common.UnitError("Invalid expression") return stack[0]
def convert(self, srcUnit, dstUnit): srcUnits = srcUnit.reduce() dstUnits = dstUnit.reduce() # Reduce all units to irreducible performedReduction = True scaleFactor = 1 while performedReduction: # Simplify units using SI prefixes scaleFactor *= self.processPrefixes(srcUnits) scaleFactor /= self.processPrefixes(dstUnits) # Factor out common units and perform a topological sort to find the next unit to reduce self.factorUnits(srcUnits, dstUnits) unitsToReduce = UC_Utils.topologicalSort( self.units, [*srcUnits.keys()] + [*dstUnits.keys()]) # Reduce units if len(unitsToReduce) == 0: break performedReduction = self.units[unitsToReduce[0]].isDerivedUnit() if performedReduction: if unitsToReduce[0] in srcUnits: scaleFactor *= self.reduceUnit(unitsToReduce[0], srcUnits) else: scaleFactor /= self.reduceUnit(unitsToReduce[0], dstUnits) # Remove cancelled units removeCancelledUnits(srcUnits) removeCancelledUnits(dstUnits) # Check for conversion error if len(srcUnits) > 0 or len(dstUnits) > 0: raise UC_Common.UnitError( f"Invalid conversion: {str(srcUnit)} to {str(dstUnit)}") return scaleFactor
def parseUnit(tokens): tokens = convertToRPN(tokens) stack = [] for token in tokens: if token == UC_Common.OPERATOR_ADD: a = stack.pop() if not isinstance(a, int): raise UC_Common.UnitError(f"Expected int; received '{a}'") b = stack.pop() if not isinstance(b, int): raise UC_Common.UnitError(f"Expected int; received '{b}'") stack.append(b + a) elif token == UC_Common.OPERATOR_SUB: a = stack.pop() if not isinstance(a, int): raise UC_Common.UnitError(f"Expected int; received '{a}'") b = stack.pop() if not isinstance(b, int): raise UC_Common.UnitError(f"Expected int; received '{b}'") stack.append(b - a) elif token == UC_Common.OPERATOR_MUL: a = stack.pop() if not isinstance(a, dict): a = {a: 1} b = stack.pop() if not isinstance(b, dict): b = {b: 1} for sym, exp in b.items(): if sym not in a: a[sym] = 0 a[sym] += exp stack.append(a) elif token == UC_Common.OPERATOR_DIV: a = stack.pop() if not isinstance(a, dict): a = {a: 1} b = stack.pop() if not isinstance(b, dict): b = {b: 1} for sym, exp in a.items(): if sym not in b: b[sym] = 0 b[sym] -= exp stack.append(b) elif token == UC_Common.OPERATOR_EXP: a = stack.pop() b = stack.pop() if not isinstance(a, int): raise UC_Common.UnitError(f"Expected int; received '{a}'") stack.append({b: a}) else: if UC_Utils.isInt(token): stack.append(int(token)) else: stack.append(token) # Aggregate into a single map units = {} while stack: top = stack.pop() if isinstance(top, dict): for sym, exp in top.items(): if sym not in units: units[sym] = 0 units[sym] += exp elif UC_Utils.isValidSymbol(top): if top not in units: units[top] = 0 units[top] += 1 else: raise UC_Common.UnitError("Invalid expression") return units
def aggregateUnits(tokens): """ Combine tokens which constitute compound units @param tokens: a list of tokens """ aggregatedTokens = [] unitTokens = [] parsingExp = 0 def appendUnitTokens(aggregatedTokens, unitTokens, token=None): # Append unit tokens to list of aggregated tokens if unitTokens: if unitTokens[-1] == UC_Common.OPERATOR_MUL or unitTokens[ -1] == UC_Common.OPERATOR_DIV: operator = unitTokens.pop() aggregatedTokens.append(unitTokens) aggregatedTokens.append(operator) else: aggregatedTokens.append(unitTokens) if token is not None: # Inject multiplication if needed if ((aggregatedTokens) and (not UC_Utils.isSpecialChar(aggregatedTokens[-1])) and (token == UC_Common.BRACKET_OPEN)): aggregatedTokens.append(UC_Common.OPERATOR_MUL) aggregatedTokens.append(token) return [] def handleParseExpDecrement(tokens, unitTokens, parsingExp): # Check if multiplication needs to be injected between adjacent units if parsingExp != 1: return parsingExp if tokens: if tokens[0] == UC_Common.OPERATOR_MUL or tokens[ 0] == UC_Common.OPERATOR_DIV: unitTokens.append(tokens.pop(0)) elif UC_Utils.isValidSymbol(tokens[0]): unitTokens.append(UC_Common.OPERATOR_MUL) return 0 def handleAppendUnitSymbol(tokens, unitTokens, parsingExp): if tokens: token = tokens[0] if token == UC_Common.OPERATOR_EXP: unitTokens.append(tokens.pop(0)) return 1 elif token == UC_Common.OPERATOR_MUL or token == UC_Common.OPERATOR_DIV: unitTokens.append(tokens.pop(0)) elif UC_Utils.isValidSymbol(token): unitTokens.append(UC_Common.OPERATOR_MUL) return parsingExp while tokens: token = UC_Utils.getNextToken(tokens) if token == UC_Common.BRACKET_OPEN: if parsingExp: unitTokens.append(token) parsingExp += 1 else: unitTokens = appendUnitTokens(aggregatedTokens, unitTokens, token) elif token == UC_Common.BRACKET_SHUT: if parsingExp: unitTokens.append(token) parsingExp = handleParseExpDecrement(tokens, unitTokens, parsingExp - 1) else: unitTokens = appendUnitTokens(aggregatedTokens, unitTokens, token) elif UC_Utils.isFloat(token): if parsingExp: if not UC_Utils.isInt(token): raise UC_Common.UnitError( f"Expected int; received '{token}'") unitTokens.append(token) parsingExp = handleParseExpDecrement(tokens, unitTokens, parsingExp) else: unitTokens = appendUnitTokens(aggregatedTokens, unitTokens, token) elif UC_Utils.isValidSymbol(token): if parsingExp: raise UC_Common.UnitError(f"Expected int; received '{token}'") unitTokens.append(token) parsingExp = handleAppendUnitSymbol(tokens, unitTokens, parsingExp) elif UC_Utils.isOperator(token): if parsingExp: raise UC_Common.UnitError(f"Expected int; received '{token}'") else: unitTokens = appendUnitTokens(aggregatedTokens, unitTokens, token) else: raise UC_Common.UnitError(f"Unknown token; received '{token}'") appendUnitTokens(aggregatedTokens, unitTokens) return aggregatedTokens
def __sub__(self, other): if self.unit != other.unit: raise UC_Common.UnitError('Incompatible units') return Quantity(self.value - other.value, self.unit.clone())
def __pow__(self, other): if other.unit.reduce(): raise UC_Common.UnitError( f"Cannot exponentiate with unit '{str(other.unit)}'") return Quantity(self.value**other.value, self.unit**other.value)