import ast
import operator as op
import math
from csbot.plugin import Plugin
from csbot.util import pairwise
[docs]def is_too_long(n):
# Don't care about floats
return isinstance(n, int) and n != 0 and math.log10(abs(n)) > 127
[docs]def guarded_power(a, b):
"""A limited power function to make sure that
commands do not take too long to process.
"""
if any(abs(n) > 1000 for n in [a, b]):
raise CalcError("would take too long to calculate")
try:
return op.pow(a, b)
except OverflowError:
raise CalcError("too large to represent as float")
[docs]def guarded_lshift(a, b):
if not (isinstance(a, int) and isinstance(b, int)):
raise CalcError("non-integer shift values")
elif b.bit_length() > 64:
# Only need to check how much the number is being shifted by
raise CalcError("would take too long to calculate")
return op.lshift(a, b)
[docs]def guarded_rshift(a, b):
if not (isinstance(a, int) and isinstance(b, int)):
raise CalcError("non-integer shift values")
return op.rshift(a, b)
# Available operators
operators = {
# boolop
ast.And: op.and_,
ast.Or: op.or_,
# operator
ast.Add: op.add,
ast.Sub: op.sub,
ast.Mult: op.mul,
ast.Div: op.truediv,
ast.Mod: op.mod,
ast.BitOr: op.or_,
ast.BitXor: op.xor,
ast.BitAnd: op.and_,
ast.FloorDiv: op.floordiv,
ast.Pow: guarded_power,
ast.LShift: guarded_lshift,
ast.RShift: guarded_rshift,
# unaryop
ast.Invert: op.inv,
ast.Not: op.not_,
ast.UAdd: op.pos,
ast.USub: op.neg,
# cmpop
ast.Eq: op.eq,
ast.NotEq: op.ne,
ast.Lt: op.lt,
ast.LtE: op.le,
ast.Gt: op.gt,
ast.GtE: op.ge,
ast.Is: op.is_,
ast.IsNot: op.is_not,
}
[docs]def guarded_factorial(a):
# Any larger than this would be too long to output regardless
if a > 100:
raise CalcError("would take too long to calculate")
return math.factorial(a)
identifiers = {
# Available constants
"e": math.e,
"pi": math.pi,
"π": math.pi,
"F": 1.2096, # Barrucadu's Constant (seconds in a microfortnight)
"c": 299792458, # m s-1
"G": 6.6738480e-11, # m3 kg-1 s-2
"h": 6.6260695729e-34, # J s
"N": 6.0221412927e23, # mol-1
# Available functions
"ceil": math.ceil,
"factorial": guarded_factorial,
"floor": math.floor,
"isfinite": math.isfinite,
"isinf": math.isinf,
"isnan": math.isnan,
"exp": math.exp,
"log": math.log,
"sqrt": math.sqrt,
# Trig
"acos": math.acos,
"asin": math.asin,
"atan": math.atan,
"cos": math.cos,
"sin": math.sin,
"tan": math.tan,
"degrees": math.degrees,
"deg": math.degrees,
"radians": math.radians,
"rad": math.radians,
}
[docs]class CalcEval(ast.NodeVisitor):
[docs] def visit_Module(self, node):
return self.visit(node.body[0]) # Special case, since we're only dealing with one-liners
[docs] def visit_Expr(self, node):
return self.visit(node.value) # Reimplementation needed or it goes via generic_visit
[docs] def visit_BinOp(self, node):
left = self.visit(node.left)
right = self.visit(node.right)
if node.op.__class__ in (ast.Mod, ast.Div, ast.FloorDiv) and right == 0:
raise CalcError("division by zero")
try:
operator = operators[node.op.__class__]
except KeyError:
raise CalcError("invalid operator")
try:
return operator(left, right)
except TypeError:
raise CalcError("invalid arguments")
[docs] def visit_UnaryOp(self, node):
try:
operator = operators[node.op.__class__]
except KeyError:
raise CalcError("invalid operator")
operand = self.visit(node.operand)
return operator(operand)
[docs] def visit_Compare(self, node):
comparisons = zip(node.ops, pairwise([node.left] + node.comparators))
try:
return all(operators[op.__class__](self.visit(left), self.visit(right))
for op, (left, right) in comparisons)
except KeyError:
raise CalcError("invalid operator")
[docs] def visit_Call(self, node):
args = [self.visit(arg) for arg in node.args]
func = self.visit(node.func)
try:
return func(*args)
except TypeError:
raise CalcError("invalid arguments")
except ValueError as e:
raise CalcError(e)
[docs] def visit_Name(self, node):
try:
return identifiers[node.id]
except KeyError:
raise CalcError("unknown constant or function")
[docs] def visit_Num(self, node):
return node.n
[docs] def visit_NameConstant(self, node):
return node.value
[docs] def visit_Str(self, node):
raise CalcError("invalid argument")
[docs] def generic_visit(self, node):
"""Fallback visitor which always raises an exception.
We evaluate expressions by using return values of node visitors, and :meth:`generic_visit`
returns None, therefore if it's called we know this is an expression we don't support and
should give an error.
"""
raise CalcError("invalid calculation")
[docs]class CalcError(Exception):
pass
[docs]class Calc(Plugin):
"""A plugin that calculates things.
"""
def _calc(self, calc_str):
"""Start the calculation, and handle any exceptions.
Returns a string of the answer.
"""
if not calc_str:
return "You want to calculate something? Type in an expression then!"
try:
try:
parsed = ast.parse(calc_str)
except SyntaxError:
raise CalcError("invalid syntax")
except MemoryError:
raise CalcError("unable to parse")
res = CalcEval().visit(parsed)
if res is None:
raise CalcError("invalid calculation")
if is_too_long(res):
raise CalcError("result too long to be printed")
return str(res)
except CalcError as ex:
return "Error, {}".format(str(ex))
[docs] @Plugin.command('calc', help='For calculating, not interpreting')
def do_some_calc(self, e):
"""What? You don't have a calculator handy?
"""
e.reply(self._calc(e["data"]))