diff --git a/README.md b/README.md index 695d242..4a61c08 100644 --- a/README.md +++ b/README.md @@ -36,14 +36,14 @@ output = table2ascii( print(output) """ -╔═════╦═══════════════════════╗ -║ # ║ G H R S ║ -╟─────╫───────────────────────╢ -║ 1 ║ 30 40 35 30 ║ -║ 2 ║ 30 40 35 30 ║ -╟─────╫───────────────────────╢ -║ SUM ║ 130 140 135 130 ║ -╚═════╩═══════════════════════╝ +╔═════════════════════════════╗ +║ # G H R S ║ +╟─────────────────────────────╢ +║ 1 30 40 35 30 ║ +║ 2 30 40 35 30 ║ +╟─────────────────────────────╢ +║ SUM 130 140 135 130 ║ +╚═════════════════════════════╝ """ ``` @@ -51,7 +51,8 @@ print(output) from table2ascii import table2ascii output = table2ascii( - body=[["Assignment", "30", "40", "35", "30"], ["Bonus", "10", "20", "5", "10"]] + body=[["Assignment", "30", "40", "35", "30"], ["Bonus", "10", "20", "5", "10"]], + first_col_heading=True, ) print(output) @@ -68,6 +69,11 @@ print(output) Soon table2ascii will support more options for customization. +| Option | Type | Default | Description | +| :-----------------: | :----: | :-----: | :--------------------------------------------------------------: | +| `first_col_heading` | `bool` | `False` | Whether to add a heading column seperator after the first column | +| `last_col_heading` | `bool` | `False` | Whether to add a heading column seperator before the last column | + ## 👨‍🎨 Use cases ### Discord messages and embeds diff --git a/table2ascii/__init__.py b/table2ascii/__init__.py index f4d2459..19c7a6d 100644 --- a/table2ascii/__init__.py +++ b/table2ascii/__init__.py @@ -1,9 +1,20 @@ -from typing import List, Optional, Union -from math import floor, ceil import enum +from dataclasses import dataclass +from math import ceil, floor +from typing import List, Optional, Union + + +@dataclass +class Options: + """Class for storing options that the user sets""" + first_col_heading: bool = False + last_col_heading: bool = False + + +class Alignment(enum.Enum): + """Enum for alignment types""" -class Align(enum.Enum): LEFT = 0 RIGHT = 1 CENTER = 2 @@ -17,6 +28,7 @@ def __init__( header: Optional[List], body: Optional[List[List]], footer: Optional[List], + options: Options, ): """Validate arguments and initialize fields""" # check if columns in header are different from footer @@ -36,6 +48,7 @@ def __init__( self.__header = header self.__body = body self.__footer = footer + self.__options = options self.__columns = self.__count_columns() self.__cell_widths = self.__get_column_widths() @@ -52,24 +65,24 @@ def __init__( self.__parts = { "top_left_corner": "╔", # A "top_and_bottom_edge": "═", # B - "first_col_top_tee": "╦", # C + "heading_col_top_tee": "╦", # C "top_tee": "═", # D "top_right_corner": "╗", # E "left_and_right_edge": "║", # F - "first_col_sep": "║", # G + "heading_col_sep": "║", # G "middle_edge": " ", # H "header_left_tee": "╟", # I "header_row_sep": "─", # J - "first_col_header_cross": "╫", # K + "heading_col_header_cross": "╫", # K "header_row_cross": "─", # L "header_right_tee": "╢", # M "footer_left_tee": "╟", # N "footer_row_sep": "─", # O - "first_col_footer_cross": "╫", # P + "heading_col_footer_cross": "╫", # P "footer_row_cross": "─", # Q "footer_right_tee": "╢", # R "bottom_left_corner": "╚", # S - "first_col_bottom_tee": "╩", # T + "heading_col_bottom_tee": "╩", # T "bottom_tee": "═", # U "bottom_right_corner": "╝", # V } @@ -102,17 +115,17 @@ def __get_column_widths(self) -> List[int]: col_counts.append(max(header_size, *body_size, footer_size) + 2) return col_counts - def __pad(self, text: str, width: int, alignment: Align = Align.CENTER): + def __pad(self, text: str, width: int, alignment: Alignment = Alignment.CENTER): """Pad a string of text to a given width with specified alignment""" - if alignment == Align.LEFT: + if alignment == Alignment.LEFT: # pad with spaces on the end return f" {text} " + (" " * (width - len(text) - 2)) - if alignment == Align.CENTER: + if alignment == Alignment.CENTER: # pad with spaces, half on each side before = " " * floor((width - len(text) - 2) / 2) after = " " * ceil((width - len(text) - 2) / 2) return before + f" {text} " + after - if alignment == Align.RIGHT: + if alignment == Alignment.RIGHT: # pad with spaces at the beginning return (" " * (width - len(text) - 2)) + f" {text} " raise ValueError(f"The value '{alignment}' is not valid for alignment.") @@ -120,26 +133,18 @@ def __pad(self, text: str, width: int, alignment: Align = Align.CENTER): def __row_to_ascii( self, left_edge: str, - first_col_sep: str, + heading_col_sep: str, column_seperator: str, right_edge: str, filler: Union[str, List], ) -> str: """Assembles a row of the ascii table""" + first_heading = self.__options.first_col_heading + last_heading = self.__options.last_col_heading # left edge of the row output = left_edge - # content across the first column - output += ( - # edge or row separator if filler is a specific character - filler * self.__cell_widths[0] - if isinstance(filler, str) - # otherwise, use the first column's content - else self.__pad(str(filler[0]), self.__cell_widths[0]) - ) - # separation of first column from the rest of the table - output += first_col_sep - # add remaining columns - for i in range(1, self.__columns): + # add columns + for i in range(self.__columns): # content between separators output += ( # edge or row separator if filler is a specific character @@ -148,17 +153,22 @@ def __row_to_ascii( # otherwise, use the column content else self.__pad(str(filler[i]), self.__cell_widths[i]) ) - # add a separator - output += column_seperator - # replace last seperator with symbol for edge of the row - output = output[0:-1] + right_edge + # column seperator + sep = column_seperator + if (i == 0 and first_heading) or (i == self.__columns - 2 and last_heading): + # use column heading if 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 return output + "\n" def __top_edge_to_ascii(self) -> str: """Assembles the top edge of the ascii table""" return self.__row_to_ascii( left_edge=self.__parts["top_left_corner"], - first_col_sep=self.__parts["first_col_top_tee"], + heading_col_sep=self.__parts["heading_col_top_tee"], column_seperator=self.__parts["top_tee"], right_edge=self.__parts["top_right_corner"], filler=self.__parts["top_and_bottom_edge"], @@ -168,7 +178,7 @@ def __bottom_edge_to_ascii(self) -> str: """Assembles the top edge of the ascii table""" return self.__row_to_ascii( left_edge=self.__parts["bottom_left_corner"], - first_col_sep=self.__parts["first_col_bottom_tee"], + heading_col_sep=self.__parts["heading_col_bottom_tee"], column_seperator=self.__parts["bottom_tee"], right_edge=self.__parts["bottom_right_corner"], filler=self.__parts["top_and_bottom_edge"], @@ -178,7 +188,7 @@ def __header_row_to_ascii(self) -> str: """Assembles the header row line of the ascii table""" return self.__row_to_ascii( left_edge=self.__parts["left_and_right_edge"], - first_col_sep=self.__parts["first_col_sep"], + heading_col_sep=self.__parts["heading_col_sep"], column_seperator=self.__parts["middle_edge"], right_edge=self.__parts["left_and_right_edge"], filler=self.__header, @@ -188,7 +198,7 @@ def __footer_row_to_ascii(self) -> str: """Assembles the header row line of the ascii table""" return self.__row_to_ascii( left_edge=self.__parts["left_and_right_edge"], - first_col_sep=self.__parts["first_col_sep"], + heading_col_sep=self.__parts["heading_col_sep"], column_seperator=self.__parts["middle_edge"], right_edge=self.__parts["left_and_right_edge"], filler=self.__footer, @@ -198,7 +208,7 @@ def __header_sep_to_ascii(self) -> str: """Assembles the seperator below the header of the ascii table""" return self.__row_to_ascii( left_edge=self.__parts["header_left_tee"], - first_col_sep=self.__parts["first_col_header_cross"], + heading_col_sep=self.__parts["heading_col_header_cross"], column_seperator=self.__parts["header_row_cross"], right_edge=self.__parts["header_right_tee"], filler=self.__parts["header_row_sep"], @@ -208,7 +218,7 @@ def __footer_sep_to_ascii(self) -> str: """Assembles the seperator below the header of the ascii table""" return self.__row_to_ascii( left_edge=self.__parts["footer_left_tee"], - first_col_sep=self.__parts["first_col_footer_cross"], + heading_col_sep=self.__parts["heading_col_footer_cross"], column_seperator=self.__parts["footer_row_cross"], right_edge=self.__parts["footer_right_tee"], filler=self.__parts["footer_row_sep"], @@ -219,7 +229,7 @@ def __body_to_ascii(self) -> str: for row in self.__body: output += self.__row_to_ascii( left_edge=self.__parts["left_and_right_edge"], - first_col_sep=self.__parts["first_col_sep"], + heading_col_sep=self.__parts["heading_col_sep"], column_seperator=self.__parts["middle_edge"], right_edge=self.__parts["left_and_right_edge"], filler=row, @@ -250,6 +260,7 @@ def table2ascii( header: Optional[List] = None, body: Optional[List[List]] = None, footer: Optional[List] = None, + **options, ) -> str: """Convert a 2D Python table to ASCII text @@ -258,4 +269,4 @@ def table2ascii( :param body: :class:`Optional[List[List]]` 2-dimensional list of values in the table's body :param footer: :class:`Optional[List]` List of column values in the table's footer row """ - return TableToAscii(header, body, footer).to_ascii() + return TableToAscii(header, body, footer, Options(**options)).to_ascii() diff --git a/tests/test_convert.py b/tests/test_convert.py index caa4ac3..b731988 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -8,6 +8,7 @@ def test_header_body_footer(): header=["#", "G", "H", "R", "S"], body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]], footer=["SUM", "130", "140", "135", "130"], + first_col_heading=True, ) expected = ( "╔═════╦═══════════════════════╗\n" @@ -26,6 +27,7 @@ def test_body_footer(): text = t2a( body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]], footer=["SUM", "130", "140", "135", "130"], + first_col_heading=True, ) expected = ( "╔═════╦═══════════════════════╗\n" @@ -42,6 +44,7 @@ def test_header_body(): text = t2a( header=["#", "G", "H", "R", "S"], body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]], + first_col_heading=True, ) expected = ( "╔═══╦═══════════════════╗\n" @@ -58,6 +61,7 @@ def test_header_footer(): text = t2a( header=["#", "G", "H", "R", "S"], footer=["SUM", "130", "140", "135", "130"], + first_col_heading=True, ) expected = ( "╔═════╦═══════════════════════╗\n" @@ -73,6 +77,7 @@ def test_header_footer(): def test_header(): text = t2a( header=["#", "G", "H", "R", "S"], + first_col_heading=True, ) expected = ( "╔═══╦═══════════════╗\n" @@ -86,6 +91,7 @@ def test_header(): def test_body(): text = t2a( body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]], + first_col_heading=True, ) expected = ( "╔═══╦═══════════════════╗\n" @@ -99,6 +105,7 @@ def test_body(): def test_footer(): text = t2a( footer=["SUM", "130", "140", "135", "130"], + first_col_heading=True, ) expected = ( "╔═════╦═══════════════════════╗\n" @@ -114,6 +121,7 @@ def test_header_footer_unequal(): t2a( header=["H", "R", "S"], footer=["SUM", "130", "140", "135", "130"], + first_col_heading=True, ) @@ -126,6 +134,7 @@ def test_header_body_unequal(): ["1", "30", "40", "35", "30", "36"], ["2", "30", "40", "35", "30"], ], + first_col_heading=True, ) @@ -138,6 +147,7 @@ def test_footer_body_unequal(): ["2", "30", "40", "35", "30"], ], footer=["SUM", "130", "140", "135", "130", "36"], + first_col_heading=True, ) @@ -145,6 +155,7 @@ def test_empty_header(): text = t2a( header=[], body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]], + first_col_heading=True, ) expected = ( "╔═══╦═══════════════════╗\n" @@ -156,7 +167,11 @@ def test_empty_header(): def test_empty_body(): - text = t2a(header=["#", "G", "H", "R", "S"], body=[]) + text = t2a( + header=["#", "G", "H", "R", "S"], + body=[], + first_col_heading=True, + ) expected = ( "╔═══╦═══════════════╗\n" "║ # ║ G H R S ║\n" diff --git a/tests/test_heading.py b/tests/test_heading.py deleted file mode 100644 index caa4ac3..0000000 --- a/tests/test_heading.py +++ /dev/null @@ -1,166 +0,0 @@ -from table2ascii import table2ascii as t2a - -import pytest - - -def test_header_body_footer(): - text = t2a( - header=["#", "G", "H", "R", "S"], - body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]], - footer=["SUM", "130", "140", "135", "130"], - ) - expected = ( - "╔═════╦═══════════════════════╗\n" - "║ # ║ G H R S ║\n" - "╟─────╫───────────────────────╢\n" - "║ 1 ║ 30 40 35 30 ║\n" - "║ 2 ║ 30 40 35 30 ║\n" - "╟─────╫───────────────────────╢\n" - "║ SUM ║ 130 140 135 130 ║\n" - "╚═════╩═══════════════════════╝\n" - ) - assert text == expected - - -def test_body_footer(): - text = t2a( - body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]], - footer=["SUM", "130", "140", "135", "130"], - ) - expected = ( - "╔═════╦═══════════════════════╗\n" - "║ 1 ║ 30 40 35 30 ║\n" - "║ 2 ║ 30 40 35 30 ║\n" - "╟─────╫───────────────────────╢\n" - "║ SUM ║ 130 140 135 130 ║\n" - "╚═════╩═══════════════════════╝\n" - ) - assert text == expected - - -def test_header_body(): - text = t2a( - header=["#", "G", "H", "R", "S"], - body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]], - ) - expected = ( - "╔═══╦═══════════════════╗\n" - "║ # ║ G H R S ║\n" - "╟───╫───────────────────╢\n" - "║ 1 ║ 30 40 35 30 ║\n" - "║ 2 ║ 30 40 35 30 ║\n" - "╚═══╩═══════════════════╝\n" - ) - assert text == expected - - -def test_header_footer(): - text = t2a( - header=["#", "G", "H", "R", "S"], - footer=["SUM", "130", "140", "135", "130"], - ) - expected = ( - "╔═════╦═══════════════════════╗\n" - "║ # ║ G H R S ║\n" - "╟─────╫───────────────────────╢\n" - "╟─────╫───────────────────────╢\n" - "║ SUM ║ 130 140 135 130 ║\n" - "╚═════╩═══════════════════════╝\n" - ) - assert text == expected - - -def test_header(): - text = t2a( - header=["#", "G", "H", "R", "S"], - ) - expected = ( - "╔═══╦═══════════════╗\n" - "║ # ║ G H R S ║\n" - "╟───╫───────────────╢\n" - "╚═══╩═══════════════╝\n" - ) - assert text == expected - - -def test_body(): - text = t2a( - body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]], - ) - expected = ( - "╔═══╦═══════════════════╗\n" - "║ 1 ║ 30 40 35 30 ║\n" - "║ 2 ║ 30 40 35 30 ║\n" - "╚═══╩═══════════════════╝\n" - ) - assert text == expected - - -def test_footer(): - text = t2a( - footer=["SUM", "130", "140", "135", "130"], - ) - expected = ( - "╔═════╦═══════════════════════╗\n" - "╟─────╫───────────────────────╢\n" - "║ SUM ║ 130 140 135 130 ║\n" - "╚═════╩═══════════════════════╝\n" - ) - assert text == expected - - -def test_header_footer_unequal(): - with pytest.raises(ValueError): - t2a( - header=["H", "R", "S"], - footer=["SUM", "130", "140", "135", "130"], - ) - - -def test_header_body_unequal(): - with pytest.raises(ValueError): - t2a( - header=["#", "G", "H", "R", "S"], - body=[ - ["0", "45", "30", "32", "28"], - ["1", "30", "40", "35", "30", "36"], - ["2", "30", "40", "35", "30"], - ], - ) - - -def test_footer_body_unequal(): - with pytest.raises(ValueError): - t2a( - body=[ - ["0", "45", "30", "32", "28"], - ["1", "30", "40", "35", "30"], - ["2", "30", "40", "35", "30"], - ], - footer=["SUM", "130", "140", "135", "130", "36"], - ) - - -def test_empty_header(): - text = t2a( - header=[], - body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]], - ) - expected = ( - "╔═══╦═══════════════════╗\n" - "║ 1 ║ 30 40 35 30 ║\n" - "║ 2 ║ 30 40 35 30 ║\n" - "╚═══╩═══════════════════╝\n" - ) - assert text == expected - - -def test_empty_body(): - text = t2a(header=["#", "G", "H", "R", "S"], body=[]) - expected = ( - "╔═══╦═══════════════╗\n" - "║ # ║ G H R S ║\n" - "╟───╫───────────────╢\n" - "╚═══╩═══════════════╝\n" - ) - assert text == expected diff --git a/tests/test_heading_cols.py b/tests/test_heading_cols.py new file mode 100644 index 0000000..451dccb --- /dev/null +++ b/tests/test_heading_cols.py @@ -0,0 +1,87 @@ +from table2ascii import table2ascii as t2a + +import pytest + + +def test_first_column_heading(): + text = t2a( + header=["#", "G", "H", "R", "S"], + body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]], + footer=["SUM", "130", "140", "135", "130"], + first_col_heading=True, + last_col_heading=False, + ) + expected = ( + "╔═════╦═══════════════════════╗\n" + "║ # ║ G H R S ║\n" + "╟─────╫───────────────────────╢\n" + "║ 1 ║ 30 40 35 30 ║\n" + "║ 2 ║ 30 40 35 30 ║\n" + "╟─────╫───────────────────────╢\n" + "║ SUM ║ 130 140 135 130 ║\n" + "╚═════╩═══════════════════════╝\n" + ) + assert text == expected + + +def test_last_column_heading(): + text = t2a( + header=["#", "G", "H", "R", "S"], + body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]], + footer=["SUM", "130", "140", "135", "130"], + first_col_heading=False, + last_col_heading=True, + ) + expected = ( + "╔═══════════════════════╦═════╗\n" + "║ # G H R ║ S ║\n" + "╟───────────────────────╫─────╢\n" + "║ 1 30 40 35 ║ 30 ║\n" + "║ 2 30 40 35 ║ 30 ║\n" + "╟───────────────────────╫─────╢\n" + "║ SUM 130 140 135 ║ 130 ║\n" + "╚═══════════════════════╩═════╝\n" + ) + assert text == expected + + +def test_both_column_heading(): + text = t2a( + header=["#", "G", "H", "R", "S"], + body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]], + footer=["SUM", "130", "140", "135", "130"], + first_col_heading=True, + last_col_heading=True, + ) + expected = ( + "╔═════╦═════════════════╦═════╗\n" + "║ # ║ G H R ║ S ║\n" + "╟─────╫─────────────────╫─────╢\n" + "║ 1 ║ 30 40 35 ║ 30 ║\n" + "║ 2 ║ 30 40 35 ║ 30 ║\n" + "╟─────╫─────────────────╫─────╢\n" + "║ SUM ║ 130 140 135 ║ 130 ║\n" + "╚═════╩═════════════════╩═════╝\n" + ) + assert text == expected + + +def test_neither_column_heading(): + text = t2a( + header=["#", "G", "H", "R", "S"], + body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]], + footer=["SUM", "130", "140", "135", "130"], + first_col_heading=False, + last_col_heading=False, + ) + expected = ( + "╔═════════════════════════════╗\n" + "║ # G H R S ║\n" + "╟─────────────────────────────╢\n" + "║ 1 30 40 35 30 ║\n" + "║ 2 30 40 35 30 ║\n" + "╟─────────────────────────────╢\n" + "║ SUM 130 140 135 130 ║\n" + "╚═════════════════════════════╝\n" + ) + assert text == expected