1
\$\begingroup\$

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
asked Mar 10, 2019 at 19:50
\$\endgroup\$
1
  • 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\$ Commented Mar 11, 2019 at 1:16

1 Answer 1

2
\$\begingroup\$

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.

answered Mar 11, 2019 at 1:17
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.