Completed
Push — stable ( ce9d65...6568cb )
by Sydney
02:01 queued 01:19
created

NumericStringParser.evaluateStack()   B

Complexity

Conditions 7

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 18
rs 7.3333
cc 7
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