1
|
|
|
# Copyright 2017 Starbot Discord Project |
2
|
|
|
# |
3
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); |
4
|
|
|
# you may not use this file except in compliance with the License. |
5
|
|
|
# You may obtain a copy of the License at |
6
|
|
|
# |
7
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0 |
8
|
|
|
# |
9
|
|
|
# Unless required by applicable law or agreed to in writing, software |
10
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS, |
11
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12
|
|
|
# See the License for the specific language governing permissions and |
13
|
|
|
# limitations under the License. |
14
|
|
|
|
15
|
|
|
from __future__ import division |
16
|
|
|
|
17
|
|
|
import math |
18
|
|
|
import operator |
19
|
|
|
|
20
|
|
|
from pyparsing import (Literal, CaselessLiteral, Word, Combine, Group, Optional, |
21
|
|
|
ZeroOrMore, Forward, nums, alphas, oneOf) |
22
|
|
|
|
23
|
|
|
from api import command, message, plugin |
24
|
|
|
|
25
|
|
|
__author__ = 'Paul McGuire' |
26
|
|
|
__version__ = '$Revision: 0.0 $' |
27
|
|
|
__date__ = '$Date: 2009-03-20 $' |
28
|
|
|
__source__ = '''http://pyparsing.wikispaces.com/file/view/fourFn.py |
29
|
|
|
http://pyparsing.wikispaces.com/message/view/home/15549426 |
30
|
|
|
''' |
31
|
|
|
__note__ = ''' |
32
|
|
|
All I've done is rewrap Paul McGuire's fourFn.py as a class, so I can use it |
33
|
|
|
more easily in other places. |
34
|
|
|
''' |
35
|
|
|
|
36
|
|
|
class NumericStringParser(object): |
37
|
|
|
'''Most of this code comes from the fourFn.py pyparsing example.''' |
38
|
|
|
def push_first(self, strg, loc, toks): |
39
|
|
|
self.expression_stack.append(toks[0]) |
40
|
|
|
def push_unary_minus(self, strg, loc, toks): |
41
|
|
|
if toks and toks[0] == '-': |
42
|
|
|
self.expression_stack.append('unary -') |
43
|
|
|
def __init__(self): |
44
|
|
|
""" |
45
|
|
|
expop :: '^' |
46
|
|
|
multop :: 'x' | '/' |
47
|
|
|
addop :: '+' | '-' |
48
|
|
|
integer :: ['+' | '-'] '0'..'9'+ |
49
|
|
|
atom :: PI | E | real | fn '(' expr ')' | '(' expr ')' |
50
|
|
|
factor :: atom [ expop factor ]* |
51
|
|
|
term :: factor [ multop factor ]* |
52
|
|
|
expr :: term [ addop term ]* |
53
|
|
|
""" |
54
|
|
|
point = Literal(".") |
55
|
|
|
exp = CaselessLiteral("E") |
56
|
|
|
fnumber = Combine(Word("+-" + nums, nums) + |
57
|
|
|
Optional(point + Optional(Word(nums))) + |
58
|
|
|
Optional(exp + Word("+-" + nums, nums))) |
59
|
|
|
ident = Word(alphas, alphas+nums+"_$") |
60
|
|
|
plus = Literal("+") |
61
|
|
|
minus = Literal("-") |
62
|
|
|
mult = Literal("x") |
63
|
|
|
div = Literal("/") |
64
|
|
|
lpar = Literal("(").suppress() |
65
|
|
|
rpar = Literal(")").suppress() |
66
|
|
|
addop = plus | minus |
67
|
|
|
multop = mult | div |
68
|
|
|
powop = Literal("^") |
69
|
|
|
pi = CaselessLiteral("PI") |
70
|
|
|
expr = Forward() |
71
|
|
|
atom = ((Optional(oneOf("- +")) + |
72
|
|
|
(pi|exp|fnumber|ident+lpar+expr+rpar).setParseAction(self.push_first)) |
73
|
|
|
| Optional(oneOf("- +")) + Group(lpar+expr+rpar) |
74
|
|
|
).setParseAction(self.push_unary_minus) |
75
|
|
|
# by defining exponentiation as "atom [ ^ factor ]..." instead of |
76
|
|
|
# "atom [ ^ atom ]...", we get right-to-left exponents, instead of left-to-right |
77
|
|
|
# that is, 2^3^2 = 2^(3^2), not (2^3)^2. |
78
|
|
|
factor = Forward() |
79
|
|
|
factor << atom + ZeroOrMore((powop + factor).setParseAction(self.push_first)) |
80
|
|
|
term = factor + ZeroOrMore((multop + factor).setParseAction(self.push_first)) |
81
|
|
|
expr << term + ZeroOrMore((addop + term).setParseAction(self.push_first)) |
82
|
|
|
# addop_term = ( addop + term ).setParseAction( self.pushFirst ) |
83
|
|
|
# general_term = term + ZeroOrMore( addop_term ) | OneOrMore( addop_term) |
84
|
|
|
# expr << general_term |
85
|
|
|
self.bnf = expr |
86
|
|
|
# map operator symbols to corresponding arithmetic operations |
87
|
|
|
epsilon = 1e-12 |
88
|
|
|
self.opn = {"+" : operator.add, |
89
|
|
|
"-" : operator.sub, |
90
|
|
|
"x" : operator.mul, |
91
|
|
|
"/" : operator.truediv, |
92
|
|
|
"^" : operator.pow} |
93
|
|
|
self.function = {"sin" : math.sin, |
94
|
|
|
"cos" : math.cos, |
95
|
|
|
"tan" : math.tan, |
96
|
|
|
"abs" : abs, |
97
|
|
|
"trunc" : lambda a: int(a), |
98
|
|
|
"round" : round, |
99
|
|
|
"sgn" : lambda a: abs(a) > epsilon and cmp(a, 0) or 0} |
100
|
|
|
|
101
|
|
|
def stack_evaluate(self, s): |
102
|
|
|
'''Backend stack evaluation''' |
103
|
|
|
op_eval = s.pop() |
104
|
|
|
if op_eval == 'unary -': |
105
|
|
|
return -self.stack_evaluate(s) |
106
|
|
|
if op_eval in "+-x/^": |
107
|
|
|
op_eval2 = self.stack_evaluate(s) |
108
|
|
|
op_eval1 = self.stack_evaluate(s) |
109
|
|
|
return self.opn[op_eval](op_eval1, op_eval2) |
110
|
|
|
elif op_eval == "PI": |
111
|
|
|
return math.pi # 3.1415926535 |
112
|
|
|
elif op_eval == "E": |
113
|
|
|
return math.e # 2.718281828 |
114
|
|
|
elif op_eval in self.function: |
115
|
|
|
return self.function[op_eval](self.stack_evaluate(s)) |
116
|
|
|
elif op_eval[0].isalpha(): |
117
|
|
|
return 0 |
118
|
|
|
else: |
119
|
|
|
return float(op_eval) |
120
|
|
|
|
121
|
|
|
def stack_eval(self, num_string, parse_all=True): |
122
|
|
|
self.expression_stack = [] |
123
|
|
|
results = self.bnf.parseString(num_string, parse_all) |
124
|
|
|
val = self.stack_evaluate(self.expression_stack[:]) |
125
|
|
|
return val |
126
|
|
|
|
127
|
|
|
def onInit(plugin_in): |
128
|
|
|
#create the basics of our plugin |
129
|
|
|
calc_command = command.Command(plugin_in, 'calc', shortdesc='Calculate given input') |
130
|
|
|
return plugin.Plugin(plugin_in, 'calc', [calc_command]) |
131
|
|
|
|
132
|
|
|
async def onCommand(message_in): |
133
|
|
|
"""Do some math.""" |
134
|
|
|
formula = message_in.body |
135
|
|
|
formula = formula.replace('*', 'x') |
136
|
|
|
|
137
|
|
|
if formula == None: |
138
|
|
|
msg = 'Usage: `{}calc [formula]`'.format('!') |
139
|
|
|
return message.Message(body=msg) |
140
|
|
|
|
141
|
|
|
try: |
142
|
|
|
nsp = NumericStringParser() |
143
|
|
|
answer = nsp.stack_eval(formula) |
144
|
|
|
except Exception as e: |
145
|
|
|
print("CALC PLUGIN EXCEPTION\r\n{}".format(e)) |
146
|
|
|
msg = 'I couldn\'t parse "{}" :(\n\n'.format(formula) |
147
|
|
|
msg += 'I understand the following syntax:\n```\n' |
148
|
|
|
msg += "expop :: '^'\n" |
149
|
|
|
msg += "multop :: 'x' | '/'\n" |
150
|
|
|
msg += "addop :: '+' | '-'\n" |
151
|
|
|
msg += "integer :: ['+' | '-'] '0'..'9'+\n" |
152
|
|
|
msg += "atom :: PI | E | real | fn '(' expr ')' | '(' expr ')'\n" |
153
|
|
|
msg += "factor :: atom [ expop factor ]*\n" |
154
|
|
|
msg += "term :: factor [ multop factor ]*\n" |
155
|
|
|
msg += "expr :: term [ addop term ]*```" |
156
|
|
|
# msg = Nullify.clean(msg) |
157
|
|
|
return message.Message(body=msg) |
158
|
|
|
|
159
|
|
|
msg = '`{}` = `{}`'.format(formula, answer) |
160
|
|
|
# Say message |
161
|
|
|
return message.Message(body=msg) |
162
|
|
|
|