overhauled calc module
This commit is contained in:
parent
d8caed033e
commit
deeaf9ecca
|
@ -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`
|
||||
|
|
|
@ -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.')
|
||||
|
|
|
@ -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 (%).
|
||||
"""
|
Loading…
Reference in New Issue
Block a user