overhauled calc module

This commit is contained in:
iou1name 2020-01-31 09:47:46 -05:00
parent d8caed033e
commit deeaf9ecca
3 changed files with 27 additions and 211 deletions

View File

@ -3,4 +3,4 @@ It's like Sopel, except rewritten from scratch using Twisted as a base and over
## Requirements
Python 3.6+
Python packages: `twisted python-dateutil requests bs4 wolframalpha emoji pillow ipython numpy numpngw`
Python packages: `twisted python-dateutil requests bs4 wolframalpha emoji pillow ipython numpy numpngw numexpr`

View File

@ -2,30 +2,44 @@
"""
A basic calculator and python interpreter application.
"""
import sys
import multiprocessing
import numpy
import numexpr
import requests
from module import commands, example
from tools.calculation import eval_equation
BASE_TUMBOLIA_URI = 'https://tumbolia-two.appspot.com/'
@commands('calc', 'c')
@example('.c 5 + 3', '8')
def c(bot, trigger):
"""Evaluate some calculation."""
"""Evaluates a mathematical expression."""
if len(trigger.args) < 2:
return bot.reply("Nothing to calculate.")
eqn = ' '.join(trigger.args[1:])
expr = ' '.join(trigger.args[1:])
def worker(expr, return_dict):
with numpy.errstate(all='ignore'):
try:
result = eval_equation(eqn)
result = "{:.10g}".format(result)
except ZeroDivisionError:
result = "Division by zero is not supported in this universe."
except Exception as e:
result = "{error}: {msg}".format(error=type(e), msg=e)
bot.reply(result)
value = numexpr.evaluate(expr, local_dict={}, global_dict={})
value = value.item()
except:
value = "Error. The expression was not understood."
return_dict['value'] = value
with multiprocessing.Manager() as manager:
d = manager.dict()
proc = multiprocessing.Process(target=worker, args=(expr, d))
proc.start()
proc.join(1)
if proc.is_alive():
proc.kill()
proc.join()
proc.close()
return bot.reply("Calculation timed out.")
bot.reply(str(d['value']))
@commands('py')
@ -42,7 +56,6 @@ def py(bot, trigger):
answer = res.text
answer = answer[:2000]
if answer:
#bot.msg can potentially lead to 3rd party commands triggering.
bot.msg(answer, length=300)
else:
bot.reply('Sorry, no result.')

View File

@ -1,197 +0,0 @@
#!/usr/bin/env python3
"""
Tools to help safely do calculations from user input.
"""
import time
import numbers
import operator
import ast
__all__ = ['eval_equation']
class ExpressionEvaluator:
"""
A generic class for evaluating limited forms of Python expressions.
Instances can overwrite binary_ops and unary_ops attributes with dicts of
the form {ast.Node, function}. When the ast.Node being used as key is
found, it will be evaluated using the given function.
"""
class Error(Exception):
pass
def __init__(self, bin_ops=None, unary_ops=None):
self.binary_ops = bin_ops or {}
self.unary_ops = unary_ops or {}
def __call__(self, expression_str, timeout=5.0):
"""
Evaluate a python expression and return the result.
Raises:
SyntaxError: If the given expression_str is not a valid python
statement.
ExpressionEvaluator.Error: If the instance of ExpressionEvaluator
does not have a handler for the ast.Node.
"""
ast_expression = ast.parse(expression_str, mode='eval')
return self._eval_node(ast_expression.body, time.time() + timeout)
def _eval_node(self, node, timeout):
"""
Recursively evaluate the given ast.Node.
Uses self.binary_ops and self.unary_ops for the implementation.
A subclass could overwrite this to handle more nodes, calling it only
for nodes it does not implement it self.
Raises:
ExpressionEvaluator.Error: If it can't handle the ast.Node.
"""
if isinstance(node, ast.Num):
return node.n
elif (isinstance(node, ast.BinOp) and
type(node.op) in self.binary_ops):
left = self._eval_node(node.left, timeout)
right = self._eval_node(node.right, timeout)
if time.time() > timeout:
raise ExpressionEvaluator.Error(
"Time for evaluating expression ran out.")
return self.binary_ops[type(node.op)](left, right)
elif (isinstance(node, ast.UnaryOp) and
type(node.op) in self.unary_ops):
operand = self._eval_node(node.operand, timeout)
if time.time() > timeout:
raise ExpressionEvaluator.Error(
"Time for evaluating expression ran out.")
return self.unary_ops[type(node.op)](operand)
raise ExpressionEvaluator.Error(
"Ast.Node '%s' not implemented." % (type(node).__name__,))
def guarded_mul(left, right):
"""Decorate a function to raise an error for values > limit."""
# Only handle ints because floats will overflow anyway.
if not isinstance(left, numbers.Integral):
pass
elif not isinstance(right, numbers.Integral):
pass
elif left in (0, 1) or right in (0, 1):
# Ignore trivial cases.
pass
elif left.bit_length() + right.bit_length() > 664386:
# 664386 is the number of bits (10**100000)**2 has, which is instant on
# my laptop, while (10**1000000)**2 has a noticeable delay. It could
# certainly be improved.
raise ValueError(
"Value is too large to be handled in limited time and memory.")
return operator.mul(left, right)
def pow_complexity(num, exp):
"""
Estimate the worst case time pow(num, exp) takes to calculate.
This function is based on experimetal data from the time it takes to
calculate "num**exp" on laptop with i7-2670QM processor on a 32 bit
CPython 2.7.6 interpreter on Windows.
It tries to implement this surface: x=exp, y=num
1e5 2e5 3e5 4e5 5e5 6e5 7e5 8e5 9e5
e1 0.03 0.09 0.16 0.25 0.35 0.46 0.60 0.73 0.88
e2 0.08 0.24 0.46 0.73 1.03 1.40 1.80 2.21 2.63
e3 0.15 0.46 0.87 1.39 1.99 2.63 3.35 4.18 5.15
e4 0.24 0.73 1.39 2.20 3.11 4.18 5.39 6.59 7.88
e5 0.34 1.03 2.00 3.12 4.48 5.97 7.56 9.37 11.34
e6 0.46 1.39 2.62 4.16 5.97 7.86 10.09 12.56 15.39
e7 0.60 1.79 3.34 5.39 7.60 10.16 13.00 16.23 19.44
e8 0.73 2.20 4.18 6.60 9.37 12.60 16.26 19.83 23.70
e9 0.87 2.62 5.15 7.93 11.34 15.44 19.40 23.66 28.58
For powers of 2 it tries to implement this surface:
1e7 2e7 3e7 4e7 5e7 6e7 7e7 8e7 9e7
1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
2 0.21 0.44 0.71 0.92 1.20 1.49 1.66 1.95 2.23
4 0.43 0.91 1.49 1.96 2.50 3.13 3.54 4.10 4.77
8 0.70 1.50 2.24 3.16 3.83 4.66 5.58 6.56 7.67
The function number were selected by starting with the theoretical
complexity of exp * log2(num)**2 and fiddling with the exponents
untill it more or less matched with the table.
Because this function is based on a limited set of data it might
not give accurate results outside these boundaries. The results
derived from large num and exp were quite accurate for small num
and very large exp though, except when num was a power of 2.
"""
if num in (0, 1) or exp in (0, 1):
return 0
elif (num & (num - 1)) == 0:
# For powers of 2 the scaling is a bit different.
return exp ** 1.092 * num.bit_length() ** 1.65 / 623212911.121
else:
return exp ** 1.590 * num.bit_length() ** 1.73 / 36864057619.3
def guarded_pow(left, right):
# Only handle ints because floats will overflow anyway.
if not isinstance(left, numbers.Integral):
pass
elif not isinstance(right, numbers.Integral):
pass
elif pow_complexity(left, right) < 0.5:
# Value 0.5 is arbitary and based on a estimated runtime of 0.5s
# on a fairly decent laptop processor.
pass
else:
raise ValueError("Pow expression too complex to calculate.")
return operator.pow(left, right)
class EquationEvaluator(ExpressionEvaluator):
__bin_ops = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: guarded_mul,
ast.Div: operator.truediv,
ast.Pow: guarded_pow,
ast.Mod: operator.mod,
ast.FloorDiv: operator.floordiv,
ast.BitXor: guarded_pow
}
__unary_ops = {
ast.USub: operator.neg,
ast.UAdd: operator.pos,
}
def __init__(self):
ExpressionEvaluator.__init__(
self,
bin_ops=self.__bin_ops,
unary_ops=self.__unary_ops
)
def __call__(self, expression_str):
result = ExpressionEvaluator.__call__(self, expression_str)
# This wrapper is here so additional sanity checks could be done
# on the result of the eval, but currently none are done.
return result
eval_equation = EquationEvaluator()
"""
Evaluates a Python equation expression and returns the result.
Supports addition (+), subtraction (-), multiplication (*), division (/),
power (**) and modulo (%).
"""