diff options
author | Reinier Heeres <reinier@heeres.eu> | 2008-12-23 21:50:32 (GMT) |
---|---|---|
committer | Reinier Heeres <reinier@heeres.eu> | 2008-12-23 21:50:32 (GMT) |
commit | 6807696129436dbf071b606300ce6dda2adb1c50 (patch) | |
tree | 984328276f12f6d18f89ea5ea4e940a379c3993f | |
parent | f0b95df2389c5686979b489fa5f2084fd3738edf (diff) |
Move to more flexible and faster parser based on python AST
-rw-r--r-- | astparser.py | 484 | ||||
-rw-r--r-- | constants.py | 42 | ||||
-rw-r--r-- | eqnparser.py | 663 | ||||
-rw-r--r-- | functions.py | 401 | ||||
-rw-r--r-- | mathlib.py | 238 | ||||
-rw-r--r-- | plotlib.py | 70 |
6 files changed, 973 insertions, 925 deletions
diff --git a/astparser.py b/astparser.py new file mode 100644 index 0000000..8acb145 --- /dev/null +++ b/astparser.py @@ -0,0 +1,484 @@ +# -*- coding: UTF-8 -*- + +import types +import parser +import re +import inspect +import math +import logging + +from gettext import gettext as _ + +# Python 2.6 has a 'public' ast module +try: + import ast +except ImportError: + import _ast as ast + +from mathlib import MathLib +from plotlib import PlotLib + +class ParseError(Exception): + def __init__(self, msg, start, end=None): + self._msg = msg + + if end is None: + end = start + 1 + self._range = (start, end) + + def get_range(self): + return self._range + + def __str__(self): + msg = _("Error at %d") % (self._range[0] + 1) + if self._msg is not None and len(self._msg) > 0: + msg += ": %s" % (self._msg) + return msg + +class Helper: + + def __init__(self, parent): + self._parent = parent + self._topics = {} + self.add_help('test', + _('This is just a test topic, use help(index) for the index')) + + def add_help(self, topic, text): + self._topics[unicode(topic)] = _(text) + self._topics[unicode(_(topic))] = _(text) + + def get_help(self, topic=None): + if isinstance(topic, ast.Name): + topic = topic.id + elif isinstance(topic, ast.Str): + topic = topic.s + elif type(topic) not in (types.StringType, types.UnicodeType) or len(topic) == 0: + return _("Use help(test) for help about 'test', or help(index) for the index") + + # TRANS: This command is descriptive, so can be translated + if topic in ('index', _('index'), 'topics', _('topics')): + ret = _('Topics') + ': ' + topics = self._topics.keys() + topics.append('index') + topics.sort() + ret += ', '.join(topics) + return ret + + # TRANS: This command is descriptive, so can be translated + if topic in ('variables', _('variables')): + ret = _('Variables') + ': ' + variables = self._parent.get_variable_names() + ret += ', '.join(variables) + return ret + + # TRANS: This command is descriptive, so can be translated + if topic in ('functions', _('functions')): + ret = _('Functions') + ': ' + functions = self._parent.get_function_names() + ret += ', '.join(functions) + return ret + + for (key, val) in self._topics.iteritems(): + if topic == key or _(topic) == key: + return val + + return _("No help about '%s' available, use help(index) for the index") % (topic) + +class AstParser: + ''' + Equation parser based on python's ast (abstract syntax tree) module. + In 2.5 this is a private module, but in 2.6 it is public. + ''' + + OPERATOR_MAP = { + u'⨯': '*', + u'×': '*', + u'÷': '/', + '^': '**', + } + + DIADIC_OPS = ( + '+', '-', '*', u'⨯', u'×', u'÷' , '/', '^', '**', + '&', '|', '=', '!=', '<', '>', '<<', '>>', '%', + ) + + PRE_OPS = ( + '-', '+', '~', + ) + + POST_OPS = ( + ) + + FLOAT_REGEXP_STR = '([+-]?[0-9]*\.?[0-9]+([eE][+-]?[0-9]+)?)' + FLOAT_REGEXP = re.compile(FLOAT_REGEXP_STR) + RANGE_REGEXP = re.compile(FLOAT_REGEXP_STR + '\.\.' + FLOAT_REGEXP_STR) + + # Unary and binary operator maps. + # Mappings to a string will be replaced by calls to MathLib functions + # with the same name. + + UNARYOP_MAP = { + ast.UAdd: lambda x: x, + ast.USub: lambda x: -x, + ast.Not: lambda x: not x, + } + + BINOP_MAP = { + ast.Add: 'add', + ast.And: lambda x, y: x and y, + ast.BitAnd: lambda x, y: x & y, + ast.BitOr: lambda x, y: x | y, + ast.BitXor: lambda x, y: x ^ y, + ast.Div: 'div', + ast.FloorDiv: 'div', + ast.LShift: 'shift_left', + ast.Mod: 'mod', + ast.Mult: 'mul', + ast.NotEq: lambda x, y: x != y, + ast.Or: lambda x, y: x or y, + ast.Pow: 'pow', + ast.RShift: 'shift_right', + ast.Sub: 'sub', + } + + CMPOP_MAP = { + ast.Gt: lambda x, y: x > y, + ast.GtE: lambda x, y: x >= y, + ast.Is: lambda x, y: x == y, + ast.IsNot: lambda x, y: x != y, + ast.Lt: lambda x, y: x < y, + ast.LtE: lambda x, y: x <= y, + } + + _ARG_STRING = 0 + _ARG_NODE = 1 + + def __init__(self, ml=None, pl=None): + self._namespace = {} + + if ml is None: + self.ml = MathLib() + else: + self.ml = ml + + if pl is None: + self.pl = PlotLib(self) + else: + self.pl = pl + + # Plug-in plot function + self.set_var('plot', self.pl.plot) + + # Help manager + self._helper = Helper(self) + self.set_var('help', self._helper.get_help) + self._special_func_args = { + (self._helper.get_help, 0): self._ARG_STRING, + (self.pl.plot, 0): self._ARG_NODE, + } + + self._load_plugins() + + # Redirect operations to registered functions + for key, val in self.UNARYOP_MAP.iteritems(): + if type(val) is types.StringType: + self.UNARYOP_MAP[key] = self.get_var(val) + for key, val in self.BINOP_MAP.iteritems(): + if type(val) is types.StringType: + self.BINOP_MAP[key] = self.get_var(val) + + def _load_plugin_items(self, items): + for name, item in items: + if name.startswith('_') or type(item) is types.ModuleType: + continue + + self.set_var(name, item) + if type(item) in (types.FunctionType, types.ClassType): + if item.__doc__ is not None: + self._helper.add_help(name, item.__doc__) + + def _load_plugins(self): + plugins = ('functions', 'constants') + for plugin in plugins: + try: + exec('import %s' % plugin) + exec('_mod = %s' % plugin) + items = inspect.getmembers(_mod) + self._load_plugin_items(items) + + except Exception, e: + logging.error('Error loading plugin: %s', e) + + def log_debug_info(self): + logging.debug('Variables:') + for name in self.get_variable_names(): + logging.debug(' %s', name) + logging.debug('Functions:') + for name in self.get_function_names(): + logging.debug(' %s', name) + logging.debug('Unary ops:') + for op in self.UNARYOP_MAP.keys(): + logging.debug(' %s', op) + logging.debug('Binary ops:') + for op in self.BINOP_MAP.keys(): + logging.debug(' %s', op) + + def set_var(self, name, value): + '''Set variable <name> to <value>, which could be a function too.''' + self._namespace[unicode(name)] = value + + def get_var(self, name): + '''Return variable value, or None if non-existent.''' + return self._namespace.get(unicode(name), None) + + def _get_names(self, start='', include_vars=True): + ret = [] + for key, val in self._namespace.iteritems(): + if type(val) is types.ClassType: + for key2, val2 in inspect.getmembers(val): + if key2.startswith('_'): + continue + + b = type(val2) not in (types.FunctionType, types.MethodType) + if not include_vars: + b = not b + if b and key2.startswith(start): + ret.append(key2) + + else: + b = type(val) not in (types.FunctionType, types.MethodType) + if not include_vars: + b = not b + if b and key.startswith(start): + ret.append(key) + + ret.sort() + return ret + + def get_names(self, start=''): + '''Return a list with names of all defined variables/functions.''' + ret = [] + for key, val in self._namespace.iteritems(): + if key.startswith(start): + ret.append(key) + + return ret + + def get_variable_names(self, start=''): + '''Return a list with names of all defined variables.''' + return self._get_names(start, include_vars=True) + + def get_function_names(self, start=''): + '''Return a list with names of all defined function.''' + return self._get_names(start, include_vars=False) + + def add_help(self, topic, text): + self._help_topics[topic] = text + + def get_diadic_operators(self): + return self.DIADIC_OPS + + def get_post_operators(self): + return self.POST_OPS + + def get_pre_operators(self): + return self.PRE_OPS + + def _resolve_arg(self, func, index, arg, level): + funcarg = (func, index) + if funcarg in self._special_func_args: + val = self._special_func_args[funcarg] + if val == self._ARG_NODE: + return arg + if val == self._ARG_STRING: + if isinstance(arg, ast.Name): + return arg.id + elif isinstance(arg, ast.Str): + return arg.s + else: + logging.error('Unable to resolve special arg %r', arg) + else: + return self._process_node(arg, level) + + def _process_node(self, node, level=0, isfunc=False): + ofs = node.col_offset + + if node is None: + return None + + elif isinstance(node, ast.Expression): + return self._process_node(node.body) + + elif isinstance(node, ast.BinOp): + left = self._process_node(node.left, level + 1) + right = self._process_node(node.right, level + 1) + if left is None or right is None: + return None + func = self.BINOP_MAP[type(node.op)] + return func(left, right) + + elif isinstance(node, ast.UnaryOp): + operand = self._process_node(node.operand, level + 1) + if operand is None: + return None + func = self.UNARYOP_MAP[type(node.op)] + return func(operand) + + elif isinstance(node, ast.Compare): + left = self._process_node(node.left) + right = self._process_node(node.comparators[0]) + func = self.CMPOP_MAP[type(node.ops[0])] + return func(left, right) + + elif isinstance(node, ast.Call): + func = self._process_node(node.func, level + 1, isfunc=True) + if func is None: + return None + + for i in range(len(node.args)): + node.args[i] = self._resolve_arg(func, i, node.args[i], level + 1) + if node.args[i] is None: + return None + kwargs = {} + for i in range(len(node.keywords)): + key = node.keywords[i].arg + val = self._process_node(node.keywords[i].value, level + 1) + if key is None or val is None: + return None + kwargs[key] = val + + try: + ret = func(*node.args, **kwargs) + return ret + except Exception, e: + msg = str(e) + raise ParseError(msg, ofs) + + elif isinstance(node, ast.Num): + return node.n + + elif isinstance(node, ast.Str): + return node.s + + elif isinstance(node, ast.Tuple): + list = [self._process_node(i, level + 1) for i in node.elts] + return tuple(list) + + elif isinstance(node, ast.Name): + if not isfunc and node.id in ('help', _('help')): + return self._helper.get_help() + + elif node.id in self._namespace: + var = self.get_var(node.id) + if type(var) is ast.Expression: + return self._process_node(var.body) + else: + return var + else: + if isfunc: + msg = _("Function '%s' not defined") % (node.id) + else: + msg = _("Variable '%s' not defined") % (node.id) + raise ParseError(msg, ofs, ofs + len(node.id)) + + elif isinstance(node, ast.Attribute): + parent = self._process_node(node.value) + if parent: + try: + val = parent.__dict__[node.attr] + return val + except Exception, e: + msg = _("Attribute '%s' does not exist)") % node.value + raise ParseError(msg, ofs, ofs + len(node.value)) + + return None + + else: + logging.debug('Unknown node: %r', repr(node)) + + return None + + def _preprocess_eqn(self, eqn): + eqn = unicode(eqn) + for key, val in self.OPERATOR_MAP.iteritems(): + eqn = eqn.replace(key, val) + + # Replace a..b ranges with (a,b) + eqn = self.RANGE_REGEXP.sub(r'(\1,\3)', eqn) + + return eqn + + def parse(self, eqn): + ''' + Parse an equation and return a parse tree. + ''' + + eqn = self._preprocess_eqn(eqn) + logging.debug('Parsing preprocessed equation: %r', eqn) + + try: + tree = compile(eqn, '<string>', 'eval', ast.PyCF_ONLY_AST) + except SyntaxError, e: + msg = _('Parse error') + raise ParseError(msg, e.offset - 1) + + return tree + + def evaluate(self, eqn): + ''' + Evaluate an equation or parse tree. + ''' + + if type(eqn) in (types.StringType, types.UnicodeType): + eqn = self.parse(eqn) + + if isinstance(eqn, ast.Expression): + ret = self._process_node(eqn.body) + else: + ret = self._process_node(eqn) + + if type(ret) is types.FunctionType: + return ret() + else: + return ret + + def parse_and_eval(self, eqn): + ''' + Parse and evaluate an equation. + ''' + + tree = self.parse(eqn) + if tree is not None: + return self.evaluate(tree) + else: + return None + +if __name__ == '__main__': + logging.basicConfig(level=logging.DEBUG) + + p = AstParser() + eqn = 'sin(45)' + ret = p.evaluate(eqn) + print 'Eqn: %s, ret: %s' % (eqn, ret) + + eqn = '2<=physics.c' + ret = p.evaluate(eqn) + print 'Eqn: %s, ret: %s' % (eqn, ret) + + eqn = 'help(functions)' + ret = p.evaluate(eqn) + print 'Eqn: %s, ret: %s' % (eqn, ret) + + eqn = 'factorize(105)' + ret = p.evaluate(eqn) + print 'Eqn: %s, ret: %s' % (eqn, ret) + + eqn = 'plot(x**2,x=-2..2)' + ret = p.evaluate(eqn) + print 'Eqn: %s, ret: %s' % (eqn, ret) + + p.set_var('a', 123) + eqn = 'a * 5' + ret = p.evaluate(eqn) + print 'Eqn: %s, ret: %s' % (eqn, ret) + diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..f071ab3 --- /dev/null +++ b/constants.py @@ -0,0 +1,42 @@ +# constants.py, constants available in Calculate, +# by Reinier Heeres <reinier@heeres.eu> +# Most of these come from Wikipedia. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import math as _math + +pi = _math.pi # 3.1415926535 +e = _math.exp(1) # 2.7182818284590451 + +class math: + golden_ratio = 1.61803398874989484820458683436563811 + +class physics: + c = 299792458 # Speed of light (in vacuum) + h = 6.6260689633e-34 # Planck's constant + hbar = 1.05457162853e-34 # Dirac's constant + mu0 = 4e-7 * pi # Magnetic permeability of vacuum + e0 = 8.854187817e-12 # Electric permeability of vacuum + + Na = 6.022141510e23 # Avogadro's number + kb = 1.380650524e-23 # Boltmann's constant + R = 8.31447215 # Gas constant + + c_e = -1.60217648740e-19 # Electron charge + m_e = 9.109382616e-31 # Electron mass + m_p = 1.6726217129e-27 # Proton mass + m_n = 1.6749272928e-27 # Neutron mass + diff --git a/eqnparser.py b/eqnparser.py deleted file mode 100644 index b09a7c3..0000000 --- a/eqnparser.py +++ /dev/null @@ -1,663 +0,0 @@ -# -*- coding: UTF-8 -*- -# eqnparser.py, generic equation parser by Reinier Heeres <reinier@heeres.eu> -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# -# Change log: -# 2007-07-03: rwh, first version - -import logging -_logger = logging.getLogger('EqnParser') - -import types -from mathlib import MathLib -from plotlib import PlotLib -from eqnparserhelp import EqnParserHelp - -from rational import Rational - -from gettext import gettext as _ - -_generic_error = _('Parse error') - -class Equation: - def __init__(self, eqn): - self.equation = eqn - -class ParserState: - OK = 1 - PARSE_ERROR = 2 - - def __init__(self, str): - self.str = str - self.strlen = len(str) - if self.strlen > 0: - self.char = str[0] - else: - self.char = None - self.ofs = 0 - self.level = -1 - self.result_type = EqnParser.TYPE_UNKNOWN - self.error_code = self.OK - self.error_msg = "" - self.error_range = (0, 0) - - def state_string(self): - return _('level: %d, ofs %d') % (self.level, self.ofs) - - def more(self): - return self.error_code == self.OK and self.ofs < self.strlen - - def next(self): - self.ofs += 1 - if self.ofs < self.strlen: - self.char = self.str[self.ofs] - else: - self.char = None - return self.char - - def prev(self): - self.ofs -= 1 - if self.ofs < self.strlen and self.ofs >= 0: - self.char = self.str[self.ofs] - else: - self.char = None - return self.char - - def set_ofs(self, o): - self.ofs = o - self.char = self.str[o] - - def inc_level(self): - self.level += 1 - - def dec_level(self): - self.level -= 1 - - def set_type(self, t): - if self.result_type == EqnParser.TYPE_UNKNOWN or t is EqnParser.TYPE_SYMBOLIC: - self.result_type = t - return True - elif self.result_type != t: - _logger.debug('Type error') - return False - else: - return True - - def set_error(self, c, msg=None, range=None): - self.error_code = c - if msg is not None: - self.error_msg = msg - if range is not None: - _logger.debug('Setting range: %r', range) - self.error_range = range - else: - _logger.debug('Setting offset: %d', self.ofs) - self.error_range = (self.ofs, self.ofs + 1) - - def get_error(self): - return self.error_code - - def set_error_range(self, r): - self.error_range = r - - def format_error(self): - msg = _("Error at %d") % (self.error_range[0]) - if self.error_msg is not None and len(self.error_msg) > 0: - msg += ": %s" % (self.error_msg) - return msg - - def copy_error(self, ps): - """Copy error state from recursively created ParserState object""" - self.error_code = ps.error_code - self.error_msg = ps.error_msg - self.error_range = (ps.error_range[0] + ps.ofs, ps.error_range[1] + ps.ofs) - -class EqnParser: - OP_INVALID = -1 - OP_PRE = 1 - OP_POST = 2 - OP_DIADIC = 3 - OP_ASSIGN = 4 - - INVALID_OP = ('err', OP_INVALID, 0, lambda x: False) - - NAME_CHARS = u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789 ' - DIGITS = u'0123456789' - SPACE_CHARS = u'\t \r\n' - -# These will be filled from register_operator - OP_START_CHARS = u"" - OP_CHARS = u"" - - TYPE_UNKNOWN = 1 - TYPE_INT = 2 - TYPE_FLOAT = 3 - TYPE_BOOL = 4 - TYPE_SYMBOLIC = 5 - - def __init__(self, ml=None): - if ml is None: - self.ml = MathLib() - else: - self.ml = ml - - self.ps = None - - self.pl = PlotLib(self) - - self.variables = {} - self.parse_var = {} # Whether or not to parse this variable recursively - self.functions = {} - self.operators = [] - - self.cached_diadic_ops = None - self.cached_pre_ops = None - self.cached_post_ops = None - - self.register_function('exp', lambda x: self.ml.exp(x[0]), {"nargs": 1}) - self.register_function('ln', lambda x: self.ml.ln(x[0]), {"nargs": 1}) - self.register_function('log', lambda x: self.ml.log10(x[0]), {"nargs": 1}) - self.register_function('log10', lambda x: self.ml.log10(x[0]), {"nargs": 1}) - self.register_function('pow', lambda x: self.ml.pow(x[0], x[1]), {"nargs": 2}) - - self.register_function('sqrt', lambda x: self.ml.sqrt(x[0]), {"nargs": 1}) - - self.register_function('sin', lambda x: self.ml.sin(x[0]), {"nargs": 1}) - self.register_function('cos', lambda x: self.ml.cos(x[0]), {"nargs": 1}) - self.register_function('tan', lambda x: self.ml.tan(x[0]), {"nargs": 1}) - - self.register_function('asin', lambda x: self.ml.asin(x[0]), {"nargs": 1}) - self.register_function('acos', lambda x: self.ml.acos(x[0]), {"nargs": 1}) - self.register_function('atan', lambda x: self.ml.atan(x[0]), {"nargs": 1}) - - self.register_function('sinh', lambda x: self.ml.sinh(x[0]), {"nargs": 1}) - self.register_function('cosh', lambda x: self.ml.cosh(x[0]), {"nargs": 1}) - self.register_function('tanh', lambda x: self.ml.tanh(x[0]), {"nargs": 1}) - - self.register_function('asinh', lambda x: self.ml.asinh(x[0]), {"nargs": 1}) - self.register_function('acosh', lambda x: self.ml.acosh(x[0]), {"nargs": 1}) - self.register_function('atanh', lambda x: self.ml.atanh(x[0]), {"nargs": 1}) - - self.register_function('round', lambda x: self.ml.round(x[0]), {"nargs": 1}) - self.register_function('floor', lambda x: self.ml.floor(x[0]), {"nargs": 1}) - self.register_function('ceil', lambda x: self.ml.ceil(x[0]), {"nargs": 1}) - - self.register_function('rand', lambda x: self.ml.rand_float(), {"nargs": 0}) - self.register_function('randint', lambda x: self.ml.rand_int(), {"nargs": 0}) - - self.register_function('mod', lambda x: self.ml.mod(x[0], x[1]), {"nargs": 2}) - - self.register_function('factorize', lambda x: self.ml.factorize(x[0]), {"nargs": 1}) - - self.register_function('plot', lambda x: self.pl.plot(x[0], x[1]), {"nargs": 2, 'parse_options': False}) - - self.register_function('help', lambda x: EqnParserHelp.help(x[0]), {"nargs": 1, 'parse_options': False}) - - self.register_function('functions', lambda x: self.functions_string(), {"nargs": 0}) - self.register_function('variables', lambda x: self.variables_string(), {"nargs": 0}) - self.register_function('operators', lambda x: self.operators_string(), {"nargs": 0}) - - self.register_operator('+', self.OP_DIADIC, 0, lambda x: self.ml.add(x[0], x[1])) - self.register_operator('+', self.OP_PRE, 2, lambda x: x[0]) - self.register_operator('-', self.OP_DIADIC, 0, lambda x: self.ml.sub(x[0], x[1])) - self.register_operator('-', self.OP_PRE, 2, lambda x: self.ml.negate(x[0])) - self.register_operator('*', self.OP_DIADIC, 1, lambda x: self.ml.mul(x[0], x[1])) - self.register_operator(u'⨯', self.OP_DIADIC, 1, lambda x: self.ml.mul(x[0], x[1])) - self.register_operator(u'×', self.OP_DIADIC, 1, lambda x: self.ml.mul(x[0], x[1])) - self.register_operator('/', self.OP_DIADIC, 1, lambda x: self.ml.div(x[0], x[1])) - self.register_operator(u'÷', self.OP_DIADIC, 1, lambda x: self.ml.div(x[0], x[1])) - - self.register_operator('^', self.OP_DIADIC, 2, lambda x: self.ml.pow(x[0], x[1])) - self.register_operator('**', self.OP_DIADIC, 2, lambda x: self.ml.pow(x[0], x[1])) - - self.register_operator('!', self.OP_POST, 0, lambda x: self.ml.factorial(x[0])) - - self.register_operator('&', self.OP_DIADIC, 0, lambda x: x[0] and x[1]) - self.register_function('and', lambda x: x[0] and x[1], {"nargs": 2}) - self.register_operator('|', self.OP_DIADIC, 0, lambda x: x[0] or x[1]) - self.register_function('or', lambda x: x[0] or x[1], {"nargs": 2}) - self.register_operator('!', self.OP_PRE, 0, lambda x: not x[0]) - self.register_function('not', lambda x: not x[0], {"nargs": 1}) - -# self.register_operator('^', self.OP_PRE, 0, lambda x: not x[0]) - self.register_function('xor', lambda x: (x[0] and not x[1]) or (x[1] and not x[0]), {"nargs": 2}) - - self.register_operator('=', self.OP_DIADIC, 0, lambda x: x[0] == x[1]) - self.register_operator('!=', self.OP_DIADIC, 0, lambda x: x[0] != x[1]) - self.register_operator('<', self.OP_DIADIC, 0, lambda x: x[0] < x[1]) - self.register_operator('>', self.OP_DIADIC, 0, lambda x: x[0] > x[1]) - - self.register_operator('<<', self.OP_DIADIC, 0, lambda x: self.ml.shift_left(x[0], x[1])) - self.register_operator('>>', self.OP_DIADIC, 0, lambda x: self.ml.shift_right(x[0], x[1])) - - self.register_operator('%', self.OP_DIADIC, 2, lambda x: self.ml.mod(x[0], x[1])) - - self.set_var('help', - _("Use help(test) for help about 'test', or help(index) for the index"), - parse=False) - - def register_function(self, name, f, opts): - self.functions[name] = (f, opts) - self.functions[_(name)] = (f, opts) - - def register_operator(self, op, type, presedence, f): - self.operators.append((op, type, presedence, f)) - - if op[0] not in self.OP_START_CHARS: - self.OP_START_CHARS += op[0] - for c in op: - if c not in self.OP_CHARS: - self.OP_CHARS += c - - def get_diadic_operators(self): - if self.cached_diadic_ops == None: - self.cached_diadic_ops = [] - for (op, type, presedence, f) in self.operators: - if type == self.OP_DIADIC: - self.cached_diadic_ops.append(op) - return self.cached_diadic_ops - - def get_pre_operators(self): - if self.cached_pre_ops == None: - self.cached_pre_ops = [] - for (op, type, presedence, f) in self.operators: - if type == self.OP_PRE: - self.cached_pre_ops.append(op) - return self.cached_pre_ops - - def get_post_operators(self): - if self.cached_post_ops == None: - self.cached_post_ops = [] - for (op, type, presedence, f) in self.operators: - if type == self.OP_POST: - self.cached_post_ops.append(op) - return self.cached_post_ops - - def reset_variable_level(self, level): - return -# for i in self.variables.keys(): -# self.variables[i].highest_level = level - - def set_var(self, name, val, parse=True): - if type(val) is types.FloatType: - self.variables[name] = self.ml.d(val) - else: - self.variables[name] = val - self.parse_var[name] = parse - - def get_var(self, name): - return self.variables.get(name, None) - - def lookup_var(self, name, ps): - c = self.ml.get_constant(name) - if c is not None: - return c - - if name in self.variables.keys(): -# if self.variables[name].highest_level > 0 and self.variables[name].highest_level != ps.level: -# _logger.error('EqnParser.lookup_var(): recursion detected') -# return None -# self.variables[name].highest_level = level - if (type(self.variables[name]) is types.UnicodeType or type(self.variables[name]) is types.StringType) \ - and self.parse_var[name]: - return self.parse(self.variables[name], reset=False) - else: - return self.variables[name] - else: - _logger.debug('variable %s not defined', name) - ps.set_type(self.TYPE_SYMBOLIC) - return None - - def get_vars(self): - list = [] - for name in self.variables: - list.append((name, self.variables[name])) - return list - - def get_var_names(self, start=None): - names = self.variables.keys() - names.sort() - - if start is None: - return names - - retnames = [] - for name in names: - if name[:len(start)] == start: - retnames.append(name) - return retnames - - def get_function_names(self): - names = self.functions.keys() - names.sort() - return names - - def eval_func(self, func, args, level): - if func not in self.functions: - _logger.error(_("Function '%s' not defined"), func) - self.ps.set_error(ParserState.PARSE_ERROR, msg=_("Function '%s' not defined") % (func)) - return None - - (f, opts) = self.functions[func] - if len(args) != opts['nargs']: - _logger.error(_('Invalid number of arguments (%d instead of %d)'), len(args), opts['nargs']) - self.ps.set_error(ParserState.PARSE_ERROR, msg=_('function takes %d args') % (opts['nargs'])) - return None - - if 'parse_options' in opts and opts['parse_options'] == False: - pargs = args - else: - pargs = [] - for i in range(len(args)): - pargs.append(self.parse(args[i], reset=False)) - if pargs[i] is None: - _logger.error(_('Unable to parse argument %d: \'%s\''), i, args[i]) - self.ps.set_error(ParserState.PARSE_ERROR, msg=_('Unable to parse argument %d: \'%s\'') % (i, args[i])) - return None - - try: - res = f(pargs) - -# Maybe we should map exceptions to more obvious error messages - except Exception, inst: - res = None - self.ps.set_error(ParserState.PARSE_ERROR, msg=_("Function error: %s") % (str(inst))) - - _logger.debug('Function \'%s\' returned %s', func, self.ml.format_number(res)) - return res - - def parse_number(self, ps): - startofs = ps.ofs - -# integer part - while ps.more() and ps.char in self.DIGITS: - ps.next() - -# part after dot - if ps.char == '.' or ps.char == self.ml.fraction_sep: - ps.next() - while ps.more() and ps.char in self.DIGITS: - ps.next() - -# exponent - if ps.char is not None and ps.char in u'eE': - ps.next() - if ps.char is not None and ps.char in u'+-': - ps.next() - while ps.more() and ps.char in self.DIGITS: - ps.next() - - _logger.debug('parse_number(): %d - %d: %s', startofs, ps.ofs, ps.str[startofs:ps.ofs]) - n = self.ml.parse_number(ps.str[startofs:ps.ofs]) - return n - - def valid_operator(self, opstr, left_val): - for op_tuple in self.operators: - (op, type, presedence, f) = op_tuple - if op == opstr: - if type == self.OP_DIADIC and left_val is not None: - return op_tuple - elif type == self.OP_POST and left_val is not None: - return op_tuple - elif type == self.OP_PRE and left_val is None: - return op_tuple - return None - - def parse_operator(self, ps, left_val): - startofs = ps.ofs - op = None - while ps.more() and ps.char in self.OP_CHARS: - ps.next() - op2 = self.valid_operator(ps.str[startofs:ps.ofs], left_val) - if op2 is not None: - op = op2 - elif op is not None: - ps.prev() - break - - if op is not None: - _logger.debug('parse_operator(): %d - %d: %s', startofs, ps.ofs, ps.str[startofs:ps.ofs]) - return op - else: - return self.INVALID_OP - - def parse_func_args(self, ps): - startofs = ps.ofs - args = [] - pcount = 1 - while ps.more() and pcount > 0: - if ps.char == ',' and pcount == 1: - args.append(ps.str[startofs:ps.ofs]) - startofs = ps.ofs + 1 - elif ps.char == '(': - pcount += 1 - elif ps.char == ')': - pcount -= 1 - if pcount == 0 and (ps.ofs - startofs) > 0: - args.append(ps.str[startofs:ps.ofs]) - ps.next() - _logger.debug('parse_func_args(): %d - %d: %r', startofs, ps.ofs, args) - return args - - def parse_var_func(self, ps): - startofs = ps.ofs - while ps.more() and ps.char in self.NAME_CHARS: - ps.next() - name = ps.str[startofs:ps.ofs] - name.strip(self.SPACE_CHARS) - name.rstrip(self.SPACE_CHARS) - -# handle function - if ps.char == '(': - ps.next() - _logger.debug('parse_var_func(): function %d - %d: %s', startofs, ps.ofs, name) - args = self.parse_func_args(ps) - ret = self.eval_func(name, args, ps.level) - if ret is None: - ps.set_error_range((startofs, ps.ofs)) - return ret - -# handle var - else: - _logger.debug('parse_var_func(): variable %d - %d: %s', startofs, ps.ofs, name) - res = self.lookup_var(name, ps) - if res is None: - ps.set_error(ParserState.PARSE_ERROR, msg=_("Variable '%s' not defined") % (name), range=(startofs, ps.ofs)) - return res - - def _parse(self, ps, presedence=None): - if presedence is None: - ps.inc_level() - _logger.debug('_parse(): %s, presedence: %r', ps.state_string(), presedence) - - op = None - left_val = None - right_val = None - - while ps.more(): -# _logger.debug('Looking at %r, ofs %d in %r', ps.char, ps.ofs, ps.str) - -# Skip spaces - if ps.char in self.SPACE_CHARS: - ps.next() - -# Left parenthesis: parse sub-expression - elif ps.char == '(': - if not (left_val is None or left_val is not None and op is not None): - _logger.error('Parse error (left parenthesis)') - ps.set_error(ParserState.PARSE_ERROR, msg=_("Left parenthesis unexpected")) - return None - - ps.next() - left_val = self._parse(ps) - -# Right parenthesis: return from parsing sub-expression -# -If we are looking ahead because of operator presedence return value -# -Else move to next character, decrease level and return value - elif ps.char == ')': - if presedence is not None: - if left_val is None: - _logger.error(_('Parse error (right parenthesis)')) - ps.set_error(ParserState.PARSE_ERROR, msg=_("Right parenthesis unexpected")) - return None - else: - _logger.debug('returning %s', self.ml.format_number(left_val)) - return left_val - - if ps.level > 0: - ps.next() - ps.dec_level() - if left_val is None: - _logger.error(_('Parse error (right parenthesis, no left_val)')) - ps.set_error(ParserState.PARSE_ERROR, msg=_("Right parenthesis unexpected")) - return None - else: - _logger.debug('returning %s', self.ml.format_number(left_val)) - return left_val - else: - _logger.error(_('Parse error (right parenthesis, no level to close)')) - ps.set_error(ParserState.PARSE_ERROR, msg=_("Right parenthesis unexpected")) - return None - -# Parse number - elif ps.char == '.' or ps.char in self.DIGITS: - if right_val is not None or left_val is not None: - _logger.error(_('Number not expected')) - ps.set_error(ParserState.PARSE_ERROR, msg=_("Number not expected")) - return None - - left_val = self.parse_number(ps) - -# Parse operator - elif ps.char in self.OP_START_CHARS: - if op is not None: - ps.set_error(ParserState.PARSE_ERROR, msg=_("Operator not expected")) - return None - - startofs = ps.ofs - op = self.parse_operator(ps, left_val) - (opstr, otype, opres, of) = op - -# Diadic operators - if otype == self.OP_DIADIC: - if presedence is not None and opres <= presedence: - ps.set_ofs(startofs) - _logger.debug('returning %s (by presedence, %d)', self.ml.format_number(left_val), ps.ofs) - return left_val - else: - right_val = self._parse(ps, presedence=opres) - if right_val is None and self.ps.get_error() == ParserState.OK: - _logger.error(_('Parse error: number or variable expected')) - ps.set_error(ParserState.PARSE_ERROR, msg=_("Number or variable expected")) - return None - elif right_val is None: - return None - - res = of([left_val, right_val]) - _logger.debug('OP: %s (%r), %s (%r) ==> %s (%r)', self.ml.format_number(left_val), left_val, self.ml.format_number(right_val), right_val, self.ml.format_number(res), res) - left_val = res - right_val = None - op = None - -# Operator that goes after value - elif otype == self.OP_POST: - res = of([left_val]) - _logger.debug('OP POST: %s ==> %s', self.ml.format_number(left_val), self.ml.format_number(res)) - left_val = res - op = None - -# Operator that goes before value - elif otype == self.OP_PRE: - right_val = self._parse(ps, presedence=opres) - if right_val is None: - return None - left_val = of([right_val]) - _logger.debug('OP PRE: %s ==> %s', self.ml.format_number(right_val), self.ml.format_number(left_val)) - op = None - - elif otype == self.OP_INVALID: - _logger.debug('Invalid operator') - ps.set_error(ParserState.PARSE_ERROR, msg=_("Invalid operator"), range=(startofs, ps.ofs)) - return None - -# Parse variable or function - else: - if left_val is not None: - _logger.debug('Operator expected: %r', self.OP_START_CHARS) - ps.set_error(ParserState.PARSE_ERROR, msg=_("Operator expected")) - return None - - left_val = self.parse_var_func(ps) - - if not ps.more() and ps.level > 0: - _logger.debug('Parse error: \')\' expected') - ps.set_error(ParserState.PARSE_ERROR, msg=_("Right parenthesis unexpected")) - return None - elif op is None and left_val is not None: - _logger.debug('returning %s', self.ml.format_number(left_val)) - return left_val - else: - _logger.error(_('_parse(): returning None')) -# Should be set somewhere already -# ps.set_error(ParserState.PARSE_ERROR) - return None - - def get_error_offset(self): - return self.ps.error_range[0] - - def get_error_range(self): - return self.ps.error_range - - def parse(self, eqn, reset=True): - """Construct ParserState object and call _parse""" - - # Parse everything in unicode - if type(eqn) is types.StringType: - eqn = unicode(eqn) - - _logger.debug('parse(): %r', eqn) - self.reset_variable_level(0) - - if reset: - self.ps = None - - oldps = self.ps - self.ps = ParserState(eqn) - ret = self._parse(self.ps) - if oldps is not None: - oldps.copy_error(self.ps) - self.ps = oldps - - return ret - - def functions_string(self): - ret = "" - for key in self.functions.keys(): - ret += key + " " - return ret - - def variables_string(self): - ret = "" - for key in self.variables.keys(): - ret += key + " " - return ret - - def operators_string(self): - ret = "" - for (op, type, p, f) in self.operators: - ret += op + " " - return ret - diff --git a/functions.py b/functions.py new file mode 100644 index 0000000..b069581 --- /dev/null +++ b/functions.py @@ -0,0 +1,401 @@ +# functions.py, functions available in Calculate, +# by Reinier Heeres <reinier@heeres.eu> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +# Any variable or function in this module that does not start with an +# underscore ('_') will be available in Calculate through astparser.py. +# Docstrings will automatically be added to the help index for that function. +# However, instead of setting the docstring on a function in the simple way we +# add it after the function definition so that they can more easily be +# localized through gettext. + +import types +import math +import random +from decimal import Decimal as _Decimal +from rational import Rational as _Rational + +from gettext import gettext as _ + +# List of functions to allow translating the function names. +_FUNCTIONS = [ + _('add'), + _('abs'), + _('acos'), + _('acosh'), + _('asin'), + _('asinh'), + _('atan'), + _('atanh'), + _('and'), + _('ceil'), + _('cos'), + _('cosh'), + _('div'), + _('gcd'), + _('exp'), + _('factorial'), + _('fac'), + _('factorize'), + _('floor'), + _('is_int'), + _('ln'), + _('log10'), + _('mul'), + _('or'), + _('rand_float'), + _('rand_int'), + _('round'), + _('sin'), + _('sinh'), + _('sinc'), + _('sqrt'), + _('sub'), + _('tan'), + _('tanh'), + _('xor'), + ] + +def _d(val): + '''Return a _Decimal object.''' + + if isinstance(val, _Decimal): + return val + elif type(val) in (types.IntType, types.LongType): + return _Decimal(val) + elif type(val) == types.StringType: + d = _Decimal(val) + return d.normalize() + elif type(val) is types.FloatType or hasattr(val, '__float__'): + s = '%.10e' % float(val) + d = _Decimal(s) + return d.normalize() + else: + return None + +_angle_scaling = 1 +def _scale_angle(x): + return x * _angle_scaling + +def _inv_scale_angle(x): + return x / _angle_scaling + +def abs(x): + return math.fabs(x) +abs.__doc__ = _( +'abs(x), return absolute value of x, which means -x for x < 0') + +def acos(x): + return _inv_scale_angle(math.acos(x)) +acos.__doc__ = _( +'acos(x), return the arc cosine of x. This is the angle for which the cosine \ +is x. Defined for -1 <= x < 1') + +def acosh(x): + return math.acosh(x) +acosh.__doc__ = _( +'acosh(x), return the arc hyperbolic cosine of x. This is the value y for \ +which the hyperbolic cosine equals x.') + +def And(x, y): + return x & y +And.__doc__ = _( +'And(x, y), logical and. Returns True if x and y are True, else returns False') + +def add(x, y): + if isinstance(x, _Decimal) or isinstance(y, _Decimal): + x = _d(x) + y = _d(y) + return x + y +add.__doc__ = _('add(x, y), return x + y') + +def asin(x): + return _inv_scale_angle(math.asin(x)) +asin.__doc__ = _( +'asin(x), return the arc sine of x. This is the angle for which the sine is ix. \ +Defined for -1 <= x <= 1') + +def asinh(x): + return math.asinh(x) +asinh.__doc__ = _( +'asinh(x), return the arc hyperbolic sine of x. This is the value y for \ +which the hyperbolic sine equals x.') + +def atan(x): + return _inv_scale_angle(math.atan(x)) +atan.__doc__ = _( +'atan(x), return the arc tangent of x. This is the angle for which the tangent \ +is x. Defined for all x') + +def atanh(x): + return math.atanh(x) +atanh.__doc__ = _( +'atanh(x), return the arc hyperbolic tangent of x. This is the value y for \ +which the hyperbolic tangent equals x.') + +def ceil(x): + return math.ceil(float(x)) +ceil.__doc__ = _('ceil(x), return the smallest integer larger than x.') + +def cos(x): + return math.cos(_scale_angle(x)) +cos.__doc__ = _( +'cos(x), return the cosine of x. This is the x-coordinate on the unit circle \ +at the angle x') + +def cosh(x): + return math.cosh(x) +cosh.__doc__ = _( +'cosh(x), return the hyperbolic cosine of x. Given by (exp(x) + exp(-x)) / 2') + +def div(x, y): + if y == 0 or y == 0.0: + raise ValueError(_('Can not divide by zero')) + + if is_int(x) and float(abs(x)) < 1e12 and \ + is_int(y) and float(abs(y)) < 1e12: + return _Rational(x, y) + + if isinstance(x, _Decimal) or isinstance(y, _Decimal): + x = _d(x) + y = _d(y) + + return x / y + +def _do_gcd(a, b): + if b == 0: + return a + else: + return self._do_gcd(b, a % b) + +def gcd(self, a, b): + TYPES = (types.IntType, types.LongType) + if type(a) not in TYPES or type(b) not in types: + raise ValueError(_('Invalid argument')) + return self._do_gcd(a, b) +gcd.__doc__ = _( +'gcd(a, b), determine the greatest common denominator of a and b. \ +For example, the biggest factor that is shared by the numbers 15 and 18 is 3.') + +def exp(x): + return math.exp(float(x)) +exp.__doc__ = _('exp(x), return the natural exponent of x. Given by e^x') + +def factorial(n): + if type(n) not in (types.IntType, types.LongType): + raise ValueError(_('Factorial only defined for integers')) + + if n == 0: + return 1 + + n = long(n) + res = long(n) + while n > 2: + res *= n - 1 + n -= 1 + + return res +factorial.__doc__ = _( +'factorial(n), return the factorial of n. \ +Given by n * (n - 1) * (n - 2) * ...') + +def fac(x): + return factorial(x) +fac.__doc__ = _( +'fac(x), return the factorial of x. Given by x * (x - 1) * (x - 2) * ...') + +def factorize(x): + if not is_int(x): + return 0 + + factors = [] + num = x + i = 2 + while i <= math.sqrt(num): + if num % i == 0: + factors.append(i) + num /= i + i = 2 + elif i == 2: + i += 1 + else: + i += 2 + factors.append(num) + + if len(factors) == 1: + return "1 * %d" % x + else: + ret = "%d" % factors[0] + for fac in factors[1:]: + ret += " * %d" % fac + return ret +factorize.__doc__ = ( +'factorize(x), determine the prime factors that together form x. \ +For examples: 15 = 3 * 5.') + +def floor(x): + return math.floor(float(x)) +floor.__doc__ = _('floor(x), return the largest integer smaller than x.') + +def is_int(n): + if type(n) in (types.IntType, types.LongType): + return True + + if not isinstance(n, _Decimal): + n = _d(n) + if n is None: + return False + + + (sign, d, e) = n.normalize().as_tuple() + return e >= 0 +is_int.__doc__ = ('is_int(n), determine whether n is an integer.') + +def ln(x): + if float(x) > 0: + return math.log(float(x)) + else: + raise ValueError(_('Logarithm(x) only defined for x > 0')) +ln.__doc__ = _( +'ln(x), return the natural logarithm of x. This is the value for which the \ +exponent exp() equals x. Defined for x >= 0.') + +def log10(x): + if float(x) > 0: + return math.log(float(x)) + else: + raise ValueError(_('Logarithm(x) only defined for x > 0')) +log10.__doc__ = _( +'log10(x), return the base 10 logarithm of x. This is the value y for which \ +10^y equals x. Defined for x >= 0.') + +def mod(x, y): + if self.is_int(y): + return x % y + else: + raise ValueError(_('Can only calculate x modulo <integer>')) + +def mul(x, y): + if isinstance(x, _Decimal) or isinstance(y, _Decimal): + x = _d(x) + y = _d(y) + return x * y +mul.__doc__ = _('mul(x, y), return x * y') + +def negate(x): + return -x +negate.__doc__ = _('negate(x), return -x') + +def Or(x, y): + return x | y +Or.__doc__ = _( +'Or(x, y), logical or. Returns True if x or y is True, else returns False') + +def pow(x, y): + if is_int(y): + if is_int(x): + return long(x) ** int(y) + elif hasattr(x, '__pow__'): + return x ** y + else: + return float(x) ** int(y) + else: + if isinstance(x, _Decimal) or isinstance(y, _Decimal): + x = _d(x) + y = _d(y) + return _d(math.pow(float(x), float(y))) +pow.__doc__ = _('pow(x, y), return x to the power y (x**y)') + +def rand_float(): + return random.random() +rand_float.__doc__ = _( +'rand_float(), return a random floating point number between 0.0 and 1.0') + +def rand_int(maxval=65535): + return random.randint(0, maxval) +rand_int.__doc__ = _( +'rand_int([<maxval>]), return a random integer between 0 and <maxval>. \ +<maxval> is an optional argument and is set to 65535 by default.') + +def round(x): + return math.round(float(x)) +round.__doc__ = _('round(x), return the integer nearest to x.') + +def shift_left(x, y): + if is_int(x) and is_int(y): + return d(int(x) << int(y)) + else: + raise ValueError(_('Bitwise operations only apply to integers')) +shift_left.__doc__ = _( +'shift_left(x, y), shift x by y bits to the left (multiply by 2 per bit)') + +def shift_right(self, x, y): + if is_int(x) and is_int(y): + return d(int(x) >> int(y)) + else: + raise ValueError(_('Bitwise operations only apply to integers')) +shift_right.__doc__ = _( +'shift_right(x, y), shift x by y bits to the right (divide by 2 per bit)') + +def sin(x): + return math.sin(_scale_angle(x)) +sin.__doc__ = _( +'sin(x), return the sine of x. This is the y-coordinate on the unit circle at \ +the angle x') + +def sinh(x): + return math.sinh(x) +sinh.__doc__ = _( +'sinh(x), return the hyperbolic sine of x. Given by (exp(x) - exp(-x)) / 2') + +def sinc(x): + if float(x) == 0.0: + return 1 + return sin(x) / x +sinc.__doc__ = _( +'sinc(x), return the sinc of x. This is given by sin(x) / x.') + +def sqrt(x): + return math.sqrt(float(x)) +sqrt.__doc__ = _( +'sqrt(x), return the square root of x. This is the value for which the square \ +equals x. Defined for x >= 0.') + +def sub(x, y): + if isinstance(x, _Decimal) or isinstance(y, _Decimal): + x = _d(x) + y = _d(y) + return x - y +add.__doc__ = _('sub(x, y), return x - y') + +def tan(x): + return math.tan(_scale_angle(x)) +tan.__doc__ = _( +'tan(x), return the tangent of x. This is the slope of the line from the origin \ +of the unit circle to the point on the unit circle defined by the angle x. Given \ +by sin(x) / cos(x)') + +def tanh(x): + return math.tanh(x) +tanh.__doc__ = _( +'tanh(x), return the hyperbolic tangent of x. Given by sinh(x) / cosh(x)') + +def xor(x, y): + return x ^ y +xor.__doc__ = _( +'xor(x, y), logical xor. Returns True if either x is True (and y is False) \ +or y is True (and x is False), else returns False') + @@ -35,37 +35,12 @@ class MathLib: ANGLE_RAD = 1 ANGLE_GRAD = 1 - MATH_FUNCTIONS = ( - 'is_int', 'is_bool', 'compare', 'negate', 'abs', 'add', 'sub', - 'mul', 'div', 'pow', 'sqrt', 'mod', 'exp', 'ln', 'log10', 'factorial', - 'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'sinh', 'cosh', 'tanh', - 'asinh', 'acosh', 'atanh', 'round', 'floor', 'ceil', 'rand_float', - 'rand_int', 'shift_left', 'shift_right', 'factorize', - ) - def __init__(self): self.constants = {} self.set_angle_type(self.ANGLE_DEG) self._setup_i18n() -#Constants should maybe become variables in eqnparser.py - self.set_constant('true', True) - self.set_constant('True', True) - self.set_constant('false', False) - self.set_constant('False', False) - self.set_constant('pi', self.parse_number('3.1415926535')) - self.set_constant('kb', self.parse_number('1.380650524e-23')) - self.set_constant('Na', self.parse_number('6.02214e23')) - self.set_constant('e', self.exp(1)) - self.set_constant('c', self.parse_number('2.99792458e8')) - self.set_constant('c_e', self.parse_number('-1.60217648740e-19')) #electron properties - self.set_constant('m_e', self.parse_number('9.109382616e-31')) - self.set_constant('c_p', self.parse_number('1.6021765314e-19')) #proton properties - self.set_constant('m_p', self.parse_number('1.6726217129e-27')) - self.set_constant('c_n', self.parse_number('0')) #neutron properties - self.set_constant('m_n', self.parse_number('1.6749272928e-27')) - def _setup_i18n(self): loc = locale.localeconv() @@ -93,27 +68,6 @@ class MathLib: self.angle_scaling = self.d(type) _logger.debug('Angle type set to:%s', self.angle_scaling) - def set_constant(self, name, val): - self.constants[name] = val - - def get_constant(self, name): - if name in self.constants: - return self.constants[name] - else: - return None - - def get_constants(self): - return self.constants - - def get_math_functions(self): - ret = {} - items = inspect.getmembers(self) - for key, val in items: - if key in self.MATH_FUNCTIONS: - ret[key] = val - - return ret - def d(self, val): if isinstance(val, Decimal): return val @@ -227,195 +181,3 @@ class MathLib: (sign, d, e) = n.normalize().as_tuple() return e >= 0 - - def is_float(self, n): - if isinstance(n, Decimal): - return not self.is_int(n) - else: - return False - - def is_bool(self, n): - return type(n) is types.BoolType - - def compare(self, x, y): - return x == y - - def negate(self, x): - return -x - - def abs(self, x): - return self.d(math.fabs(x)) - - def add(self, x, y): - if isinstance(x, Decimal) or isinstance(y, Decimal): - x = self.d(x) - y = self.d(y) - return x + y - - def sub(self, x, y): - if isinstance(x, Decimal) or isinstance(y, Decimal): - x = self.d(x) - y = self.d(y) - return x - y - - def mul(self, x, y): - if isinstance(x, Decimal) or isinstance(y, Decimal): - x = self.d(x) - y = self.d(y) - return x * y - - def div(self, x, y): - if y == 0 or y == 0.0: - return _('Undefined') - - if self.is_int(x) and float(self.abs(y)) < 1e12 and \ - self.is_int(x) and float(self.abs(y)) < 1e12: - return Rational(x, y) - - if isinstance(x, Decimal) or isinstance(y, Decimal): - x = self.d(x) - y = self.d(y) - - return x / y - - def pow(self, x, y): - if self.is_int(y): - if self.is_int(x): - return long(x) ** int(y) - elif hasattr(x, '__pow__'): - return x ** y - else: - return float(x) ** int(y) - else: - if isinstance(x, Decimal) or isinstance(y, Decimal): - x = self.d(x) - y = self.d(y) - return self.d(math.pow(float(x), float(y))) - - def sqrt(self, x): - return self.d(math.sqrt(float(x))) - - def mod(self, x, y): - if self.is_int(y): - return x % y - else: - return self.d(0) - - def exp(self, x): - return self.d(math.exp(float(x))) - - def ln(self, x): - if float(x) > 0: - return self.d(math.log(float(x))) - else: - return 0 - - def log10(self, x): - if float(x) > 0: - return self.d(math.log10(float(x))) - else: - return 0 - - def factorial(self, n): - if not self.is_int(n): - return self.d(0) - - if n == 0: - return 1 - - n = long(n) - res = long(n) - while n > 2: - res *= n - 1 - n -= 1 - - return res - - def sin(self, x): - return self.d(math.sin(float(x * self.angle_scaling))) - - def cos(self, x): - return self.d(math.cos(float(x * self.angle_scaling))) - - def tan(self, x): - return self.d(math.tan(float(x * self.angle_scaling))) - - def asin(self, x): - return self.d(math.asin(float(x))) / self.angle_scaling - - def acos(self, x): - return self.d(math.acos(float(x))) / self.angle_scaling - - def atan(self, x): - return self.d(math.atan(float(x))) / self.angle_scaling - - def sinh(self, x): - return self.d(math.sinh(float(x))) - - def cosh(self, x): - return self.d(math.cosh(float(x))) - - def tanh(self, x): - return self.d(math.tanh(float(x))) - - def asinh(self, x): - return self.d(math.asinh(float(x))) - - def acosh(self, x): - return self.d(math.acosh(float(x))) - - def atanh(self, x): - return self.d(math.atanh(float(x))) - - def round(self, x): - return self.d(round(float(x))) - - def floor(self, x): - return self.d(math.floor(float(x))) - - def ceil(self, x): - return self.d(math.ceil(float(x))) - - def rand_float(self): - return self.d(random.random()) - - def rand_int(self): - return self.d(random.randint(0, 65535)) - - def shift_left(self, x, y): - if self.is_int(x) and self.is_int(y): - return self.d(int(x) << int(y)) - else: - return 0 - - def shift_right(self, x, y): - if self.is_int(x) and self.is_int(y): - return self.d(int(x) >> int(y)) - else: - return 0 - - def factorize(self, x): - if not self.is_int(x): - return 0 - - factors = [] - num = x - i = 2 - while i <= math.sqrt(num): - if num % i == 0: - factors.append(i) - num /= i - i = 2 - elif i == 2: - i += 1 - else: - i += 2 - factors.append(num) - - if len(factors) == 1: - return "1 * %d" % x - else: - str = "%d" % factors[0] - for fac in factors[1:]: - str += " * %d" % fac - return str @@ -39,27 +39,25 @@ class PlotLib: def get_svg(self): return self.svg_data - def parse_range(self, range): - p1 = range.split('=') - if len(p1) != 2: - return None - p2 = p1[1].split('..') - if len(p2) != 2: - return None - return (p1[0], (float(p2[0]), float(p2[1]))) - - def evaluate(self, eqn, var, range, n=100): + def evaluate(self, eqn, var, range, points=100): x_old = self.parser.get_var(var) + if type(eqn) in (types.StringType, types.UnicodeType): + eqn = self.parser.parse(eqn) + res = [] - d = float((range[1] - range[0])) / (n - 1) + d = float((range[1] - range[0])) / (points - 1) x = range[0] - while n > 0: + while points > 0: self.parser.set_var(var, x) - v = float(self.parser.parse(eqn)) + ret = self.parser.evaluate(eqn) + if ret is not None: + v = float(ret) + else: + v = 0 res.append((x, v)) x += d - n -= 1 + points -= 1 self.parser.set_var(var, x_old) return res @@ -104,11 +102,17 @@ class PlotLib: self.maxx = max(float(x), self.maxx) self.maxy = max(float(y), self.maxy) - x_space = 0.02 * (self.maxx - self.minx) + if self.minx == self.maxx: + xYspace = 0.5 + else: + x_space = 0.02 * (self.maxx - self.minx) self.minx -= x_space self.maxx += x_space - y_space = 0.02 * (self.maxy - self.miny) + if self.miny == self.maxy: + y_space = 0.5 + else: + y_space = 0.02 * (self.maxy - self.miny) self.miny -= y_space self.maxy += y_space @@ -150,21 +154,39 @@ class PlotLib: f.write(self.svg_data) f.close() - def plot(self, eqn, range_spec): - _logger.debug('plot(): %r, %r', eqn, range_spec) + def plot(self, eqn, **kwargs): + ''' + Plot function <eqn>. + + kwargs can contain: 'points' + + The last item in kwargs is interpreted as the variable that should + be varied. + ''' + + _logger.debug('plot(): %r, %r', eqn, kwargs) + + if 'points' in kwargs: + points = kwargs['points'] + del kwargs['points'] + else: + points = 100 + + if len(kwargs) > 1: + _logger.error('Too many variables specified') + return None - (var, range) = self.parse_range(range_spec) - if range is None: - _logger.error('Unable to parse range') - return False + for var, range in kwargs.iteritems(): + pass _logger.info('Plot range for var %s: %r', var, range) self.set_size(250, 250) self.create_image() - self.draw_axes(var, eqn) + # FIXME: should use equation as label + self.draw_axes(var, 'f(x)') - vals = self.evaluate(eqn, var, range) + vals = self.evaluate(eqn, var, range, points=points) # print 'vals: %r' % vals self.add_curve(vals) |