I have been developing a graphical calculator, and to allow for natural user input I have written a function which converts their input into a python readable form.
This is my first time using regex, and I believe I managed to get each of the expressions to do what I intended by them.
However, as can been seen there are quite a few different cases to consider, and so the function is relatively long considering what it does.
I was wondering if there was a better solution than this to solve my problem, but if not can I improve my implementation at all?
def edit_function_string(func):
"""Converts the input function into a form executable by Python"""
func = re.sub(r'\s+', '', func) # Strip whitespace
func = re.sub(r'\^', r'**', func) # replaces '^' with '**'
func = re.sub(r'/(([\w()]+(\*\*)?)*)', r'/(1円)', func) # replaces '/nf(x)' with '/(nf(x))'
func = re.sub(r'\*\*(([\w()]+(\*\*)?)*)', r'**(1円)', func) # replaces '**nf(x)' with '**(nf(x))'
func = re.sub(r'(\d+)x', r'1円*x', func) # replaces 'nx' with 'n*x'
func = re.sub(r'(math\.)?ceil(ing)?', 'math.ceil', func) # replaces 'ceil(ing)' with 'math.ceil'
func = re.sub(r'(math\.)?floor', 'math.floor', func) # replaces 'floor' with 'math.floor'
func = re.sub(r'(math\.f)?abs(olute)?|modulus', 'math.fabs', func) # replaces 'abs(olute)' with 'math.fabs'
func = re.sub(r'(math\.)?sqrt|root', 'math.sqrt', func) # replaces 'sqrt' or 'root' with 'math.sqrt'
func = re.sub(r'(math\.)?log|ln', 'math.log', func) # replaces 'log' or 'ln' with 'math.log'
func = re.sub(r'(math\.)?exp', 'math.exp', func) # replaces 'exp' with 'math.exp'
func = re.sub(r'\|(.+?)\|', r'math.fabs(1円)', func) # replaces '|x|' with 'math.fabs(x)'
func = re.sub(r'([\w]+)!|\((.+?)\)!', r'math.factorial(1円2円)', func) # replaces 'x!' with 'math.factorial(x)'
for f in ('sin', 'cos', 'tan', 'sinh', 'cosh', 'tanh'):
# replaces trigonometric or hyperbolic functions with the correct syntax
func = re.sub(r'^(\d*){f}\(|([+\-*/()])(\d*){f}\('.format(f=f),
r'1円2円3円math.{f}('.format(f=f), func)
# replaces inverse trigonometric or hyperbolic functions with the correct syntax
func = re.sub(r'^(\d*)a(rc?)?{f}\(|([+\-*/()])(\d*)a(rc?)?{f}\('.format(f=f),
r'3円1円4円math.a{f}('.format(f=f), func)
for f, reciprocal in (('sec', 'cos'), ('cosec', 'sin'), ('csc', 'sin'), ('cot', 'tan'),
('sech', 'cosh'), ('cosech', 'sinh'), ('csch', 'sinh'), ('coth', 'tanh')):
# replaces reciprocal trigonometric or hyperbolic functions with the correct syntax
func = re.sub(r'^(\d*){f}\((.+?)\)|([+\-*/()])(\d*){f}\((.+?)\)'.format(f=f),
r'3円1円4円(1/math.{reciprocal}(2円5円))'.format(reciprocal=reciprocal), func)
# replaces inverse reciprocal trigonometric or hyperbolic functions with the correct syntax
func = re.sub(r'^(\d*)a(rc?)?{f}\((.+?)\)|([+\-*/()])(\d*)a(rc?)?{f}\((.+?)\)'.format(f=f),
r'4円1円5円(1/math.a{reciprocal}(3円7円))'.format(reciprocal=reciprocal), func)
for i in range(2): # Runs twice in order to deal with overlapping matches
for constant in ('e', 'pi', 'tau'):
# replaces 'e', 'pi', or 'tau' with 'math.e', 'math.pi', or 'math.tau' respectfully
# unless in another function such as: 'math.ceil'
func = re.sub(r'^(\d*){constant}(x?)$|^(\d*){constant}(x?)([+\-*/(])|'
r'([+\-*/()])(\d*){constant}(x?)([+\-*/()])|'
r'([+\-*/)])(\d*){constant}(x?)$'.format(constant=constant),
r'6円10円1円3円7円11円math.{constant}2円4円8円12円5円9円'.format(constant=constant), func)
# replaces 'math.ex', 'math.pix', or 'math.tau' with 'math.e*x', 'math.pi*x', or 'math.tau*x' respectfully
# unless part of another function
func = re.sub(r'math\.{constant}x([+\-*/()])|math.ex$'.format(constant=constant),
r'math.{constant}*x1円'.format(constant=constant), func)
func = re.sub(r'([\dx])math\.', r'1円*math.', func) # replaces 'nmath.' with 'n*math.'
func = re.sub(r'([\dx])\(', r'1円*(', func) # replaces 'n(' with 'n*('
return func
-
3\$\begingroup\$ I would suggest you consider writing an expression parser. Eli Bendersky has an excellent article on Top-Down Operator Precedence parsers that uses Python for implementation. \$\endgroup\$aghast– aghast2019年03月11日 01:16:40 +00:00Commented Mar 11, 2019 at 1:16
1 Answer 1
The main problem I see with this code is that it's write-only:
- Single character names. These are deadly to understanding the code.
- Over-reliance on regular expressions. These take a long time to read, and in my experience usually hide at least one bug each.
- Magical values which should be constants, such as
2
. - A single variable which is changed several times.
- No unit tests, although they may be elsewhere.
Other than that, provide one way to run each function. Since you already support providing the same name as the Python function, why not just mandate that and make the code significantly simpler? In fact, why invent your own DSL at all for this problem? Just giving the user a Python shell with mathematical functions already imported should get them 90% of the way there.
Explore related questions
See similar questions with these tags.