|
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
|
|
|
|