Python: apply math order of operations to reorder strings following hierarchy in parentheses

Question:

How can be done with Python to reorder a string applying the math operations order with parenthesys?

Let me use an example:

"son(father(granpa)mother))" ===> "granpa father mother son"

The idea is to reorder the original string using the standar math order operations. In the math order operations, parenthesys has the highest priority. Let me use a math example:

4 + (5 + (3 + 3)) = ((3 + 3) + 5 ) + 4 = 3 + 3 + 5 + 4 = 14

EDIT: This example uses just + because python always do * before +, in the same parenthesys level, thats not the point, the point is the order in strings because will only concatenate the result reordering it.

The target is to reorder a string containing variables to study posible optimizations to operations.
Example of string I want to reorder:

def xor_with_nands(a,b):
  return f"~(~(~({a} & {b}) & {a}) & ~(~({a} & {b}) & {b}))"

>> xor_with_nands(0,1)
>> ~(~(~(0 & 1) & 0) & ~(~(0 & 1) & 1))
>> eval(xor_with_nands(0,1))
>> 1

If there is a way to create a function that reorders the strings respecting the mathematical order of the parentheses (just parenthses, not other math operations order), it may be possible to analyze optimizations in some processes.

The goal is to get a tool that visualizes nested logical operations in order of execution to visually understand them and hopefully simplify them.

Conclusion: Shunting Yard Algorithm is amazing. Thank you very much!

Asked By: Nand0san

||

Answers:

You are looking for a Shunting Yard Algorithm.

This will convert your math notation (also known as infix notation) into a format that can easily be processed by a computer (known as postfix notation). See here: https://en.wikipedia.org/wiki/Shunting-yard_algorithm for a better description.

Essentially, this algorithm will take your expression as convert it into queue which is already ordered correctly.

Here is some pseudocode taken from Brilliant.org:

1.  While there are tokens to be read:
2.        Read a token
3.        If it's a number add it to queue
4.        If it's an operator
5.               While there's an operator on the top of the stack with greater precedence:
6.                       Pop operators from the stack onto the output queue
7.               Push the current operator onto the stack
8.        If it's a left bracket push it onto the stack
9.        If it's a right bracket 
10.            While there's not a left bracket at the top of the stack:
11.                     Pop operators from the stack onto the output queue.
12.             Pop the left bracket from the stack and discard it
13. While there are operators on the stack, pop them to the queue

This will allow you to define your custom operators and define their precedence.

Answered By: Akilan Manivannan

Here I use a recursive approach of processing the string character-by-character. It’s not terribly efficient, but it gets the job done.

def tokenize_parentheticals(s, idx=None):
    # first, use a single-element list as essentially a 'mutable integer'
    # that we can pass up and down the chain of recursive calls to never
    # move backwards in the string
    if idx is None:
        idx = [0]
    # the start of each token is the character at the beginning of the string.
    start = idx[0]
    # We will store all tokens in each 'layer' and yield them all at the
    # end of the layer
    tokens = []
    # count through the string character by character, and look for either
    # an open-paren or a close-paren
    while idx[0] < len(s):
        c = s[idx[0]]
        idx[0] += 1
        # if we encounter an open-paren, then 
        #   (1) terminate the current token and add it to our list
        #   (2) recurse inside the next set of parentheses
        # once we return from the recursion, update the start of the next 
        # token to the character after the most recent close-paren
        if c == '(':
            tokens.append(s[start:idx[0] - 1])
            yield from tokenize_parentheticals(s, idx)
            start = idx[0]
        # If we encounter a close-paren, then
        #   (1) terminate the current token and add it to our list
        #   (2) un-recurse, go up a level
        elif c == ')':
            tokens.append(s[start:idx[0] - 1]
            break
    # before exiting, yield all non-empty tokens
    yield from (t for t in tokens if t)


inp = "son(father(granpa)mother))"
' '.join(tokenize_parentheticals(inp))
# 'granpa father mother son'

list(tokenize_parentheticals('4 + (5 + (3 + 3))'))
# ['3 + 3', '5 + ', '4 + ']

list(tokenize_parentheticals(xor_with_nands(0, 1)))
# ['0 & 1', '~', ' & 0', '0 & 1', '~', ' & 1', '~', ' & ~', '~']

disclaimer: I wrote this answer without reading the edit to your question and the actual use you have for this. This may not be a robust enough solution to fit your needs. Customize as necessary.

Answered By: Green Cloak Guy

This might be helpful:

from collections import defaultdict
import re

def sort_terms(s, ops = ''):
    d = defaultdict(list)
    depth = 0
    pattern = r'[()]|[^( )' + ops + ']+'
    for item in re.findall(pattern,s):
        if item == '(':
            depth += 1
        elif item == ')':
            depth -= 1
        else:
            d[depth].append(item)
    terms = []
    for depth in sorted(d.keys(),reverse = True):
        terms.extend(d[depth])
    return terms

Examples:

>>> sort_terms("son(father(granpa)mother))")
['granpa', 'father', 'mother', 'son']
>>> sort_terms('4 + (5 + (3 + 3))','+')
['3', '3', '5', '4']
>>> '+'.join(sort_terms('4 + (5 + (3 + 3))','+'))
'3+3+5+4'
>>> sort_terms('~(~(~(0 & 1) & 0) & ~(~(0 & 1) & 1))','~&')
['0', '1', '0', '1', '0', '1']

The operators themselves are stripped away, but in the question itself you indicated that you only wanted to pay attention to the parentheses.

Answered By: John Coleman
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.