2
\$\begingroup\$

Problem:

Given a string representing a mathematical expression with constants (pi, e, psi, i), basic arithmetic operations (+, -, *, /), parentheses, fractions, and exponentiation, write a program or function to convert a string to its equivalent LaTeX code. The input string will only contain digits, arithmetic operators, parentheses, spaces, and constants.

Example:
  • Input: 2 + (3 * π) / (4 - e)^2

  • Output: $2 + \frac{3\cdot\pi}{(4 - e)^2}$

  • Input: π * 22/7 - 9.81

  • Output: $\pi\cdot\frac{22}{7} - 9.81$

  • Input: 2 - e^i + 3^(32)*(2*ψ)/(7*π)

  • Output: $2 - e^i + 3^{32}\cdot\frac{2\cdot\psi}{7\cdot\pi}$

Constraints:

  • 0 < Input string < 100 chars.
  • You need to implement fractions using the \frac{}{} command in LaTeX.

Scoring:

This is , so the shortest code wins.

Latex commands:

  • \pi for the constant "π".
  • \psi for the constant "ψ".
  • e for the constant "e".
  • i for the constant "i".
  • \cdot for multiplication.
  • \frac{}{} for fractions.
  • ^ operator for exponentiation. "{}" for exponents are optional.
  • The output should be enclosed in "$" or "$$" to indicate a math expression in LaTeX.
\$\endgroup\$
17
  • 2
    \$\begingroup\$ (-1) IMO This is a chameleon challenge for “parse a numerical expression”, which has been done to death. The formatting of the expression as LaTeX is easy, the parsing is the hard part. My suggestion would be to allow the input to be taken as a pre-parsed expression. (By parsed I mean as a tree or similar structure with operator precedence, parentheses, etc established). \$\endgroup\$
    – noodle man
    Feb 18 at 7:47
  • \$\begingroup\$ It’s possible some solutions won’t need to entirely parse the input but I think it’s at least partially necessary \$\endgroup\$
    – noodle man
    Feb 18 at 7:53
  • \$\begingroup\$ Why is 3 * π 3\cdot\pi and not 3\pi? \$\endgroup\$
    – Neil
    Feb 18 at 7:59
  • 2
    \$\begingroup\$ @Neil that would move the exponent from the denominator to outside the fraction, changing meaning. \$\endgroup\$ Feb 18 at 9:12
  • 1
    \$\begingroup\$ @doubleunary I guess that could turn into $2 + \frac{3\cdot\pi}{{(4 - e)}^2}$? \$\endgroup\$
    – Neil
    Feb 18 at 10:48

2 Answers 2

8
\$\begingroup\$

Tamsin, 375 220 211 bytes

1=2->E&'$'+E+'$'.2=3->R&{"/"&3->E&R<-'\\frac{'+R+'}{'+E+'}'}&(("-"|"+"|"*"&'\\cdot')->O&2->E&''+O+E)/R.3=(s&"("&2->E&")"&s&'('+E+')'|("π"&'\\pi'|"ψ"&'\\psi'|" "|$:alnum)/'')->R&("^"&3->E&'^{'+E+'}')/R.s={" "}.

Try it online!

Ungolfed and heavily commented version:

# expr1 (entrypoint); match `expr2` (store in E); return '$' + E + '$'
expr1 = expr2 -> E & '$' + E + '$'.

# implements "/", "-", "+", "*" over expr3
expr2 = expr3 -> R                             # match `expr3`    (store in R)
      & {                                      # while possible:
          "/"                                  #   match "/"
        & expr3 -> E                           #   match `expr3`  (store in E)
        & R <- '\\frac{' + R + '}{' + E + '}'  #   R = '\frac{' + R + '}{' + E + '}'
      } & (                                    # scope (
        ("-" | "+" | "*" & '\\cdot') -> O      #   match operator (store in O)
        & expr2 -> E                           #   match `expr2`  (store in E)
        & '' + O + E                           #   '' + O + E     (implicit return)
      )/R.                                     # ) match scope zero or more times then concatenate R and matches (implicit return)

# implements "()", numbers and "^"
expr3 = (                    # scope (
            spaces           #     match `spaces`
          & "("              #     match "("
          & expr2 -> E       #     match `expr1`   (store in E)
          & ")"              #     match ")"
          & spaces           #     match `spaces`
          & '(' + E + ')'    #     '(' + E + ')'   (implicit return)
        |                    #   or
          (                  #     scope (
              "π" & '\\pi'   #          match "π"  (replace with \pi)
            | "ψ" & '\\psi'  #       or match "ψ"  (replace with \psi)
            | " "            #       or match " "
            | $:alnum        #       or match alphanumeric [0-9A-Za-z]
          )/''               #     ) match scope zero or more times then concatenate matches (implicit return)
      ) -> R                 # ) store return from scope in R
      & (                    # scope (
        "^"                  #   match "^"
        & expr3 -> E         #   match `expr3`     (store in E)
        & '^{' + E + '}'     #   '^{' + E + '}'    (implicit return)
      )/R.                   # ) match scope zero or more times then concatenate R and matches (implicit return)

spaces = {" "}.  # zero or more spaces

# command to golf and copy to clipboard:
# sed 's/paces\|expr\| \|#.*//g; s/""/" "/g' latex.tamsin | tr -d '\n' | wl-copy
\$\endgroup\$
1
\$\begingroup\$

Python 3, 513 bytes

from re import*
R=str.replace
def F(r,c):
	while'/^*'[c]in r:
		i=r.index('/^*'[c])
		r=r[:i-1]+[['\\frac{%s}{%s}','%s^{%s}','%s \\cdot %s'][c]%((c>0)*sub('(%\d+)([}]*)$','(\\1)\\2',r[i-1])or r[i-1],r[i+1])]+r[i+2:]
	return r
P=lambda e:R(R(' '.join(F(F(F(findall('%*[.\d]+|i|e|π|ψ|\+|-|\*|/|\^',e),1),0),2)),'π','\\pi'),'ψ','\\psi')
def f(e,t=[]):
	while'('in e:
		e=sub('\(([^()]*)\)',lambda m:f'%{(len(t),t.append(P(m[1])))[0]}',e)
	z=lambda e:t and z(R(e,f'%{len(t)-1}',t.pop()))or e
	return'$%s$'%z(P(e))

Try it online!

This took quite a while to golf down and for me to feel all right about it, but at this point I think I've done about all that I can without additional help. If we're allowed to go wild with parenthesis, then I can reduce this some more, but I'm not sure if just producing mathematically-equivalent LaTeX-accepted output is a valid solution.

The short explanation is that I use regex to replace inner parenthetical expressions with a meta token, inserting the LaTeX-ed version of that minimal expression into a work list, and after all parenthetical statements are replaced with meta tokens I go back and replace each meta token with its processed form, resulting in the fully LaTeX formatted string.

Full explanation:

# Needed for re.sub and re.findall
import re

'''
Parameters:	array - An array of tokens, or expressions, representing the current expression
		index - 0 for division, 1 for exponentiation, 2 for multiplication
Formats the provided array in one of three ways
    - Start by finding the index of the requested character
    - While that character is still a lone element in the array
        - Get the index of the requested opertor
        - Get the values on either side of the operator
        - Set the replacement string as appropriate for the operator
	    - If the operation is exponentiation or multiplication, we need to wrap the lefthand operand in parenthesis if it was a meta token (denoted by a token of the form '%<d>')
	- Replace indices (i - 1, i, and i + 1) with the LaTeX string representing the operation, and format the string to insert the formatted left and righthand sides
'''
def Format(array, index):
	c = '/^*'[index]	# Get the character we're doing formatting for
	while c in array:	# Do this until all standalone c have been formatted
		i = array.index('/^*'[index])		# Get the index into the list of c
		replacement = ''			# Placeholder for the text replacement
		l, r = array[i - 1], array[i + 1]	# The left and righthand sides of the operator

		# If division, insert l and r as-is in the {}'s
		if c == '/':
			replacement = '\\frac{%s}{%s}'

		# If exponentiation, we need to wrap the lefthand side with
		#  parenthesis iff it's a meta token (of the form '%\d+')
		elif c == '^':
			replacement = '%s^{%s}'
			# Find all occurrences of '%\d+' in array[i - 1]
			l = re.sub('(%\d+)([}]*)$', '(\\1)\\2', array[i-1])

		# If multiplication, we need to wrap the lefthand side with
		#  parenthesis iff it's a meta token (of the form '%\d+')
		elif c == '*':
			replacement='%s \\cdot %s'
			l = re.sub('(%\d+)([}]*)$', '(\\1)\\2', array[i-1])

		# Replace indices (i - 1, i, i + 1) with the LaTeX string,
		#  inserting our formatted left and righthand sides
		array = array[:i-1] + [replacement % (l, r)] + array[i+2:]
	
	# Return array as-is
	return array

# Takes a string expression of tokens and operators, and returns the string
#  representation of its LaTeX formatting
def Process(e):
	array = re.findall('%*[.\d]+|i|e|π|ψ|\+|-|\*|/|\^',e)	# Split the string into all valid tokens
	array = Format(array, 1)		# Format the list for exponentiation
	array = Format(array, 0)		# Format the list for division
	array = Format(array, 2)		# Format the list for multiplication
	string = ' '.join(array)		# Join all of the tokens in the expression
	string = string.replace('π','\\pi')	# Replace π with LaTeX command
	string = string.replace('ψ','\\psi')	# Replace ψ with LaTeX command
	return string				# Return the processed string

# The main function that performs the conversion
# Takes an expression, and uses a provided list to do work
def f(e, t=[]):
	# Define an inner function to hand to re.sub
	def helper(m):
		l = len(t)		# Get the current length of t
		t.append(Process(m[1]))	# Append the processed form of our matched group
		return f'%{l}'		# Return a meta token referencing the index in t we just added

	# While there are parenthesis still to parse through
	while '(' in e:
		# Find all occurrences of parenthesis with no parenthesis inside them
		#  and pass them to helper. This will replace an expression of the form
		#     (2 / ψ)
		#  with a meta token of the form
		#     %D
		#  where t[D] holds the LaTeX formatted representation of the expression
		#     t[D] = '\\frac{2}{\\psi}'
		e = re.sub('\(([^()]+)\)', helper, e)

	# Since we only processed sub-expressions, process the remainder
	e = Process(e)

	# Go through and replace each meta token with its true value
	# Since we added them 0->len(t)-1, we replace in reverse order
	#  by popping the values off of t
	while t:
		# Replace '%i' with the i-th value of t in our expression
		e = e.replace(f'%{len(t) - 1}', t.pop())

	# Finally wrap the expression in $'s and return
	return f'${e}$'
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Very nice Indeed! \$\endgroup\$
    – Simd
    Feb 23 at 14:53

Your Answer

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

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