1"""
2Module for formatting output data in console (to string).
3"""
4
5from __future__ import annotations
6
7from shutil import get_terminal_size
8from typing import TYPE_CHECKING
9
10import numpy as np
11
12from pandas.io.formats.printing import pprint_thing
13
14if TYPE_CHECKING:
15 from collections.abc import Iterable
16
17 from pandas.io.formats.format import DataFrameFormatter
18
19
20class StringFormatter:
21 """Formatter for string representation of a dataframe."""
22
23 def __init__(self, fmt: DataFrameFormatter, line_width: int | None = None) -> None:
24 self.fmt = fmt
25 self.adj = fmt.adj
26 self.frame = fmt.frame
27 self.line_width = line_width
28
29 def to_string(self) -> str:
30 text = self._get_string_representation()
31 if self.fmt.should_show_dimensions:
32 text = f"{text}{self.fmt.dimensions_info}"
33 return text
34
35 def _get_strcols(self) -> list[list[str]]:
36 strcols = self.fmt.get_strcols()
37 if self.fmt.is_truncated:
38 strcols = self._insert_dot_separators(strcols)
39 return strcols
40
41 def _get_string_representation(self) -> str:
42 if self.fmt.frame.empty:
43 return self._empty_info_line
44
45 strcols = self._get_strcols()
46
47 if self.line_width is None:
48 # no need to wrap around just print the whole frame
49 return self.adj.adjoin(1, *strcols)
50
51 if self._need_to_wrap_around:
52 return self._join_multiline(strcols)
53
54 return self._fit_strcols_to_terminal_width(strcols)
55
56 @property
57 def _empty_info_line(self) -> str:
58 return (
59 f"Empty {type(self.frame).__name__}\n"
60 f"Columns: {pprint_thing(self.frame.columns)}\n"
61 f"Index: {pprint_thing(self.frame.index)}"
62 )
63
64 @property
65 def _need_to_wrap_around(self) -> bool:
66 return bool(self.fmt.max_cols is None or self.fmt.max_cols > 0)
67
68 def _insert_dot_separators(self, strcols: list[list[str]]) -> list[list[str]]:
69 str_index = self.fmt._get_formatted_index(self.fmt.tr_frame)
70 index_length = len(str_index)
71
72 if self.fmt.is_truncated_horizontally:
73 strcols = self._insert_dot_separator_horizontal(strcols, index_length)
74
75 if self.fmt.is_truncated_vertically:
76 strcols = self._insert_dot_separator_vertical(strcols, index_length)
77
78 return strcols
79
80 @property
81 def _adjusted_tr_col_num(self) -> int:
82 return self.fmt.tr_col_num + 1 if self.fmt.index else self.fmt.tr_col_num
83
84 def _insert_dot_separator_horizontal(
85 self, strcols: list[list[str]], index_length: int
86 ) -> list[list[str]]:
87 strcols.insert(self._adjusted_tr_col_num, [" ..."] * index_length)
88 return strcols
89
90 def _insert_dot_separator_vertical(
91 self, strcols: list[list[str]], index_length: int
92 ) -> list[list[str]]:
93 n_header_rows = index_length - len(self.fmt.tr_frame)
94 row_num = self.fmt.tr_row_num
95 for ix, col in enumerate(strcols):
96 cwidth = self.adj.len(col[row_num])
97
98 if self.fmt.is_truncated_horizontally:
99 is_dot_col = ix == self._adjusted_tr_col_num
100 else:
101 is_dot_col = False
102
103 if cwidth > 3 or is_dot_col:
104 dots = "..."
105 else:
106 dots = ".."
107
108 if ix == 0 and self.fmt.index:
109 dot_mode = "left"
110 elif is_dot_col:
111 cwidth = 4
112 dot_mode = "right"
113 else:
114 dot_mode = "right"
115
116 dot_str = self.adj.justify([dots], cwidth, mode=dot_mode)[0]
117 col.insert(row_num + n_header_rows, dot_str)
118 return strcols
119
120 def _join_multiline(self, strcols_input: Iterable[list[str]]) -> str:
121 lwidth = self.line_width
122 adjoin_width = 1
123 strcols = list(strcols_input)
124
125 if self.fmt.index:
126 idx = strcols.pop(0)
127 lwidth -= np.array([self.adj.len(x) for x in idx]).max() + adjoin_width
128
129 col_widths = [
130 np.array([self.adj.len(x) for x in col]).max() if len(col) > 0 else 0
131 for col in strcols
132 ]
133
134 assert lwidth is not None
135 col_bins = _binify(col_widths, lwidth)
136 nbins = len(col_bins)
137
138 str_lst = []
139 start = 0
140 for i, end in enumerate(col_bins):
141 row = strcols[start:end]
142 if self.fmt.index:
143 row.insert(0, idx)
144 if nbins > 1:
145 nrows = len(row[-1])
146 if end <= len(strcols) and i < nbins - 1:
147 row.append([" \\"] + [" "] * (nrows - 1))
148 else:
149 row.append([" "] * nrows)
150 str_lst.append(self.adj.adjoin(adjoin_width, *row))
151 start = end
152 return "\n\n".join(str_lst)
153
154 def _fit_strcols_to_terminal_width(self, strcols: list[list[str]]) -> str:
155 from pandas import Series
156
157 lines = self.adj.adjoin(1, *strcols).split("\n")
158 max_len = Series(lines).str.len().max()
159 # plus truncate dot col
160 width, _ = get_terminal_size()
161 dif = max_len - width
162 # '+ 1' to avoid too wide repr (GH PR #17023)
163 adj_dif = dif + 1
164 col_lens = Series([Series(ele).str.len().max() for ele in strcols])
165 n_cols = len(col_lens)
166 counter = 0
167 while adj_dif > 0 and n_cols > 1:
168 counter += 1
169 mid = round(n_cols / 2)
170 mid_ix = col_lens.index[mid]
171 col_len = col_lens[mid_ix]
172 # adjoin adds one
173 adj_dif -= col_len + 1
174 col_lens = col_lens.drop(mid_ix)
175 n_cols = len(col_lens)
176
177 # subtract index column
178 max_cols_fitted = n_cols - self.fmt.index
179 # GH-21180. Ensure that we print at least two.
180 max_cols_fitted = max(max_cols_fitted, 2)
181 self.fmt.max_cols_fitted = max_cols_fitted
182
183 # Call again _truncate to cut frame appropriately
184 # and then generate string representation
185 self.fmt.truncate()
186 strcols = self._get_strcols()
187 return self.adj.adjoin(1, *strcols)
188
189
190def _binify(cols: list[int], line_width: int) -> list[int]:
191 adjoin_width = 1
192 bins = []
193 curr_width = 0
194 i_last_column = len(cols) - 1
195 for i, w in enumerate(cols):
196 w_adjoined = w + adjoin_width
197 curr_width += w_adjoined
198 if i_last_column == i:
199 wrap = curr_width + 1 > line_width and i > 0
200 else:
201 wrap = curr_width + 2 > line_width and i > 0
202 if wrap:
203 bins.append(i)
204 curr_width = w_adjoined
205
206 bins.append(len(cols))
207 return bins