Numeric Expression Evaluator

#!/usr/bin/python3
# ===================================================================
# evaluate a numeric expression
# display the results or an error message
#
# THIS IS CURRENTLY AN INCOMPLETE WORK IN PROGRESS
#
# Last Modified: 10/15/2020
# ===================================================================

import copy
import sys
import re
from collections import deque
import numeric_expression_tokenizer as netok


# -------------------------------------------------------------------
# ---- fix/modify unary operator tokens in token list
# ----
# ---- this is to distinguish them from regular +/- binary operators.
# ---- unary operators follow binary operators or they are the first
# ---- token in the token list.
# ----
# ---- change '+' to 'u+'
# ---- change '-' to 'u-'
# -------------------------------------------------------------------

def fix_unary_operators(tok_list):

    idx      = 0               # token list index
    list_len = len(tok_list)   # token list length

    # ---- these operators can proceed a unary operator

    proceed_ops = ['-', '+', '%', '*', '/', '(', '**' ]


    # ---- look at each token in the token list

    while idx < list_len:

        # ---- first token in the list?

        if idx == 0:

            if tok_list[idx] == '-':
                tok_list[idx] = 'u-'
            elif tok_list[idx] == '+':
                tok_list[idx] = 'u+'

            idx += 1
            continue

        # ---- is the current token a unary operator (+/-)?

        if tok_list[idx] == '-' or tok_list[idx] == '+':
        
            # ---- does it need fixing?

            if tok_list[idx-1] in proceed_ops:

                # ---- fix it

                if tok_list[idx] == '+':
                    tok_list[idx] = 'u+'
                elif tok_list[idx] == '-':
                    tok_list[idx] = 'u-'

        idx += 1

    return True


# -------------------------------------------------------------------
# ---- test if a key exists in a dictionary
# -------------------------------------------------------------------

def check_key(dict,key):

    if key in dict:
        return True
    return False


# -------------------------------------------------------------------
# ---- evaluate the numeric expression's token list
# -------------------------------------------------------------------

def evaluate(tok_list):

    results = 0                # expression results

    o_stack = deque()          # operator stack (LIFO)
    v_stack = deque()          # value stack    (LIFO)

    # ---- operators

    ops   = ['-', '+', '%', '*', '/', '(', ')', '**', 'u+', 'u-' ]
    u_ops = [ 'u-', 'u+' ]
    b_ops = ['-', '+', '%', '*', '/', '(', ')', '**' ]

    # ---- operator precedence (higher precedence, higher number)

    op_prec = { '-'  : 1,  '+'  : 1,
                '*'  : 2,  '/'  : 2,  '%'  : 2,
                'u+' : 3,  'u-' : 3,
                '**' : 4,
                '('  : 5,  ')'  : 5 }


    # ---------------------------------------------------------------
    # display operator and value stack (LIFO)
    # ---------------------------------------------------------------

    def display_o_v_stacks(title=None):

       # ---- get stack lengths

       o_len = len(o_stack)
       v_len = len(v_stack)

       # ---- display stacks side by side

       if title:
          print(title)
       print('O-Stack     V-Stack')

       for i in range(max(o_len,v_len)):
           if i < o_len:
               o = o_stack[i]
           else:
               o = ''
           if i < v_len:
               v = v_stack[i]
           else:
               v = ''
           print('  {:7}     {}'.format(o,v))


    # ---------------------------------------------------------------
    # convert string to number
    # ---------------------------------------------------------------

    def convert_string_to_number(s):
        if re.search('\.',s):
            return float(s)
        elif re.search('[eE]',s):
            return float(s)
        return int(s)

    # ---------------------------------------------------------------
    # unary operation
    # ---------------------------------------------------------------

    def unary_operation(o,v):

        n = convert_string_to_number(v)
        if o == 'u-':
               return str(-n)
        return str(n)

    # ---------------------------------------------------------------
    # binary operation
    # ---------------------------------------------------------------

    def binary_operation(o,a,b):

        aa = convert_string_to_number(a)
        bb = convert_string_to_number(b)

        if o == '-':
           n = aa - bb
        elif o == '+':
           n = aa + bb
        elif o == '/':
           n = aa / bb
        elif o == '*':
           n = aa * bb
        elif o == '%':
           n = aa % bb
        elif o == '**':
           n = aa ** bb

        return str(n)


    # ---- process token stack


    for tok in tok_list:

        if tok in ops:
            o_stack.appendleft(tok)
        else:
            v_stack.appendleft(tok)

    display_o_v_stacks('---- initial stack state --------')

    while len(o_stack) > 0:

        o = o_stack.pop()

        if o in u_ops and len(v_stack) >= 1:
            v = v_stack.pop()
            v = unary_operation(o,v)
            v_stack.appendleft(v)
            ##display_o_v_stacks('---- intermediate stack state ---')
            continue

        if o in b_ops and len(v_stack) >= 2:
            b = v_stack.pop()
            a = v_stack.pop()
            v = binary_operation(o,a,b)
            v_stack.appendleft(v)
            ##display_o_v_stacks('---- intermediate stack state ---')
            continue

        print('ERROR processing numeric expression') 
        display_o_v_stacks('Error stack state')
        return(False,0)

    display_o_v_stacks('---- final stack state ----------')    

    return (True,v_stack.pop())


# -------------------------------------------------------------------
# ---- main
# -------------------------------------------------------------------

if __name__ == '__main__':

    # ---------------------------------------------------------------
    # ----running Python3?
    # ---------------------------------------------------------------

    def RunningPython3():
        if sys.version_info[0] == 3:
            return True
        return False

    # ---------------------------------------------------------------
    # ---- prompt the user for input
    # ---------------------------------------------------------------

    def GetUserInput(prompt,py3):
        if py3:
            return input(prompt).strip()
        else:
            return raw_input(prompt).strip()

    # ---------------------------------------------------------------
    # ---- pause program
    # ---------------------------------------------------------------

    def Pause(py3):
        print('')
        GetUserInput('Press enter to continue ',py3)

    # ---------------------------------------------------------------
    # ---- ask the user for a numeric expression and evaluate it
    # ---------------------------------------------------------------

    py3 = RunningPython3()

    while True:

        # ---- get user input

        print()
        e = GetUserInput('Enter expression: ',py3)

        if not e:
            break


        # ---- tokenize the numeric expression entered by the user

        (success,token_list) = netok.parse(e)

        if not success:
            sys.exit()

        ##for tok in token_list:
        ##     print('raw tok: {}'.format(tok))


        # ---- find and fix unary operators, if any
        # ---- set unary '-' to 'u-'
        # ---- set inary '+' to 'u+'

        if not fix_unary_operators(token_list):
            sys.exit()
 
        # ---- evaluate the numeric expression token list

        (success,results) = evaluate(token_list)

        if not success:
            sys.exit()


        # ---- display the results

        print()
        print('results: {}'.format(results))

        Pause(py3)

    print()