Skip to content

ENH: add LaTeX math mode with parentheses #51903

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 55 additions & 16 deletions pandas/io/formats/style_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -1117,7 +1117,8 @@ def format(
2 & \textbf{\$\%\#} \\
\end{tabular}

Using ``escape`` in 'latex-math' mode.
Applying ``escape`` in 'latex-math' mode. In the example below
we enter math mode using the charackter ``$``.

>>> df = pd.DataFrame([[r"$\sum_{i=1}^{10} a_i$ a~b $\alpha \
... = \frac{\beta}{\zeta^2}$"], ["%#^ $ \$x^2 $"]])
Expand All @@ -1129,6 +1130,20 @@ def format(
1 & \%\#\textasciicircum \space $ \$x^2 $ \\
\end{tabular}

We can use the charackter ``\(`` to enter math mode and the charackter ``\)``
to close math mode.

>>> df = pd.DataFrame([[r"\(\sum_{i=1}^{10} a_i\) a~b \(\alpha \
... = \frac{\beta}{\zeta^2}\)"], ["%#^ \( \$x^2 \)"]])
>>> df.style.format(escape="latex-math").to_latex()
... # doctest: +SKIP
\begin{tabular}{ll}
& 0 \\
0 & \(\sum_{i=1}^{10} a_i\) a\textasciitilde b \(\alpha
= \frac{\beta}{\zeta^2}\) \\
1 & \%\#\textasciicircum \space \( \$x^2 \) \\
\end{tabular}

Pandas defines a `number-format` pseudo CSS attribute instead of the `.format`
method to create `to_excel` permissible formatting. Note that semi-colons are
CSS protected characters but used as separators in Excel's format string.
Expand Down Expand Up @@ -2357,17 +2372,20 @@ def _escape_latex(s):
.replace("~", "\\textasciitilde ")
.replace("^ ", "^\\space ") # since \textasciicircum gobbles spaces
.replace("^", "\\textasciicircum ")
.replace("ab2§=§8yz(", "\\( ")
.replace("ab2§=§8yz)", "\\) ")
.replace("ab2§=§8yz", "\\textbackslash ")
)


def _escape_latex_math(s):
r"""
All characters between two characters ``$`` are preserved.
All characters in LaTeX math mode are preserved.

The substrings in LaTeX math mode, which start with the character ``$``
and end with ``$``, are preserved without escaping. Otherwise
regular LaTeX escaping applies. See ``_escape_latex()``.
The substrings in LaTeX math mode, which either are surrounded
by two characters ``$`` or start with the character ``\(`` and end with ``\)``,
are preserved without escaping. Otherwise regular LaTeX escaping applies.
See ``_escape_latex()``.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_escape_latex is an internal function so shouldn't be publically stated or pointed to

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

completely agree, I'll remove _escape_latex() from docs


Parameters
----------
Expand All @@ -2379,16 +2397,37 @@ def _escape_latex_math(s):
str :
Escaped string
"""
s = s.replace(r"\$", r"rt8§=§7wz")
pattern = re.compile(r"\$.*?\$")
pos = 0
ps = pattern.search(s, pos)
res = []
while ps:
res.append(_escape_latex(s[pos : ps.span()[0]]))
res.append(ps.group())
pos = ps.span()[1]

def _math_mode_with_dollar(s):
s = s.replace(r"\$", r"rt8§=§7wz")
pattern = re.compile(r"\$.*?\$")
pos = 0
ps = pattern.search(s, pos)
res = []
while ps:
res.append(_escape_latex(s[pos : ps.span()[0]]))
res.append(ps.group())
pos = ps.span()[1]
ps = pattern.search(s, pos)

res.append(_escape_latex(s[pos : len(s)]))
return "".join(res).replace(r"rt8§=§7wz", r"\$")

def _math_mode_with_parentheses(s):
s = s.replace(r"\(", r"LEFT§=§6yzLEFT").replace(r"\)", r"RIGHTab5§=§RIGHT")
res = []
for item in re.split(r"LEFT§=§6yz|ab5§=§RIGHT", s):
if item.startswith("LEFT") and item.endswith("RIGHT"):
res.append(item.replace("LEFT", r"\(").replace("RIGHT", r"\)"))
else:
res.append(
_escape_latex(item).replace("LEFT", r"\(").replace("RIGHT", r"\)")
)
return "".join(res)

res.append(_escape_latex(s[pos : len(s)]))
return "".join(res).replace(r"rt8§=§7wz", r"\$")
if s.replace(r"\$", "ab").find(r"$") > -1:
return _math_mode_with_dollar(s)
elif s.find(r"\(") > -1:
return _math_mode_with_parentheses(s)
else:
return _escape_latex(s)
19 changes: 14 additions & 5 deletions pandas/tests/io/formats/style/test_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,13 +192,22 @@ def test_format_escape_html(escape, exp):
assert styler._translate(True, True)["head"][0][1]["display_value"] == f"&{exp}&"


def test_format_escape_latex_math():
chars = r"$\frac{1}{2} \$ x^2$ ~%#^"
df = DataFrame([[chars]])
@pytest.mark.parametrize(
"chars, expected",
[
(r"$\frac{1}{2} \$ x^2$ ", r"$\frac{1}{2} \$ x^2$ "),
(r"\(\frac{1}{2} \$ x^2\) ", r"\(\frac{1}{2} \$ x^2\) "),
(r"\)", r"\) "),
],
)
def test_format_escape_latex_math(chars, expected):
df = DataFrame([["".join([chars, "~%#^"])]])

expected = r"$\frac{1}{2} \$ x^2$ \textasciitilde \%\#\textasciicircum "
s = df.style.format("{0}", escape="latex-math")
assert expected == s._translate(True, True)["body"][0][1]["display_value"]
assert (
"".join([expected, r"\textasciitilde \%\#\textasciicircum "])
== s._translate(True, True)["body"][0][1]["display_value"]
)


def test_format_escape_na_rep():
Expand Down