I have a dictionary containing a list of object as
objects = {'A1': obj_1,
'A2': obj_2,
}
I then have a string as
cmd = '(1.3A1 + 2(A2 + 0.7A3)) or 2(A4 to A6)'
I want to translate this to a command as
max( 1.3*objects['A1'] + 2*(objects['A2'] + 0.73*objects['A3']), 2*max(objects['A4'], objects['A5'], objects['A6']))
My try
As I didn't find any better option, I started writing a parser from scratches.
PERSONAL NOTE: I don't think that attaching a 150-line code to a SO question is good practice as this will imply that the reader should read and understand it which is a demanding task. Nonetheless my previous question was downvoted because I didn't put my solution. So here you are...
import re
from more_itertools import stagger
def comb_to_py(string, objects):
# Split the line
toks = split_comb_string(string)
# Escape for empty string
if toks[0] == 'none':
return []
# initialize iterator
# I could use a deque here. Let's see what works the best
iterator = stagger(toks, offsets=range(2), longest=True)
return comb_it_to_py(iterator, objects)
def split_comb_string(string):
# Add whitespaces between tokes when they could be implicit to allow string
# splitting i.e. before/after plus (+), minus and closed bracket
string = re.sub(r' ?([\+\-)]) ?', r' \1 ', string)
# remove double spaces
string = re.sub(' +', ' ', string)
# Avoid situations as 'A1 + - 2A2' and replace them with 'A1 - 2A2'
string = re.sub(r'\+ *\-', r'-', string)
# Avoid situations as 'A1 - - 2A2' and replace them with 'A1 + 2A2'
string = re.sub(r'\- *\-', r'+', string)
# Add whitespace after "(" (we do not want to add it in front of it)
string = re.sub(r'\( ?', r'( ', string)
return string.strip().split(' ')
def comb_it_to_py(iterator, objects):
for items in iterator:
# item[0] is a case token (e.g. 1.2A3)
# This should occur only with the first element
if re.fullmatch(r'([\d.]*)([a-zA-Z(]+\d*)', items[0]) is not None:
res = parse_case(items[0], objects, iterator)
elif items[0] == ')' or items[0] is None:
return res
# plus (+)
elif items[0] == '+':
# skip one position
skip_next(iterator)
# add following item
res += parse_case(items[1], objects, iterator)
# minus (-)
elif items[0] == '-':
# skip one position
skip_next(iterator)
# add following item
res -= parse_case(items[1], objects, iterator)
else:
raise(ValueError(f'Invalid or misplaced token {items[0]}'))
return res
def parse_case(tok, objects, iterator):
# Translate a case string into an object.
# It handles also brackets as "cases" calling comb_it_to_py recursively
res = re.match(r'([\d.]*)(\S*)', tok)
if res[1] == '':
mult = 1
else:
mult = float(res[1])
if res[2] == '(':
return mult * comb_it_to_py(iterator, objects)
else:
return mult * objects[res[2]]
def skip_next(iterator):
try:
next(iterator)
except StopIteration:
pass
if __name__ == '__main__':
from numpy import isclose
def test(string, expected_result):
try:
res = comb_to_py(string, objects)
except Exception as e:
print(f"Error during test on '{string}'")
raise e
assert isclose(res.value, expected_result), f"Failed test on '{string}'"
objects = {'A1': 1, 'A2':2, 'A10':3}
test('A2', 2)
test('1.3A2', 2.6)
test('1.3A2 + 3A1', 5.6)
test('1.3A2+ 3A1', 5.6)
test('1.3A2 +3A1', 5.6)
test('1.3A2+3A1', 5.6)
test('1.3A2 - 3A1', -0.4)
test('1.3A2 -3A1', -0.4)
test('1.3A2- 3A1', -0.4)
test('1.3A2-3A1', -0.4)
test('1.3A2 + -3A1', -0.4)
test('1.3A2 +-3A1', -0.4)
test('1.3A2 - -3A1', 5.6)
test('A1 + 2(A2+A10)', 25)
test('A1 - 2(A2+A10)', -23)
test('2(A2+A10) + A1', 25)
test('2(A2+A10) - A1', 23)
test('2(A2+A10) - -A1', 25)
test('2(A2+A10) - -2A1', 26)
This code is not only lengthy, but also very easy to break. The whole code is based on the correct split of the string and the regex section is there only to be sure that the string is split correctly, which totally depend on the position of the whitespaces inside the string, even if - in this specific syntax - most whitespaces should not be parsed at all.
Moreover, this code still doesn't handle the or keyword (where A or B should translate into max(A,B) and the to keyword (where A1 to A9 should translate in max([Ai for Ai in range(A1, A9)])).
Question
Is this the best approach or is there a more robust way for this type of tasks?
Note
I gave a look to pyparsing. It looks as a possibility, but, if I understood well, it should be used as a more robust "line-splitting", while the tokens would still have to be translated to an operation one by one manually. Is this correct?