From dc5be5c778077ed6d1dca1eeecf195a381ee578c Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Fri, 11 Feb 2022 14:33:34 -0700 Subject: [PATCH 1/6] feat: support for multiline cells --- table2ascii/table_to_ascii.py | 77 ++++++++++++++++++++--------------- tests/test_alignments.py | 25 ++++++++++++ tests/test_convert.py | 24 +++++++++++ 3 files changed, 93 insertions(+), 33 deletions(-) diff --git a/table2ascii/table_to_ascii.py b/table2ascii/table_to_ascii.py index 2d331ba..3c2ef62 100644 --- a/table2ascii/table_to_ascii.py +++ b/table2ascii/table_to_ascii.py @@ -91,15 +91,18 @@ def __auto_column_widths(self) -> List[int]: List[int]: The minimum number of characters needed for each column """ column_widths = [] + # lambda function to get the length of the longest line in a multi-line string + longest_line = lambda text: max(len(line) for line in str(text).splitlines()) + # get the width necessary for each column for i in range(self.__columns): # number of characters in column of i of header, each body row, and footer - header_size = len(str(self.__header[i])) if self.__header else 0 + header_size = longest_line(self.__header[i]) if self.__header else 0 body_size = ( - map(lambda row, i=i: len(str(row[i])), self.__body) + map(lambda row, i=i: longest_line(row[i]), self.__body) if self.__body else [0] ) - footer_size = len(str(self.__footer[i])) if self.__footer else 0 + footer_size = longest_line(self.__footer[i]) if self.__footer else 0 # get the max and add 2 for padding each side with a space column_widths.append(max(header_size, *body_size, footer_size) + 2) return column_widths @@ -144,37 +147,45 @@ def __row_to_ascii( Returns: str: The line in the ascii table """ - # left edge of the row - output = left_edge + output = "" + # get number of lines for rendering row + num_lines = max(len(str(cell).splitlines()) for cell in filler) # add columns - for i in range(self.__columns): - # content between separators - output += ( - # edge or row separator if filler is a specific character - filler * self.__column_widths[i] - if isinstance(filler, str) - # otherwise, use the column content - else self.__pad( - filler[i], self.__column_widths[i], self.__alignments[i] - ) - ) - # column seperator - sep = column_seperator - if i == 0 and self.__first_col_heading: - # use column heading if first column option is specified - sep = heading_col_sep - elif i == self.__columns - 2 and self.__last_col_heading: - # use column heading if last column option is specified - sep = heading_col_sep - elif i == self.__columns - 1: - # replace last seperator with symbol for edge of the row - sep = right_edge - output += sep - # don't use separation row if it's only space - if output.strip() == "": - return "" - # otherwise, return the row followed by newline - return output + "\n" + for line in range(num_lines): + # left edge of the row + output += left_edge + for col in range(self.__columns): + # content between separators + col_content = "" + if isinstance(filler, str): + col_content = filler * self.__column_widths[col] + else: + col_lines = str(filler[col]).splitlines() + if line < len(col_lines): + col_content = col_lines[line] + col_content = self.__pad( + col_content, + self.__column_widths[col], + self.__alignments[col], + ) + output += col_content + # column seperator + sep = column_seperator + if col == 0 and self.__first_col_heading: + # use column heading if first column option is specified + sep = heading_col_sep + elif col == self.__columns - 2 and self.__last_col_heading: + # use column heading if last column option is specified + sep = heading_col_sep + elif col == self.__columns - 1: + # replace last seperator with symbol for edge of the row + sep = right_edge + output += sep + output += "\n" + # don't use separation row if it's only space + if output.strip() == "": + output = "" + return output def __top_edge_to_ascii(self) -> str: """ diff --git a/tests/test_alignments.py b/tests/test_alignments.py index f2754ae..3ffd1a9 100644 --- a/tests/test_alignments.py +++ b/tests/test_alignments.py @@ -65,3 +65,28 @@ def test_alignment_numeric_data(): "╚════╩══════════════════════╝" ) assert text == expected + + +def test_alignments_multiline_data(): + text = t2a( + header=["Multiline\nHeader\nCell", "G", "Two\nLines", "R", "S"], + body=[[1, "Alpha\nBeta\nGamma", 3, 4, "One\nTwo"]], + footer=["A", "Footer\nBreak", 1, "Second\nCell\nBroken", 3], + alignments=[Alignment.LEFT, Alignment.RIGHT, Alignment.CENTER, Alignment.LEFT, Alignment.CENTER], + ) + expected = ( + "╔═══════════════════════════════════════════╗\n" + "║ Multiline G Two R S ║\n" + "║ Header Lines ║\n" + "║ Cell ║\n" + "╟───────────────────────────────────────────╢\n" + "║ 1 Alpha 3 4 One ║\n" + "║ Beta Two ║\n" + "║ Gamma ║\n" + "╟───────────────────────────────────────────╢\n" + "║ A Footer 1 Second 3 ║\n" + "║ Break Cell ║\n" + "║ Broken ║\n" + "╚═══════════════════════════════════════════╝" + ) + assert text == expected diff --git a/tests/test_convert.py b/tests/test_convert.py index e549392..8223a2b 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -199,3 +199,27 @@ def test_numeric_data(): "╚════╩══════════════════════╝" ) assert text == expected + + +def test_multiline_cells(): + text = table = t2a( + header=["Multiline\nHeader\nCell", "G", "Two\nLines", "R", "S"], + body=[[1, "Alpha\nBeta\nGamma", 3, 4, "One\nTwo"]], + footer=["A", "Footer\nBreak", 1, "Second\nCell\nBroken", 3], + ) + expected = ( + "╔═══════════════════════════════════════════╗\n" + "║ Multiline G Two R S ║\n" + "║ Header Lines ║\n" + "║ Cell ║\n" + "╟───────────────────────────────────────────╢\n" + "║ 1 Alpha 3 4 One ║\n" + "║ Beta Two ║\n" + "║ Gamma ║\n" + "╟───────────────────────────────────────────╢\n" + "║ A Footer 1 Second 3 ║\n" + "║ Break Cell ║\n" + "║ Broken ║\n" + "╚═══════════════════════════════════════════╝" + ) + assert text == expected From e4898f4a83bc880844f8520d0ea17cd735ae835a Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Fri, 11 Feb 2022 14:52:44 -0700 Subject: [PATCH 2/6] refactor: linting, variables and comments --- table2ascii/table_to_ascii.py | 36 +++++++++++++++++++++-------------- tests/test_convert.py | 2 +- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/table2ascii/table_to_ascii.py b/table2ascii/table_to_ascii.py index 3c2ef62..bb38e36 100644 --- a/table2ascii/table_to_ascii.py +++ b/table2ascii/table_to_ascii.py @@ -90,9 +90,12 @@ def __auto_column_widths(self) -> List[int]: Returns: List[int]: The minimum number of characters needed for each column """ + + def longest_line(text) -> int: + """Returns the length of the longest line in a multi-line string""" + return max(len(line) for line in str(text).splitlines()) + column_widths = [] - # lambda function to get the length of the longest line in a multi-line string - longest_line = lambda text: max(len(line) for line in str(text).splitlines()) # get the width necessary for each column for i in range(self.__columns): # number of characters in column of i of header, each body row, and footer @@ -148,36 +151,41 @@ def __row_to_ascii( str: The line in the ascii table """ output = "" - # get number of lines for rendering row + # find the maximum number of lines a single cell in the column has num_lines = max(len(str(cell).splitlines()) for cell in filler) # add columns - for line in range(num_lines): + for line_index in range(num_lines): # left edge of the row output += left_edge - for col in range(self.__columns): + for col_index in range(self.__columns): # content between separators col_content = "" + # if filler is a separator character, repeat it for the full width of the column if isinstance(filler, str): - col_content = filler * self.__column_widths[col] + col_content = filler * self.__column_widths[col_index] + # otherwise, use the text from the corresponding column in the filler list else: - col_lines = str(filler[col]).splitlines() - if line < len(col_lines): - col_content = col_lines[line] + # get the text of the current line in the cell + # if there are fewer lines in the current cell than others, empty string is used + col_lines = str(filler[col_index]).splitlines() + if line_index < len(col_lines): + col_content = col_lines[line_index] + # pad the text to the width of the column using the alignment col_content = self.__pad( col_content, - self.__column_widths[col], - self.__alignments[col], + self.__column_widths[col_index], + self.__alignments[col_index], ) output += col_content # column seperator sep = column_seperator - if col == 0 and self.__first_col_heading: + if col_index == 0 and self.__first_col_heading: # use column heading if first column option is specified sep = heading_col_sep - elif col == self.__columns - 2 and self.__last_col_heading: + elif col_index == self.__columns - 2 and self.__last_col_heading: # use column heading if last column option is specified sep = heading_col_sep - elif col == self.__columns - 1: + elif col_index == self.__columns - 1: # replace last seperator with symbol for edge of the row sep = right_edge output += sep diff --git a/tests/test_convert.py b/tests/test_convert.py index 8223a2b..b0d71cc 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -202,7 +202,7 @@ def test_numeric_data(): def test_multiline_cells(): - text = table = t2a( + text = t2a( header=["Multiline\nHeader\nCell", "G", "Two\nLines", "R", "S"], body=[[1, "Alpha\nBeta\nGamma", 3, 4, "One\nTwo"]], footer=["A", "Footer\nBreak", 1, "Second\nCell\nBroken", 3], From 9c7db141c6ab30cfa8c8768939d699f571160f5f Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Fri, 11 Feb 2022 14:59:22 -0700 Subject: [PATCH 3/6] fix: keep blank lines in borderless table --- table2ascii/table_to_ascii.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/table2ascii/table_to_ascii.py b/table2ascii/table_to_ascii.py index bb38e36..adbd1ad 100644 --- a/table2ascii/table_to_ascii.py +++ b/table2ascii/table_to_ascii.py @@ -191,7 +191,7 @@ def __row_to_ascii( output += sep output += "\n" # don't use separation row if it's only space - if output.strip() == "": + if num_lines == 1 and output.strip() == "": output = "" return output From 9b2e61ec530c8bc6915650f15db36e20d7ef089f Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Fri, 11 Feb 2022 15:10:40 -0700 Subject: [PATCH 4/6] fix: allow only empty strings in row --- table2ascii/table_to_ascii.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/table2ascii/table_to_ascii.py b/table2ascii/table_to_ascii.py index adbd1ad..6e1512a 100644 --- a/table2ascii/table_to_ascii.py +++ b/table2ascii/table_to_ascii.py @@ -93,7 +93,7 @@ def __auto_column_widths(self) -> List[int]: def longest_line(text) -> int: """Returns the length of the longest line in a multi-line string""" - return max(len(line) for line in str(text).splitlines()) + return max(len(line) for line in str(text).splitlines()) if text else 0 column_widths = [] # get the width necessary for each column @@ -151,8 +151,8 @@ def __row_to_ascii( str: The line in the ascii table """ output = "" - # find the maximum number of lines a single cell in the column has - num_lines = max(len(str(cell).splitlines()) for cell in filler) + # find the maximum number of lines a single cell in the column has (minimum of 1) + num_lines = max(len(str(cell).splitlines()) for cell in filler) or 1 # add columns for line_index in range(num_lines): # left edge of the row @@ -191,7 +191,7 @@ def __row_to_ascii( output += sep output += "\n" # don't use separation row if it's only space - if num_lines == 1 and output.strip() == "": + if isinstance(filler, str) and output.strip() == "": output = "" return output From 6f8bc663f4b824bd9e42f63efed5eb161ab51cd4 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Fri, 11 Feb 2022 15:19:51 -0700 Subject: [PATCH 5/6] fix: width calculation for falsy values --- table2ascii/table_to_ascii.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/table2ascii/table_to_ascii.py b/table2ascii/table_to_ascii.py index 6e1512a..1a1a1b3 100644 --- a/table2ascii/table_to_ascii.py +++ b/table2ascii/table_to_ascii.py @@ -91,21 +91,21 @@ def __auto_column_widths(self) -> List[int]: List[int]: The minimum number of characters needed for each column """ - def longest_line(text) -> int: + def longest_line(text: str) -> int: """Returns the length of the longest line in a multi-line string""" - return max(len(line) for line in str(text).splitlines()) if text else 0 + return max(len(line) for line in text.splitlines()) if len(text) else 0 column_widths = [] # get the width necessary for each column for i in range(self.__columns): # number of characters in column of i of header, each body row, and footer - header_size = longest_line(self.__header[i]) if self.__header else 0 + header_size = longest_line(str(self.__header[i])) if self.__header else 0 body_size = ( - map(lambda row, i=i: longest_line(row[i]), self.__body) + map(lambda row, i=i: longest_line(str(row[i])), self.__body) if self.__body else [0] ) - footer_size = longest_line(self.__footer[i]) if self.__footer else 0 + footer_size = longest_line(str(self.__footer[i])) if self.__footer else 0 # get the max and add 2 for padding each side with a space column_widths.append(max(header_size, *body_size, footer_size) + 2) return column_widths From 2e1078d6e4f7df25295766f1245e0b7417655d62 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Fri, 11 Feb 2022 15:24:55 -0700 Subject: [PATCH 6/6] docs: add comment --- table2ascii/table_to_ascii.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/table2ascii/table_to_ascii.py b/table2ascii/table_to_ascii.py index 1a1a1b3..e2673a0 100644 --- a/table2ascii/table_to_ascii.py +++ b/table2ascii/table_to_ascii.py @@ -153,10 +153,11 @@ def __row_to_ascii( output = "" # find the maximum number of lines a single cell in the column has (minimum of 1) num_lines = max(len(str(cell).splitlines()) for cell in filler) or 1 - # add columns + # repeat for each line of text in the cell for line_index in range(num_lines): # left edge of the row output += left_edge + # add columns for col_index in range(self.__columns): # content between separators col_content = ""