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
|
## Requirements
|
||||||
Python 3.6+
|
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.
|
A basic calculator and python interpreter application.
|
||||||
"""
|
"""
|
||||||
import sys
|
import multiprocessing
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
import numexpr
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from module import commands, example
|
from module import commands, example
|
||||||
from tools.calculation import eval_equation
|
|
||||||
|
|
||||||
BASE_TUMBOLIA_URI = 'https://tumbolia-two.appspot.com/'
|
BASE_TUMBOLIA_URI = 'https://tumbolia-two.appspot.com/'
|
||||||
|
|
||||||
@commands('calc', 'c')
|
@commands('calc', 'c')
|
||||||
@example('.c 5 + 3', '8')
|
@example('.c 5 + 3', '8')
|
||||||
def c(bot, trigger):
|
def c(bot, trigger):
|
||||||
"""Evaluate some calculation."""
|
"""Evaluates a mathematical expression."""
|
||||||
if len(trigger.args) < 2:
|
if len(trigger.args) < 2:
|
||||||
return bot.reply("Nothing to calculate.")
|
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:
|
try:
|
||||||
result = eval_equation(eqn)
|
value = numexpr.evaluate(expr, local_dict={}, global_dict={})
|
||||||
result = "{:.10g}".format(result)
|
value = value.item()
|
||||||
except ZeroDivisionError:
|
except:
|
||||||
result = "Division by zero is not supported in this universe."
|
value = "Error. The expression was not understood."
|
||||||
except Exception as e:
|
return_dict['value'] = value
|
||||||
result = "{error}: {msg}".format(error=type(e), msg=e)
|
|
||||||
bot.reply(result)
|
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')
|
@commands('py')
|
||||||
|
@ -42,7 +56,6 @@ def py(bot, trigger):
|
||||||
answer = res.text
|
answer = res.text
|
||||||
answer = answer[:2000]
|
answer = answer[:2000]
|
||||||
if answer:
|
if answer:
|
||||||
#bot.msg can potentially lead to 3rd party commands triggering.
|
|
||||||
bot.msg(answer, length=300)
|
bot.msg(answer, length=300)
|
||||||
else:
|
else:
|
||||||
bot.reply('Sorry, no result.')
|
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