Coverage for moptipy / utils / latex.py: 80%
69 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-24 08:49 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-24 08:49 +0000
1"""The latex text format driver."""
3from io import TextIOBase
4from typing import Final
6from moptipy.utils.formatted_string import (
7 NAN,
8 NEGATIVE_INFINITY,
9 NUMBER,
10 POSITIVE_INFINITY,
11 SPECIAL,
12 TEXT,
13)
14from moptipy.utils.text_format import TextFormatDriver
16#: the exponent prefix
17_EPREFIX = r"\hspace*{0.15em}*\hspace*{0.1em}10\textsuperscript{"
19#: special characters in LaTeX
20SPECIAL_CHARS: Final[dict[str, str]] = {
21 "\u2205": r"$\emptyset$",
22 "\u221E": r"$\infty$",
23 "-\u221E": r"$-\infty$",
24 "inf": r"$\infty$",
25 "-inf": r"$-\infty$",
26 "nan": r"$\emptyset$",
27 "\u03b1": r"$\alpha$",
28 "\u2014": "---",
29}
32class LaTeX(TextFormatDriver):
33 r"""
34 The LaTeX text driver.
36 >>> from io import StringIO
37 >>> from moptipy.utils.formatted_string import FormattedStr
38 >>> from moptipy.utils.table import Table
39 >>> s = StringIO()
40 >>> latex = LaTeX.instance()
41 >>> print(str(latex))
42 tex
43 >>> with Table(s, "lrc", latex) as t:
44 ... with t.header() as header:
45 ... with header.row() as h:
46 ... h.cell(FormattedStr("1", bold=True))
47 ... h.cell(FormattedStr("2", code=True))
48 ... h.cell(FormattedStr("3", italic=True))
49 ... with t.section() as g:
50 ... with g.row() as r:
51 ... r.cell("a")
52 ... r.cell("b")
53 ... r.cell("c")
54 ... with g.row() as r:
55 ... r.cell("d")
56 ... r.cell("e")
57 ... r.cell("f")
58 >>> print(f"'{s.getvalue()}'")
59 '\begin{tabular}{lrc}%
60 \hline%
61 {\textbf{1}}&{\texttt{2}}&{\textit{3}}\\%
62 \hline%
63 a&b&c\\%
64 d&e&f\\%
65 \hline%
66 \end{tabular}%
67 '
68 """
70 def begin_table_body(self, stream: TextIOBase, cols: str) -> None:
71 """Write the beginning of the table body."""
72 stream.write(f"\\begin{{tabular}}{{{cols}}}%\n")
74 def end_table_body(self, stream: TextIOBase, cols: str) -> None:
75 """Write the ending of the table body."""
76 stream.write("\\end{tabular}%\n")
78 def begin_table_header(self, stream: TextIOBase, cols: str) -> None:
79 """Begin the header of a LaTeX table."""
80 stream.write("\\hline%\n")
82 def end_table_header(self, stream: TextIOBase, cols: str) -> None:
83 """End the header of a LaTeX table."""
84 stream.write("\\hline%\n")
86 def end_table_section(self, stream: TextIOBase, cols: str,
87 section_index: int, n_rows: int) -> None:
88 """End a table section."""
89 stream.write("\\hline%\n")
91 def end_table_section_header(self, stream: TextIOBase, cols: str,
92 section_index: int) -> None:
93 """End a table section header."""
94 stream.write("\\hline%\n")
96 def end_table_row(self, stream: TextIOBase, cols: str,
97 section_index: int, row_index: int) -> None:
98 """End a row in a LaTeX table."""
99 stream.write("\\\\%\n")
101 def begin_table_cell(self, stream: TextIOBase, cols: str,
102 section_index: int, row_index: int,
103 col_index: int, cell_mode: int) -> None:
104 """Begin a LaTeX table cell."""
105 if col_index > 0:
106 stream.write("&")
108 def text(self, stream: TextIOBase, text: str, bold: bool, italic: bool,
109 code: bool, mode: int) -> None:
110 """Print a text string."""
111 if len(text) <= 0:
112 return
113 if bold:
114 stream.write("{\\textbf{")
115 if italic:
116 stream.write("{\\textit{")
117 if code:
118 stream.write("{\\texttt{")
120 if mode == TEXT:
121 stream.write(text.replace("_", "\\_"))
122 elif mode == NUMBER:
123 i: int = text.find("e")
124 if i < 0:
125 i = text.find("E")
126 if i > 0:
127 stream.write(f"{text[:i]}{_EPREFIX}{text[i + 1:]}}}")
128 else:
129 stream.write(text.replace("_", "\\_"))
130 elif mode == NAN:
131 stream.write(r"$\emptyset$")
132 elif mode == POSITIVE_INFINITY:
133 stream.write(r"$\infty$")
134 elif mode == NEGATIVE_INFINITY:
135 stream.write(r"$-\infty")
136 elif mode == SPECIAL:
137 s: Final[str] = str(text)
138 if s not in SPECIAL_CHARS:
139 raise ValueError(f"invalid special character: {s!r}")
140 stream.write(SPECIAL_CHARS[s])
141 else:
142 raise ValueError(f"invalid mode {mode} for text {text!r}.")
144 if code:
145 stream.write("}}")
146 if italic:
147 stream.write("}}")
148 if bold:
149 stream.write("}}")
151 def __str__(self):
152 """
153 Get the appropriate file suffix.
155 :returns: the file suffix
156 :retval 'tex': always
157 """
158 return "tex"
160 @staticmethod
161 def instance() -> "LaTeX":
162 """
163 Get the LaTeX format singleton instance.
165 :returns: the singleton instance of the LaTeX format
166 """
167 attr: Final[str] = "_instance"
168 func: Final = LaTeX.instance
169 if not hasattr(func, attr):
170 setattr(func, attr, LaTeX())
171 return getattr(func, attr)