latex.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831
  1. """
  2. Module for formatting output data in Latex.
  3. """
  4. from __future__ import annotations
  5. from abc import (
  6. ABC,
  7. abstractmethod,
  8. )
  9. from typing import (
  10. TYPE_CHECKING,
  11. Iterator,
  12. Sequence,
  13. )
  14. import numpy as np
  15. from pandas.core.dtypes.generic import ABCMultiIndex
  16. if TYPE_CHECKING:
  17. from pandas.io.formats.format import DataFrameFormatter
  18. def _split_into_full_short_caption(
  19. caption: str | tuple[str, str] | None
  20. ) -> tuple[str, str]:
  21. """Extract full and short captions from caption string/tuple.
  22. Parameters
  23. ----------
  24. caption : str or tuple, optional
  25. Either table caption string or tuple (full_caption, short_caption).
  26. If string is provided, then it is treated as table full caption,
  27. while short_caption is considered an empty string.
  28. Returns
  29. -------
  30. full_caption, short_caption : tuple
  31. Tuple of full_caption, short_caption strings.
  32. """
  33. if caption:
  34. if isinstance(caption, str):
  35. full_caption = caption
  36. short_caption = ""
  37. else:
  38. try:
  39. full_caption, short_caption = caption
  40. except ValueError as err:
  41. msg = "caption must be either a string or a tuple of two strings"
  42. raise ValueError(msg) from err
  43. else:
  44. full_caption = ""
  45. short_caption = ""
  46. return full_caption, short_caption
  47. class RowStringConverter:
  48. r"""Converter for dataframe rows into LaTeX strings.
  49. Parameters
  50. ----------
  51. formatter : `DataFrameFormatter`
  52. Instance of `DataFrameFormatter`.
  53. multicolumn: bool, optional
  54. Whether to use \multicolumn macro.
  55. multicolumn_format: str, optional
  56. Multicolumn format.
  57. multirow: bool, optional
  58. Whether to use \multirow macro.
  59. """
  60. def __init__(
  61. self,
  62. formatter: DataFrameFormatter,
  63. multicolumn: bool = False,
  64. multicolumn_format: str | None = None,
  65. multirow: bool = False,
  66. ) -> None:
  67. self.fmt = formatter
  68. self.frame = self.fmt.frame
  69. self.multicolumn = multicolumn
  70. self.multicolumn_format = multicolumn_format
  71. self.multirow = multirow
  72. self.clinebuf: list[list[int]] = []
  73. self.strcols = self._get_strcols()
  74. self.strrows = list(zip(*self.strcols))
  75. def get_strrow(self, row_num: int) -> str:
  76. """Get string representation of the row."""
  77. row = self.strrows[row_num]
  78. is_multicol = (
  79. row_num < self.column_levels and self.fmt.header and self.multicolumn
  80. )
  81. is_multirow = (
  82. row_num >= self.header_levels
  83. and self.fmt.index
  84. and self.multirow
  85. and self.index_levels > 1
  86. )
  87. is_cline_maybe_required = is_multirow and row_num < len(self.strrows) - 1
  88. crow = self._preprocess_row(row)
  89. if is_multicol:
  90. crow = self._format_multicolumn(crow)
  91. if is_multirow:
  92. crow = self._format_multirow(crow, row_num)
  93. lst = []
  94. lst.append(" & ".join(crow))
  95. lst.append(" \\\\")
  96. if is_cline_maybe_required:
  97. cline = self._compose_cline(row_num, len(self.strcols))
  98. lst.append(cline)
  99. return "".join(lst)
  100. @property
  101. def _header_row_num(self) -> int:
  102. """Number of rows in header."""
  103. return self.header_levels if self.fmt.header else 0
  104. @property
  105. def index_levels(self) -> int:
  106. """Integer number of levels in index."""
  107. return self.frame.index.nlevels
  108. @property
  109. def column_levels(self) -> int:
  110. return self.frame.columns.nlevels
  111. @property
  112. def header_levels(self) -> int:
  113. nlevels = self.column_levels
  114. if self.fmt.has_index_names and self.fmt.show_index_names:
  115. nlevels += 1
  116. return nlevels
  117. def _get_strcols(self) -> list[list[str]]:
  118. """String representation of the columns."""
  119. if self.fmt.frame.empty:
  120. strcols = [[self._empty_info_line]]
  121. else:
  122. strcols = self.fmt.get_strcols()
  123. # reestablish the MultiIndex that has been joined by get_strcols()
  124. if self.fmt.index and isinstance(self.frame.index, ABCMultiIndex):
  125. out = self.frame.index.format(
  126. adjoin=False,
  127. sparsify=self.fmt.sparsify,
  128. names=self.fmt.has_index_names,
  129. na_rep=self.fmt.na_rep,
  130. )
  131. # index.format will sparsify repeated entries with empty strings
  132. # so pad these with some empty space
  133. def pad_empties(x):
  134. for pad in reversed(x):
  135. if pad:
  136. return [x[0]] + [i if i else " " * len(pad) for i in x[1:]]
  137. gen = (pad_empties(i) for i in out)
  138. # Add empty spaces for each column level
  139. clevels = self.frame.columns.nlevels
  140. out = [[" " * len(i[-1])] * clevels + i for i in gen]
  141. # Add the column names to the last index column
  142. cnames = self.frame.columns.names
  143. if any(cnames):
  144. new_names = [i if i else "{}" for i in cnames]
  145. out[self.frame.index.nlevels - 1][:clevels] = new_names
  146. # Get rid of old multiindex column and add new ones
  147. strcols = out + strcols[1:]
  148. return strcols
  149. @property
  150. def _empty_info_line(self) -> str:
  151. return (
  152. f"Empty {type(self.frame).__name__}\n"
  153. f"Columns: {self.frame.columns}\n"
  154. f"Index: {self.frame.index}"
  155. )
  156. def _preprocess_row(self, row: Sequence[str]) -> list[str]:
  157. """Preprocess elements of the row."""
  158. if self.fmt.escape:
  159. crow = _escape_symbols(row)
  160. else:
  161. crow = [x if x else "{}" for x in row]
  162. if self.fmt.bold_rows and self.fmt.index:
  163. crow = _convert_to_bold(crow, self.index_levels)
  164. return crow
  165. def _format_multicolumn(self, row: list[str]) -> list[str]:
  166. r"""
  167. Combine columns belonging to a group to a single multicolumn entry
  168. according to self.multicolumn_format
  169. e.g.:
  170. a & & & b & c &
  171. will become
  172. \multicolumn{3}{l}{a} & b & \multicolumn{2}{l}{c}
  173. """
  174. row2 = row[: self.index_levels]
  175. ncol = 1
  176. coltext = ""
  177. def append_col() -> None:
  178. # write multicolumn if needed
  179. if ncol > 1:
  180. row2.append(
  181. f"\\multicolumn{{{ncol:d}}}{{{self.multicolumn_format}}}"
  182. f"{{{coltext.strip()}}}"
  183. )
  184. # don't modify where not needed
  185. else:
  186. row2.append(coltext)
  187. for c in row[self.index_levels :]:
  188. # if next col has text, write the previous
  189. if c.strip():
  190. if coltext:
  191. append_col()
  192. coltext = c
  193. ncol = 1
  194. # if not, add it to the previous multicolumn
  195. else:
  196. ncol += 1
  197. # write last column name
  198. if coltext:
  199. append_col()
  200. return row2
  201. def _format_multirow(self, row: list[str], i: int) -> list[str]:
  202. r"""
  203. Check following rows, whether row should be a multirow
  204. e.g.: becomes:
  205. a & 0 & \multirow{2}{*}{a} & 0 &
  206. & 1 & & 1 &
  207. b & 0 & \cline{1-2}
  208. b & 0 &
  209. """
  210. for j in range(self.index_levels):
  211. if row[j].strip():
  212. nrow = 1
  213. for r in self.strrows[i + 1 :]:
  214. if not r[j].strip():
  215. nrow += 1
  216. else:
  217. break
  218. if nrow > 1:
  219. # overwrite non-multirow entry
  220. row[j] = f"\\multirow{{{nrow:d}}}{{*}}{{{row[j].strip()}}}"
  221. # save when to end the current block with \cline
  222. self.clinebuf.append([i + nrow - 1, j + 1])
  223. return row
  224. def _compose_cline(self, i: int, icol: int) -> str:
  225. """
  226. Create clines after multirow-blocks are finished.
  227. """
  228. lst = []
  229. for cl in self.clinebuf:
  230. if cl[0] == i:
  231. lst.append(f"\n\\cline{{{cl[1]:d}-{icol:d}}}")
  232. # remove entries that have been written to buffer
  233. self.clinebuf = [x for x in self.clinebuf if x[0] != i]
  234. return "".join(lst)
  235. class RowStringIterator(RowStringConverter):
  236. """Iterator over rows of the header or the body of the table."""
  237. @abstractmethod
  238. def __iter__(self) -> Iterator[str]:
  239. """Iterate over LaTeX string representations of rows."""
  240. class RowHeaderIterator(RowStringIterator):
  241. """Iterator for the table header rows."""
  242. def __iter__(self) -> Iterator[str]:
  243. for row_num in range(len(self.strrows)):
  244. if row_num < self._header_row_num:
  245. yield self.get_strrow(row_num)
  246. class RowBodyIterator(RowStringIterator):
  247. """Iterator for the table body rows."""
  248. def __iter__(self) -> Iterator[str]:
  249. for row_num in range(len(self.strrows)):
  250. if row_num >= self._header_row_num:
  251. yield self.get_strrow(row_num)
  252. class TableBuilderAbstract(ABC):
  253. """
  254. Abstract table builder producing string representation of LaTeX table.
  255. Parameters
  256. ----------
  257. formatter : `DataFrameFormatter`
  258. Instance of `DataFrameFormatter`.
  259. column_format: str, optional
  260. Column format, for example, 'rcl' for three columns.
  261. multicolumn: bool, optional
  262. Use multicolumn to enhance MultiIndex columns.
  263. multicolumn_format: str, optional
  264. The alignment for multicolumns, similar to column_format.
  265. multirow: bool, optional
  266. Use multirow to enhance MultiIndex rows.
  267. caption: str, optional
  268. Table caption.
  269. short_caption: str, optional
  270. Table short caption.
  271. label: str, optional
  272. LaTeX label.
  273. position: str, optional
  274. Float placement specifier, for example, 'htb'.
  275. """
  276. def __init__(
  277. self,
  278. formatter: DataFrameFormatter,
  279. column_format: str | None = None,
  280. multicolumn: bool = False,
  281. multicolumn_format: str | None = None,
  282. multirow: bool = False,
  283. caption: str | None = None,
  284. short_caption: str | None = None,
  285. label: str | None = None,
  286. position: str | None = None,
  287. ) -> None:
  288. self.fmt = formatter
  289. self.column_format = column_format
  290. self.multicolumn = multicolumn
  291. self.multicolumn_format = multicolumn_format
  292. self.multirow = multirow
  293. self.caption = caption
  294. self.short_caption = short_caption
  295. self.label = label
  296. self.position = position
  297. def get_result(self) -> str:
  298. """String representation of LaTeX table."""
  299. elements = [
  300. self.env_begin,
  301. self.top_separator,
  302. self.header,
  303. self.middle_separator,
  304. self.env_body,
  305. self.bottom_separator,
  306. self.env_end,
  307. ]
  308. result = "\n".join([item for item in elements if item])
  309. trailing_newline = "\n"
  310. result += trailing_newline
  311. return result
  312. @property
  313. @abstractmethod
  314. def env_begin(self) -> str:
  315. """Beginning of the environment."""
  316. @property
  317. @abstractmethod
  318. def top_separator(self) -> str:
  319. """Top level separator."""
  320. @property
  321. @abstractmethod
  322. def header(self) -> str:
  323. """Header lines."""
  324. @property
  325. @abstractmethod
  326. def middle_separator(self) -> str:
  327. """Middle level separator."""
  328. @property
  329. @abstractmethod
  330. def env_body(self) -> str:
  331. """Environment body."""
  332. @property
  333. @abstractmethod
  334. def bottom_separator(self) -> str:
  335. """Bottom level separator."""
  336. @property
  337. @abstractmethod
  338. def env_end(self) -> str:
  339. """End of the environment."""
  340. class GenericTableBuilder(TableBuilderAbstract):
  341. """Table builder producing string representation of LaTeX table."""
  342. @property
  343. def header(self) -> str:
  344. iterator = self._create_row_iterator(over="header")
  345. return "\n".join(list(iterator))
  346. @property
  347. def top_separator(self) -> str:
  348. return "\\toprule"
  349. @property
  350. def middle_separator(self) -> str:
  351. return "\\midrule" if self._is_separator_required() else ""
  352. @property
  353. def env_body(self) -> str:
  354. iterator = self._create_row_iterator(over="body")
  355. return "\n".join(list(iterator))
  356. def _is_separator_required(self) -> bool:
  357. return bool(self.header and self.env_body)
  358. @property
  359. def _position_macro(self) -> str:
  360. r"""Position macro, extracted from self.position, like [h]."""
  361. return f"[{self.position}]" if self.position else ""
  362. @property
  363. def _caption_macro(self) -> str:
  364. r"""Caption macro, extracted from self.caption.
  365. With short caption:
  366. \caption[short_caption]{caption_string}.
  367. Without short caption:
  368. \caption{caption_string}.
  369. """
  370. if self.caption:
  371. return "".join(
  372. [
  373. r"\caption",
  374. f"[{self.short_caption}]" if self.short_caption else "",
  375. f"{{{self.caption}}}",
  376. ]
  377. )
  378. return ""
  379. @property
  380. def _label_macro(self) -> str:
  381. r"""Label macro, extracted from self.label, like \label{ref}."""
  382. return f"\\label{{{self.label}}}" if self.label else ""
  383. def _create_row_iterator(self, over: str) -> RowStringIterator:
  384. """Create iterator over header or body of the table.
  385. Parameters
  386. ----------
  387. over : {'body', 'header'}
  388. Over what to iterate.
  389. Returns
  390. -------
  391. RowStringIterator
  392. Iterator over body or header.
  393. """
  394. iterator_kind = self._select_iterator(over)
  395. return iterator_kind(
  396. formatter=self.fmt,
  397. multicolumn=self.multicolumn,
  398. multicolumn_format=self.multicolumn_format,
  399. multirow=self.multirow,
  400. )
  401. def _select_iterator(self, over: str) -> type[RowStringIterator]:
  402. """Select proper iterator over table rows."""
  403. if over == "header":
  404. return RowHeaderIterator
  405. elif over == "body":
  406. return RowBodyIterator
  407. else:
  408. msg = f"'over' must be either 'header' or 'body', but {over} was provided"
  409. raise ValueError(msg)
  410. class LongTableBuilder(GenericTableBuilder):
  411. """Concrete table builder for longtable.
  412. >>> from pandas.io.formats import format as fmt
  413. >>> df = pd.DataFrame({"a": [1, 2], "b": ["b1", "b2"]})
  414. >>> formatter = fmt.DataFrameFormatter(df)
  415. >>> builder = LongTableBuilder(formatter, caption='a long table',
  416. ... label='tab:long', column_format='lrl')
  417. >>> table = builder.get_result()
  418. >>> print(table)
  419. \\begin{longtable}{lrl}
  420. \\caption{a long table}
  421. \\label{tab:long}\\\\
  422. \\toprule
  423. {} & a & b \\\\
  424. \\midrule
  425. \\endfirsthead
  426. \\caption[]{a long table} \\\\
  427. \\toprule
  428. {} & a & b \\\\
  429. \\midrule
  430. \\endhead
  431. \\midrule
  432. \\multicolumn{3}{r}{{Continued on next page}} \\\\
  433. \\midrule
  434. \\endfoot
  435. <BLANKLINE>
  436. \\bottomrule
  437. \\endlastfoot
  438. 0 & 1 & b1 \\\\
  439. 1 & 2 & b2 \\\\
  440. \\end{longtable}
  441. <BLANKLINE>
  442. """
  443. @property
  444. def env_begin(self) -> str:
  445. first_row = (
  446. f"\\begin{{longtable}}{self._position_macro}{{{self.column_format}}}"
  447. )
  448. elements = [first_row, f"{self._caption_and_label()}"]
  449. return "\n".join([item for item in elements if item])
  450. def _caption_and_label(self) -> str:
  451. if self.caption or self.label:
  452. double_backslash = "\\\\"
  453. elements = [f"{self._caption_macro}", f"{self._label_macro}"]
  454. caption_and_label = "\n".join([item for item in elements if item])
  455. caption_and_label += double_backslash
  456. return caption_and_label
  457. else:
  458. return ""
  459. @property
  460. def middle_separator(self) -> str:
  461. iterator = self._create_row_iterator(over="header")
  462. # the content between \endfirsthead and \endhead commands
  463. # mitigates repeated List of Tables entries in the final LaTeX
  464. # document when dealing with longtable environments; GH #34360
  465. elements = [
  466. "\\midrule",
  467. "\\endfirsthead",
  468. f"\\caption[]{{{self.caption}}} \\\\" if self.caption else "",
  469. self.top_separator,
  470. self.header,
  471. "\\midrule",
  472. "\\endhead",
  473. "\\midrule",
  474. f"\\multicolumn{{{len(iterator.strcols)}}}{{r}}"
  475. "{{Continued on next page}} \\\\",
  476. "\\midrule",
  477. "\\endfoot\n",
  478. "\\bottomrule",
  479. "\\endlastfoot",
  480. ]
  481. if self._is_separator_required():
  482. return "\n".join(elements)
  483. return ""
  484. @property
  485. def bottom_separator(self) -> str:
  486. return ""
  487. @property
  488. def env_end(self) -> str:
  489. return "\\end{longtable}"
  490. class RegularTableBuilder(GenericTableBuilder):
  491. """Concrete table builder for regular table.
  492. >>> from pandas.io.formats import format as fmt
  493. >>> df = pd.DataFrame({"a": [1, 2], "b": ["b1", "b2"]})
  494. >>> formatter = fmt.DataFrameFormatter(df)
  495. >>> builder = RegularTableBuilder(formatter, caption='caption', label='lab',
  496. ... column_format='lrc')
  497. >>> table = builder.get_result()
  498. >>> print(table)
  499. \\begin{table}
  500. \\centering
  501. \\caption{caption}
  502. \\label{lab}
  503. \\begin{tabular}{lrc}
  504. \\toprule
  505. {} & a & b \\\\
  506. \\midrule
  507. 0 & 1 & b1 \\\\
  508. 1 & 2 & b2 \\\\
  509. \\bottomrule
  510. \\end{tabular}
  511. \\end{table}
  512. <BLANKLINE>
  513. """
  514. @property
  515. def env_begin(self) -> str:
  516. elements = [
  517. f"\\begin{{table}}{self._position_macro}",
  518. "\\centering",
  519. f"{self._caption_macro}",
  520. f"{self._label_macro}",
  521. f"\\begin{{tabular}}{{{self.column_format}}}",
  522. ]
  523. return "\n".join([item for item in elements if item])
  524. @property
  525. def bottom_separator(self) -> str:
  526. return "\\bottomrule"
  527. @property
  528. def env_end(self) -> str:
  529. return "\n".join(["\\end{tabular}", "\\end{table}"])
  530. class TabularBuilder(GenericTableBuilder):
  531. """Concrete table builder for tabular environment.
  532. >>> from pandas.io.formats import format as fmt
  533. >>> df = pd.DataFrame({"a": [1, 2], "b": ["b1", "b2"]})
  534. >>> formatter = fmt.DataFrameFormatter(df)
  535. >>> builder = TabularBuilder(formatter, column_format='lrc')
  536. >>> table = builder.get_result()
  537. >>> print(table)
  538. \\begin{tabular}{lrc}
  539. \\toprule
  540. {} & a & b \\\\
  541. \\midrule
  542. 0 & 1 & b1 \\\\
  543. 1 & 2 & b2 \\\\
  544. \\bottomrule
  545. \\end{tabular}
  546. <BLANKLINE>
  547. """
  548. @property
  549. def env_begin(self) -> str:
  550. return f"\\begin{{tabular}}{{{self.column_format}}}"
  551. @property
  552. def bottom_separator(self) -> str:
  553. return "\\bottomrule"
  554. @property
  555. def env_end(self) -> str:
  556. return "\\end{tabular}"
  557. class LatexFormatter:
  558. r"""
  559. Used to render a DataFrame to a LaTeX tabular/longtable environment output.
  560. Parameters
  561. ----------
  562. formatter : `DataFrameFormatter`
  563. longtable : bool, default False
  564. Use longtable environment.
  565. column_format : str, default None
  566. The columns format as specified in `LaTeX table format
  567. <https://en.wikibooks.org/wiki/LaTeX/Tables>`__ e.g 'rcl' for 3 columns
  568. multicolumn : bool, default False
  569. Use \multicolumn to enhance MultiIndex columns.
  570. multicolumn_format : str, default 'l'
  571. The alignment for multicolumns, similar to `column_format`
  572. multirow : bool, default False
  573. Use \multirow to enhance MultiIndex rows.
  574. caption : str or tuple, optional
  575. Tuple (full_caption, short_caption),
  576. which results in \caption[short_caption]{full_caption};
  577. if a single string is passed, no short caption will be set.
  578. label : str, optional
  579. The LaTeX label to be placed inside ``\label{}`` in the output.
  580. position : str, optional
  581. The LaTeX positional argument for tables, to be placed after
  582. ``\begin{}`` in the output.
  583. See Also
  584. --------
  585. HTMLFormatter
  586. """
  587. def __init__(
  588. self,
  589. formatter: DataFrameFormatter,
  590. longtable: bool = False,
  591. column_format: str | None = None,
  592. multicolumn: bool = False,
  593. multicolumn_format: str | None = None,
  594. multirow: bool = False,
  595. caption: str | tuple[str, str] | None = None,
  596. label: str | None = None,
  597. position: str | None = None,
  598. ) -> None:
  599. self.fmt = formatter
  600. self.frame = self.fmt.frame
  601. self.longtable = longtable
  602. self.column_format = column_format
  603. self.multicolumn = multicolumn
  604. self.multicolumn_format = multicolumn_format
  605. self.multirow = multirow
  606. self.caption, self.short_caption = _split_into_full_short_caption(caption)
  607. self.label = label
  608. self.position = position
  609. def to_string(self) -> str:
  610. """
  611. Render a DataFrame to a LaTeX tabular, longtable, or table/tabular
  612. environment output.
  613. """
  614. return self.builder.get_result()
  615. @property
  616. def builder(self) -> TableBuilderAbstract:
  617. """Concrete table builder.
  618. Returns
  619. -------
  620. TableBuilder
  621. """
  622. builder = self._select_builder()
  623. return builder(
  624. formatter=self.fmt,
  625. column_format=self.column_format,
  626. multicolumn=self.multicolumn,
  627. multicolumn_format=self.multicolumn_format,
  628. multirow=self.multirow,
  629. caption=self.caption,
  630. short_caption=self.short_caption,
  631. label=self.label,
  632. position=self.position,
  633. )
  634. def _select_builder(self) -> type[TableBuilderAbstract]:
  635. """Select proper table builder."""
  636. if self.longtable:
  637. return LongTableBuilder
  638. if any([self.caption, self.label, self.position]):
  639. return RegularTableBuilder
  640. return TabularBuilder
  641. @property
  642. def column_format(self) -> str | None:
  643. """Column format."""
  644. return self._column_format
  645. @column_format.setter
  646. def column_format(self, input_column_format: str | None) -> None:
  647. """Setter for column format."""
  648. if input_column_format is None:
  649. self._column_format = (
  650. self._get_index_format() + self._get_column_format_based_on_dtypes()
  651. )
  652. elif not isinstance(input_column_format, str):
  653. raise ValueError(
  654. f"column_format must be str or unicode, "
  655. f"not {type(input_column_format)}"
  656. )
  657. else:
  658. self._column_format = input_column_format
  659. def _get_column_format_based_on_dtypes(self) -> str:
  660. """Get column format based on data type.
  661. Right alignment for numbers and left - for strings.
  662. """
  663. def get_col_type(dtype) -> str:
  664. if issubclass(dtype.type, np.number):
  665. return "r"
  666. return "l"
  667. dtypes = self.frame.dtypes._values
  668. return "".join(map(get_col_type, dtypes))
  669. def _get_index_format(self) -> str:
  670. """Get index column format."""
  671. return "l" * self.frame.index.nlevels if self.fmt.index else ""
  672. def _escape_symbols(row: Sequence[str]) -> list[str]:
  673. """Carry out string replacements for special symbols.
  674. Parameters
  675. ----------
  676. row : list
  677. List of string, that may contain special symbols.
  678. Returns
  679. -------
  680. list
  681. list of strings with the special symbols replaced.
  682. """
  683. return [
  684. (
  685. x.replace("\\", "\\textbackslash ")
  686. .replace("_", "\\_")
  687. .replace("%", "\\%")
  688. .replace("$", "\\$")
  689. .replace("#", "\\#")
  690. .replace("{", "\\{")
  691. .replace("}", "\\}")
  692. .replace("~", "\\textasciitilde ")
  693. .replace("^", "\\textasciicircum ")
  694. .replace("&", "\\&")
  695. if (x and x != "{}")
  696. else "{}"
  697. )
  698. for x in row
  699. ]
  700. def _convert_to_bold(crow: Sequence[str], ilevels: int) -> list[str]:
  701. """Convert elements in ``crow`` to bold."""
  702. return [
  703. f"\\textbf{{{x}}}" if j < ilevels and x.strip() not in ["", "{}"] else x
  704. for j, x in enumerate(crow)
  705. ]
  706. if __name__ == "__main__":
  707. import doctest
  708. doctest.testmod()