0
\$\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\3math.{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\4math.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\11math.{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}*x\1'.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
\$\endgroup\$
  • 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\$ – Austin Hastings Mar 11 at 1:16
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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.