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