0

I'm trying to develop a simple python method that will allow me to compute basic mathematical operations. The point here is that I can't use eval(), exec() or any other functions that evaluate python statemets, so I have to do it manually. Up to now, I've came across with this piece of code:

solutionlist = list(basicoperationslist)
for i in range(0, len(solutionlist)):
    if '+' in solutionlist[i]:
        y = solutionlist[i].split('+')
        solutionlist[i] = str(int(y[0]) + int(y[1]))
    elif '*' in solutionlist[i]:
        y = solutionlist[i].split('*')
        solutionlist[i] = str(int(y[0]) * int(y[1]))
    elif '/' in solutionlist[i]:
        y = solutionlist[i].split('/')
        solutionlist[i] = str(int(y[0]) // int(y[1]))
    elif '-' in solutionlist[i]:
        y = solutionlist[i].split('-')
        solutionlist[i] = str(int(y[0]) - int(y[1]))
print("The solutions are: " + ', '.join(solutionlist))

So we have two lists of Strings, the basicoperationlist has operations of the following format: 2940-81, 101-16, 46/3, 10*9, 145/24, -34-40. They will always have two numbers, and one operand in the middle. The problem with my solution is that when I have an operation like the last one, the .split() method splits my list into an empty list and a list with the complete operation. In summary, this solution does not work well when we mix negative numbers an the minus operation. I don't know if it fails in any other case because I've only managed to notice the error I previously described. The idea is that at the end of the method, I have the solutionlist as a list of Strings that are going to be the ordered answers to the basic mathematical operations. This is the error that prompts out whenever my code encounters an operation like the last one: ValueError: invalid literal for int() with base 10: ''

The basicoperationslist is defined here:

basicoperationslist = re.findall('[-]*\d+[+/*-]+\d+', step2processedoperation)

As you see, I use a regex to extract the basicoperations from a larger operation. The step2processedoperation is an String that a server sends to my machine. But as example it may contain:

((87/(64*(98-94)))+((3-(97-27))-(89/69)))

It contains complete and balanced mathematical operations.

Maybe someone could help me to solve this problem or maybe I should change this method completely.

Thank you in advance.

enon97
  • 11
  • 7
  • 1
    Welcome to StackOverflow! Your code snippet is not complete since you do not define `basicoperationslist`. Please show a complete, self-contained snippet that shows your error as well as the traceback of the error for that snippet. Please read and follow [How to create a Minimal, Complete, and Verifiable example](http://stackoverflow.com/help/mcve). – Rory Daulton Apr 25 '18 at 08:48
  • Your solution is pretty simple, if you are sure that you will have 2 operands, then after the split check if you got 3 values in the list. If there are three then one of them is a negative no. – fazkan Apr 25 '18 at 08:48
  • also how are you dealing with -34-(-10) – fazkan Apr 25 '18 at 08:49
  • That error you mention also says on which line it occurs. – Jongware Apr 25 '18 at 08:50
  • Have a look at: https://docs.python.org/3/library/stdtypes.html#str.split, you can specify the add number of splits you wan to – Maximilian Peters Apr 25 '18 at 08:50
  • Use `if solutionlist[i].startswith('-')` to check if you are dealing with a negative number. then you can `split` differently. It may be easier to take a whole different approach and use a regex. – DeepSpace Apr 25 '18 at 08:52
  • I've show you how the lists are defined. But not sure what do you mean for a traceback of the error. – enon97 Apr 25 '18 at 08:56
  • This method is supossed not to have parenthesis in the basicoperationslits. So -34-(-10) would be -34--10. The error here is that the split method splits the string by the first minus, and it lefts an empty string an another string with the complete operation. – enon97 Apr 25 '18 at 08:58
  • The problem with .startswith('-') would be that I need to tell the .split() to split the string in the second match... – enon97 Apr 25 '18 at 09:02
  • @enon97, so you check the list that you get after split("-"). if its 3 then the, the location of the empty string shows the location of the extra minus. if its 4 then both are negative numbers... – fazkan Apr 25 '18 at 09:32

2 Answers2

0

I'd ditch the whole splitting approach as it is way too complex and may fail in certain cases as you noticed.

Instead I'd use a regex and the operator module to simplify things.

import re
import operator

operators = {'+': operator.add,
             '-': operator.sub,
             '*': operator.mul,
             '/': operator.truediv}

regex = re.compile(r'(-?\d+)'       # 1 or more digits with an optional leading '-'
                   r'(\+|-|\*|/)'   # one of +, - , *, / 
                   r'(\d+)',        # 1 or more digits
                   re.VERBOSE)

exprssions = ['2940-81', '101-16', '46/3', '10*9', '145/24', '-34-40']

for expr in exprssions:
    a, op,  b = regex.search(expr).groups()
    print(operators[op](int(a), int(b)))

# 2859
# 85
# 15.333333333333334
#  90
# 6.041666666666667
# -74

This approach is easier to adapt to new cases (new operators for example)

DeepSpace
  • 78,697
  • 11
  • 109
  • 154
  • Your solution is very interesting. I did not manage to came up with this approach. The problem here is that I need to do int operations. And the division is not '/', it's the '//' python operator (entire division) – enon97 Apr 25 '18 at 09:05
  • @enon97 I'm not sure what you mean. If you want to use "integer division" you can use `operator.floordiv` instead of `operator.truediv` that I used in the `operators` dictionary. – DeepSpace Apr 25 '18 at 09:09
  • Yes, you are right, I've just search for it :operator.floordiv(a, b)¶ operator.__floordiv__(a, b) Return a // b. – enon97 Apr 25 '18 at 09:14
  • By the way, does this works if we have for example 34--10? – enon97 Apr 25 '18 at 09:52
  • @enon97 not as it is, but it can be fixed by a very simple change to the regex (the second `(\d+)` should be changed to `(-?\d+)`). – DeepSpace Apr 25 '18 at 10:03
  • Thank you so much. I'm having a lot of trouble while trying to save the solutions that operators[op](int(a), int(b)) output into a list of strings. Could you guide me to do that? – enon97 Apr 25 '18 at 10:23
  • Also I would like the print to print the solutions in the same line, like "The Solutions are: 2859, 85, 15.333333333333333333334, 90" and so on. Sorry for asking you that simple questions but I'm new to python and your code is kind of complex to me. – enon97 Apr 25 '18 at 10:24
  • @enon97 Simply create a new list beofre the loop and instead of `print` use `.append`. – DeepSpace Apr 25 '18 at 10:25
0

You can easily use operator and a dict to store the operations instead of a long list of if-else

This solution can also calculate more complicated expressions through recursion.

Define the operations and their order

from operator import add, sub, mul, floordiv, truediv
from functools import reduce

OPERATIONS = {
    '+': add,
    '-': sub,
    '*': mul,
    '/': floordiv, # or '/': truediv,
    '//': floordiv,
}
OPERATION_ORDER = (('+', '-'), ('//', '/', '*'))

The simple case of a single number

def calculate(expression):
    # expression = expression.strip()
    try:
        return int(expression)
    except ValueError:
        pass

The calculation

    for operations in OPERATION_ORDER:
        for operation in operations:
            if operation not in expression:
                continue
            parts = expression.split(operation)

            parts = map(calculate, parts) # recursion
            value = reduce(OPERATIONS[operation], parts)
#             print(expression, value)
            return value

Negative first number

before the calculation:

negative = False
if expression[0] == '-':
    negative = True
    expression = expression[1:]

in the calculation, after the splitting of the string:

        if negative:
            parts[0] = '-' + parts[0]

So in total this becomes:

def calculate(expression):
    try:
        return int(expression)
    except ValueError:
        pass

    negative = False
    if expression[0] == '-':
        negative = True
        expression = expression[1:]

    for operations in OPERATION_ORDER:
        for operation in operations:
            if operation not in expression:
                continue
            parts = expression.split(operation)
            if negative:
                parts[0] = '-' + parts[0]

            parts = map(calculate, parts) # recursion
            return reduce(OPERATIONS[operation], parts)

Parentheses

Checking whether there are parentheses can be done easily with re. First we need to make sure it doesn't recognise the 'simple' parenthised intermediary results (like (-1))

PATTERN_PAREN_SIMPLE= re.compile('\((-?\d+)\)')
PAREN_OPEN = '|'
PAREN_CLOSE = '#'
def _encode(expression):
    return PATTERN_PAREN_SIMPLE.sub(rf'{PAREN_OPEN}\1{PAREN_CLOSE}', expression)

def _decode(expression):
    return expression.replace(PAREN_OPEN, '(').replace(PAREN_CLOSE, ')')

def contains_parens(expression):
    return '(' in _encode(expression)

Then to calculate the leftmost outermost parentheses, you can use this function

def _extract_parens(expression, func=calculate):
#     print('paren: ', expression)
    expression = _encode(expression)
    begin, expression = expression.split('(', 1)
    characters = iter(expression)

    middle = _search_closing_paren(characters)

    middle = _decode(''.join(middle))
    middle = func(middle)

    end = ''.join(characters)
    result = f'{begin}({middle}){end}' if( begin or end) else str(middle)
    return _decode(result)


def _search_closing_paren(characters, close_char=')', open_char='('):
    count = 1
    for char in characters:
        if char == open_char:
            count += 1
        if char == close_char:
            count -= 1
        if not count:
            return
        else:
            yield char

The reason for the () around calculate(middle) is because an intermediate result can be negative, and this might pose problems later on if the parentheses are left out here.

The beginning of the algorithm then changes to:

def calculate(expression):
    expression = expression.replace(' ', '')
    while contains_parens(expression):
        expression = _extract_parens(expression)
    if PATTERN_PAREN_SIMPLE.fullmatch(expression):
        expression = expression[1:-1]
    try:
        return int(expression)
    except ValueError:
        pass

Since intermediary results can be negative, we need regular expressions to split on the - to prevent 5 * (-1) from being split on the -

So I reordered the possible operations like this:

OPERATIONS = (
    (re.compile('\+'), add),
    (re.compile('(?<=[\d\)])-'), sub), # not match the - in `(-1)`
    (re.compile('\*'), mul),
    (re.compile('//'), floordiv),
    (re.compile('/'), floordiv), # or '/': truediv,
)

The pattern for the - only matches if it is preceded by a ) or a digit. This way we can drop the negative flag and handling

The rest of the algorithms changes then to:

    operation, parts = split_expression(expression)
    parts = map(calculate, parts) # recursion
    return reduce(operation, parts)

def split_expression(expression):
    for pattern, operation in OPERATIONS:
        parts = pattern.split(expression)
        if len(parts) > 1:
            return operation, parts

complete algorithm

The complete code can be found here

testing:

def test_expression(expression):
    return calculate(expression) == eval(expression.replace('/','//'))  # the replace to get floor division

def test_calculate():
    assert test_expression('1')
    assert test_expression(' 1 ')
    assert test_expression('(1)')
    assert test_expression('(-1)')
    assert test_expression('(-1) - (-1)')
    assert test_expression('((-1) - (-1))')
    assert test_expression('4 * 3 - 4 * 4')
    assert test_expression('4 * 3 - 4 / 4')
    assert test_expression('((87/(64*(98-94)))+((3-(97-27))-(89/69)))')

test_calculate()

power:

Adding power becomes as simple as adding

(re.compile('\*\*'), pow),
(re.compile('\^'), pow),

to OPERATIONS

calculate('2 + 4 * 10^5')
400002
Maarten Fabré
  • 6,938
  • 1
  • 17
  • 36
  • Thank you so much for your idea. But I think that this is a too much complex solution for my needs. If I want to solve an operation recursively, it would be one like this: ((87/(64*(98-94)))+((3-(97-27))-(89/69))) – enon97 Apr 25 '18 at 09:50