|
1 |
| -import enum |
2 |
| -from dataclasses import dataclass |
3 |
| -from math import ceil, floor |
4 |
| -from typing import List, Optional, Union |
| 1 | +from .table_to_ascii import table2ascii |
| 2 | +from .alignment import Alignment |
5 | 3 |
|
6 |
| - |
7 |
| -class Alignment(enum.Enum): |
8 |
| - """Enum for alignment types""" |
9 |
| - |
10 |
| - LEFT = 0 |
11 |
| - CENTER = 1 |
12 |
| - RIGHT = 2 |
13 |
| - |
14 |
| - |
15 |
| -@dataclass |
16 |
| -class Options: |
17 |
| - """Class for storing options that the user sets""" |
18 |
| - |
19 |
| - header: Optional[List] = None |
20 |
| - body: Optional[List[List]] = None |
21 |
| - footer: Optional[List] = None |
22 |
| - first_col_heading: bool = False |
23 |
| - last_col_heading: bool = False |
24 |
| - column_widths: Optional[List[int]] = None |
25 |
| - alignments: Optional[List[Alignment]] = None |
26 |
| - |
27 |
| - |
28 |
| -class TableToAscii: |
29 |
| - """Class used to convert a 2D Python table to ASCII text""" |
30 |
| - |
31 |
| - def __init__(self, options: Options): |
32 |
| - """Validate arguments and initialize fields""" |
33 |
| - # initialize fields |
34 |
| - self.__header = options.header |
35 |
| - self.__body = options.body |
36 |
| - self.__footer = options.footer |
37 |
| - self.__first_col_heading = options.first_col_heading |
38 |
| - self.__last_col_heading = options.last_col_heading |
39 |
| - |
40 |
| - # calculate number of columns |
41 |
| - self.__columns = self.__count_columns() |
42 |
| - |
43 |
| - # check if footer has a different number of columns |
44 |
| - if options.footer and len(options.footer) != self.__columns: |
45 |
| - raise ValueError( |
46 |
| - "Footer must have the same number of columns as the other rows" |
47 |
| - ) |
48 |
| - # check if any rows in body have a different number of columns |
49 |
| - if options.body and any(len(row) != self.__columns for row in options.body): |
50 |
| - raise ValueError( |
51 |
| - "All rows in body must have the same number of columns as the other rows" |
52 |
| - ) |
53 |
| - |
54 |
| - # calculate or use given column widths |
55 |
| - self.__column_widths = options.column_widths or self.__auto_column_widths() |
56 |
| - |
57 |
| - # check if column widths specified have a different number of columns |
58 |
| - if options.column_widths and len(options.column_widths) != self.__columns: |
59 |
| - raise ValueError( |
60 |
| - "Length of `column_widths` list must equal the number of columns" |
61 |
| - ) |
62 |
| - # check if column widths are not all at least 2 |
63 |
| - if options.column_widths and min(options.column_widths) < 2: |
64 |
| - raise ValueError( |
65 |
| - "All values in `column_widths` must be greater than or equal to 2" |
66 |
| - ) |
67 |
| - |
68 |
| - self.__alignments = options.alignments or [Alignment.CENTER] * self.__columns |
69 |
| - |
70 |
| - # check if alignments specified have a different number of columns |
71 |
| - if options.alignments and len(options.alignments) != self.__columns: |
72 |
| - raise ValueError( |
73 |
| - "Length of `alignments` list must equal the number of columns" |
74 |
| - ) |
75 |
| - |
76 |
| - """ |
77 |
| - ╔═════╦═══════════════════════╗ ABBBBBCBBBBBDBBBBBDBBBBBDBBBBBE |
78 |
| - ║ # ║ G H R S ║ F G H H H F |
79 |
| - ╟─────╫───────────────────────╢ IJJJJJKJJJJJLJJJJJLJJJJJLJJJJJM |
80 |
| - ║ 1 ║ 30 40 35 30 ║ F G H H H F |
81 |
| - ║ 2 ║ 30 40 35 30 ║ F G H H H F |
82 |
| - ╟─────╫───────────────────────╢ NOOOOOPOOOOOQOOOOOQOOOOOQOOOOOR |
83 |
| - ║ SUM ║ 130 140 135 130 ║ F G H H H F |
84 |
| - ╚═════╩═══════════════════════╝ SBBBBBTBBBBBUBBBBBUBBBBBUBBBBBV |
85 |
| - """ |
86 |
| - self.__parts = { |
87 |
| - "top_left_corner": "╔", # A |
88 |
| - "top_and_bottom_edge": "═", # B |
89 |
| - "heading_col_top_tee": "╦", # C |
90 |
| - "top_tee": "═", # D |
91 |
| - "top_right_corner": "╗", # E |
92 |
| - "left_and_right_edge": "║", # F |
93 |
| - "heading_col_sep": "║", # G |
94 |
| - "middle_edge": " ", # H |
95 |
| - "header_left_tee": "╟", # I |
96 |
| - "header_row_sep": "─", # J |
97 |
| - "heading_col_header_cross": "╫", # K |
98 |
| - "header_row_cross": "─", # L |
99 |
| - "header_right_tee": "╢", # M |
100 |
| - "footer_left_tee": "╟", # N |
101 |
| - "footer_row_sep": "─", # O |
102 |
| - "heading_col_footer_cross": "╫", # P |
103 |
| - "footer_row_cross": "─", # Q |
104 |
| - "footer_right_tee": "╢", # R |
105 |
| - "bottom_left_corner": "╚", # S |
106 |
| - "heading_col_bottom_tee": "╩", # T |
107 |
| - "bottom_tee": "═", # U |
108 |
| - "bottom_right_corner": "╝", # V |
109 |
| - } |
110 |
| - |
111 |
| - def __count_columns(self) -> int: |
112 |
| - """Get the number of columns in the table |
113 |
| - based on the provided header, footer, and body lists. |
114 |
| - """ |
115 |
| - if self.__header: |
116 |
| - return len(self.__header) |
117 |
| - if self.__footer: |
118 |
| - return len(self.__footer) |
119 |
| - if self.__body and len(self.__body) > 0: |
120 |
| - return len(self.__body[0]) |
121 |
| - return 0 |
122 |
| - |
123 |
| - def __auto_column_widths(self) -> List[int]: |
124 |
| - """Get the minimum number of characters needed for the values |
125 |
| - in each column in the table with 1 space of padding on each side. |
126 |
| - """ |
127 |
| - column_widths = [] |
128 |
| - for i in range(self.__columns): |
129 |
| - # number of characters in column of i of header, each body row, and footer |
130 |
| - header_size = len(self.__header[i]) if self.__header else 0 |
131 |
| - body_size = ( |
132 |
| - map(lambda row, i=i: len(row[i]), self.__body) if self.__body else [0] |
133 |
| - ) |
134 |
| - footer_size = len(self.__footer[i]) if self.__footer else 0 |
135 |
| - # get the max and add 2 for padding each side with a space |
136 |
| - column_widths.append(max(header_size, *body_size, footer_size) + 2) |
137 |
| - return column_widths |
138 |
| - |
139 |
| - def __pad(self, text: str, width: int, alignment: Alignment): |
140 |
| - """Pad a string of text to a given width with specified alignment""" |
141 |
| - if alignment == Alignment.LEFT: |
142 |
| - # pad with spaces on the end |
143 |
| - return f" {text} " + (" " * (width - len(text) - 2)) |
144 |
| - if alignment == Alignment.CENTER: |
145 |
| - # pad with spaces, half on each side |
146 |
| - before = " " * floor((width - len(text) - 2) / 2) |
147 |
| - after = " " * ceil((width - len(text) - 2) / 2) |
148 |
| - return before + f" {text} " + after |
149 |
| - if alignment == Alignment.RIGHT: |
150 |
| - # pad with spaces at the beginning |
151 |
| - return (" " * (width - len(text) - 2)) + f" {text} " |
152 |
| - raise ValueError(f"The value '{alignment}' is not valid for alignment.") |
153 |
| - |
154 |
| - def __row_to_ascii( |
155 |
| - self, |
156 |
| - left_edge: str, |
157 |
| - heading_col_sep: str, |
158 |
| - column_seperator: str, |
159 |
| - right_edge: str, |
160 |
| - filler: Union[str, List], |
161 |
| - ) -> str: |
162 |
| - """Assembles a row of the ascii table""" |
163 |
| - # left edge of the row |
164 |
| - output = left_edge |
165 |
| - # add columns |
166 |
| - for i in range(self.__columns): |
167 |
| - # content between separators |
168 |
| - output += ( |
169 |
| - # edge or row separator if filler is a specific character |
170 |
| - filler * self.__column_widths[i] |
171 |
| - if isinstance(filler, str) |
172 |
| - # otherwise, use the column content |
173 |
| - else self.__pad( |
174 |
| - str(filler[i]), self.__column_widths[i], self.__alignments[i] |
175 |
| - ) |
176 |
| - ) |
177 |
| - # column seperator |
178 |
| - sep = column_seperator |
179 |
| - if i == 0 and self.__first_col_heading: |
180 |
| - # use column heading if first column option is specified |
181 |
| - sep = heading_col_sep |
182 |
| - elif i == self.__columns - 2 and self.__last_col_heading: |
183 |
| - # use column heading if last column option is specified |
184 |
| - sep = heading_col_sep |
185 |
| - elif i == self.__columns - 1: |
186 |
| - # replace last seperator with symbol for edge of the row |
187 |
| - sep = right_edge |
188 |
| - output += sep |
189 |
| - return output + "\n" |
190 |
| - |
191 |
| - def __top_edge_to_ascii(self) -> str: |
192 |
| - """Assembles the top edge of the ascii table""" |
193 |
| - return self.__row_to_ascii( |
194 |
| - left_edge=self.__parts["top_left_corner"], |
195 |
| - heading_col_sep=self.__parts["heading_col_top_tee"], |
196 |
| - column_seperator=self.__parts["top_tee"], |
197 |
| - right_edge=self.__parts["top_right_corner"], |
198 |
| - filler=self.__parts["top_and_bottom_edge"], |
199 |
| - ) |
200 |
| - |
201 |
| - def __bottom_edge_to_ascii(self) -> str: |
202 |
| - """Assembles the top edge of the ascii table""" |
203 |
| - return self.__row_to_ascii( |
204 |
| - left_edge=self.__parts["bottom_left_corner"], |
205 |
| - heading_col_sep=self.__parts["heading_col_bottom_tee"], |
206 |
| - column_seperator=self.__parts["bottom_tee"], |
207 |
| - right_edge=self.__parts["bottom_right_corner"], |
208 |
| - filler=self.__parts["top_and_bottom_edge"], |
209 |
| - ) |
210 |
| - |
211 |
| - def __header_row_to_ascii(self) -> str: |
212 |
| - """Assembles the header row line of the ascii table""" |
213 |
| - return self.__row_to_ascii( |
214 |
| - left_edge=self.__parts["left_and_right_edge"], |
215 |
| - heading_col_sep=self.__parts["heading_col_sep"], |
216 |
| - column_seperator=self.__parts["middle_edge"], |
217 |
| - right_edge=self.__parts["left_and_right_edge"], |
218 |
| - filler=self.__header, |
219 |
| - ) |
220 |
| - |
221 |
| - def __footer_row_to_ascii(self) -> str: |
222 |
| - """Assembles the header row line of the ascii table""" |
223 |
| - return self.__row_to_ascii( |
224 |
| - left_edge=self.__parts["left_and_right_edge"], |
225 |
| - heading_col_sep=self.__parts["heading_col_sep"], |
226 |
| - column_seperator=self.__parts["middle_edge"], |
227 |
| - right_edge=self.__parts["left_and_right_edge"], |
228 |
| - filler=self.__footer, |
229 |
| - ) |
230 |
| - |
231 |
| - def __header_sep_to_ascii(self) -> str: |
232 |
| - """Assembles the seperator below the header of the ascii table""" |
233 |
| - return self.__row_to_ascii( |
234 |
| - left_edge=self.__parts["header_left_tee"], |
235 |
| - heading_col_sep=self.__parts["heading_col_header_cross"], |
236 |
| - column_seperator=self.__parts["header_row_cross"], |
237 |
| - right_edge=self.__parts["header_right_tee"], |
238 |
| - filler=self.__parts["header_row_sep"], |
239 |
| - ) |
240 |
| - |
241 |
| - def __footer_sep_to_ascii(self) -> str: |
242 |
| - """Assembles the seperator below the header of the ascii table""" |
243 |
| - return self.__row_to_ascii( |
244 |
| - left_edge=self.__parts["footer_left_tee"], |
245 |
| - heading_col_sep=self.__parts["heading_col_footer_cross"], |
246 |
| - column_seperator=self.__parts["footer_row_cross"], |
247 |
| - right_edge=self.__parts["footer_right_tee"], |
248 |
| - filler=self.__parts["footer_row_sep"], |
249 |
| - ) |
250 |
| - |
251 |
| - def __body_to_ascii(self) -> str: |
252 |
| - return "".join( |
253 |
| - self.__row_to_ascii( |
254 |
| - left_edge=self.__parts["left_and_right_edge"], |
255 |
| - heading_col_sep=self.__parts["heading_col_sep"], |
256 |
| - column_seperator=self.__parts["middle_edge"], |
257 |
| - right_edge=self.__parts["left_and_right_edge"], |
258 |
| - filler=row, |
259 |
| - ) |
260 |
| - for row in self.__body |
261 |
| - ) |
262 |
| - |
263 |
| - def to_ascii(self) -> str: |
264 |
| - # top row of table |
265 |
| - table = self.__top_edge_to_ascii() |
266 |
| - # add table header |
267 |
| - if self.__header: |
268 |
| - table += self.__header_row_to_ascii() |
269 |
| - table += self.__header_sep_to_ascii() |
270 |
| - # add table body |
271 |
| - if self.__body: |
272 |
| - table += self.__body_to_ascii() |
273 |
| - # add table footer |
274 |
| - if self.__footer: |
275 |
| - table += self.__footer_sep_to_ascii() |
276 |
| - table += self.__footer_row_to_ascii() |
277 |
| - # bottom row of table |
278 |
| - table += self.__bottom_edge_to_ascii() |
279 |
| - # reurn ascii table |
280 |
| - return table |
281 |
| - |
282 |
| - |
283 |
| -def table2ascii(**options) -> str: |
284 |
| - """Convert a 2D Python table to ASCII text |
285 |
| -
|
286 |
| - ### Arguments |
287 |
| - :param header: :class:`Optional[List]` List of column values in the table's header row |
288 |
| - :param body: :class:`Optional[List[List]]` 2-dimensional list of values in the table's body |
289 |
| - :param footer: :class:`Optional[List]` List of column values in the table's footer row |
290 |
| - :param column_widths: :class:`Optional[List[int]]` List of widths in characters for each column (defaults to auto-sizing) |
291 |
| - :param alignments: :class:`Optional[List[Alignment]]` List of alignments (ex. `[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT]`) |
292 |
| - :param first_col_heading: :class:`Optional[bool]` Whether to add a header column separator after the first column |
293 |
| - :param last_col_heading: :class:`Optional[bool]` Whether to add a header column separator before the last column |
294 |
| - """ |
295 |
| - return TableToAscii(Options(**options)).to_ascii() |
| 4 | +__all__ = [ |
| 5 | + "table2ascii", |
| 6 | + "Alignment", |
| 7 | +] |
0 commit comments