From 9c91e5c5d6fb380df36ff1deb1c1d514e250b267 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Wed, 28 Dec 2022 00:53:51 -0700 Subject: [PATCH 1/6] feat: Added decimal alignment option --- table2ascii/alignment.py | 9 ++--- table2ascii/table_to_ascii.py | 66 ++++++++++++++++++++++++++++++----- tests/test_alignments.py | 25 ++++++++++++- 3 files changed, 86 insertions(+), 14 deletions(-) diff --git a/table2ascii/alignment.py b/table2ascii/alignment.py index 0a8e5f7..4c6ece9 100644 --- a/table2ascii/alignment.py +++ b/table2ascii/alignment.py @@ -15,16 +15,16 @@ class Alignment(IntEnum): ["Cheese", "Dairy", "$10.99", "No"], ["Apples", "Produce", "$0.99", "Yes"], ], - alignments=[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT, Alignment.LEFT], + alignments=[Alignment.LEFT, Alignment.CENTER, Alignment.DECIMAL, Alignment.RIGHT], ) \"\"\" ╔════════════════════════════════════════╗ ║ Product Category Price In Stock ║ ╟────────────────────────────────────────╢ - ║ Milk Dairy $2.99 Yes ║ - ║ Cheese Dairy $10.99 No ║ - ║ Apples Produce $0.99 Yes ║ + ║ Milk Dairy $2.99 Yes ║ + ║ Cheese Dairy $10.99 No ║ + ║ Apples Produce $0.99 Yes ║ ╚════════════════════════════════════════╝ \"\"\" """ @@ -32,3 +32,4 @@ class Alignment(IntEnum): LEFT = 0 CENTER = 1 RIGHT = 2 + DECIMAL = 3 diff --git a/table2ascii/table_to_ascii.py b/table2ascii/table_to_ascii.py index 8116467..0031a08 100644 --- a/table2ascii/table_to_ascii.py +++ b/table2ascii/table_to_ascii.py @@ -67,15 +67,19 @@ def __init__( if not header and not body and not footer: raise NoHeaderBodyOrFooterError() - # calculate or use given column widths - self.__column_widths = self.__calculate_column_widths(options.column_widths) - self.__alignments = options.alignments or [Alignment.CENTER] * self.__columns # check if alignments specified have a different number of columns if options.alignments and len(options.alignments) != self.__columns: raise AlignmentCountMismatchError(options.alignments, self.__columns) + # keep track of the positions of the decimal points for decimal alignment + # this will be calculated and set when __calculate_column_widths() is called + self.__decimal_positions = [0] * self.__columns + + # calculate or use given column widths + self.__column_widths = self.__calculate_column_widths(options.column_widths) + # check if the cell padding is valid if self.__cell_padding < 0: raise InvalidCellPaddingError(self.__cell_padding) @@ -125,8 +129,30 @@ def get_column_width(row: Sequence[SupportsStr], column: int) -> int: header_size = get_column_width(self.__header, i) if self.__header else 0 body_size = max(get_column_width(row, i) for row in self.__body) if self.__body else 0 footer_size = get_column_width(self.__footer, i) if self.__footer else 0 + min_text_width = max(header_size, body_size, footer_size) + # if any columns have Alignment.DECIMAL, calculate based on text before and after the decimal point + if self.__alignments[i] == Alignment.DECIMAL: + # values in the i-th column of header, body, and footer + values = [str(self.__header[i])] if self.__header else [] + values += [str(row[i]) for row in self.__body] if self.__body else [] + values += [str(self.__footer[i])] if self.__footer else [] + # filter out values that are not numbers and split at the decimal point + split_values = [ + self.__split_decimal(value) for value in values if self.__is_number(value) + ] + # get the max digits before and after the decimal point, skip if there are no decimal values + if len(split_values) > 0: + max_before_decimal = max(self.__str_width(parts[0]) for parts in split_values) + max_after_decimal = max(self.__str_width(parts[1]) for parts in split_values) + # add 1 for the decimal point if there are any decimal point values + has_decimal = any(value.count(".") == 1 for value in values) + decimal_size = max_before_decimal + max_after_decimal + int(has_decimal) + # store the max digits before the decimal point for decimal alignment + self.__decimal_positions[i] = max_before_decimal + # update the min text width if the decimal size is larger + min_text_width = max(min_text_width, decimal_size) # get the max and add 2 for padding each side with a space depending on cell padding - column_widths.append(max(header_size, body_size, footer_size) + self.__cell_padding * 2) + column_widths.append(min_text_width + self.__cell_padding * 2) return column_widths def __calculate_column_widths( @@ -169,32 +195,44 @@ def __fix_rows_beginning_with_merge(self) -> None: if self.__footer and self.__footer[0] == Merge.LEFT: self.__footer[0] = "" - def __pad(self, cell_value: SupportsStr, width: int, alignment: Alignment) -> str: + def __pad(self, cell_value: SupportsStr, width: int, col_index: int) -> str: """Pad a string of text to a given width with specified alignment Args: cell_value: The text in the cell to pad width: The width in characters to pad to - alignment: The alignment to use + col_index: The index of the column Returns: The padded text """ + alignment = self.__alignments[col_index] text = str(cell_value) padding = " " * self.__cell_padding padded_text = f"{padding}{text}{padding}" text_width = self.__str_width(padded_text) + # change alignment if decimal alignment is specified and the cell is not a number + if alignment == Alignment.DECIMAL and not self.__is_number(text): + alignment = Alignment.CENTER + # pad the text based on the alignment if alignment == Alignment.LEFT: # pad with spaces on the end return padded_text + (" " * (width - text_width)) - if alignment == Alignment.CENTER: + elif alignment == Alignment.CENTER: # pad with spaces, half on each side before = " " * floor((width - text_width) / 2) after = " " * ceil((width - text_width) / 2) return before + padded_text + after - if alignment == Alignment.RIGHT: + elif alignment == Alignment.RIGHT: # pad with spaces at the beginning return (" " * (width - text_width)) + padded_text + elif alignment == Alignment.DECIMAL: + # pad such that the decimal point is aligned to the column's decimal position + decimal_position = self.__decimal_positions[col_index] + text_before_decimal = self.__split_decimal(text)[0] + before = " " * (decimal_position - self.__str_width(text_before_decimal)) + after = " " * (width - text_width - len(before)) + return before + padded_text + after raise InvalidAlignmentError(alignment) def __wrap_long_lines_in_merged_cells( @@ -403,7 +441,7 @@ def __get_padded_cell_line_content( return self.__pad( cell_value=col_content, width=pad_width, - alignment=self.__alignments[col_index], + col_index=col_index, ) def __top_edge_to_ascii(self) -> str: @@ -530,6 +568,16 @@ def __str_width(self, text: str) -> int: # if use_wcwidth is False or wcswidth fails, fall back to len return width if width >= 0 else len(text) + @staticmethod + def __is_number(text: str) -> bool: + """Returns True if the string is a number, with or without a decimal point""" + return text.replace(".", "", 1).isdecimal() + + @staticmethod + def __split_decimal(text: str) -> tuple[str, str]: + """Splits a string into a tuple of the integer and decimal parts""" + return tuple(text.split(".", 1)) if "." in text else (text, "") + def to_ascii(self) -> str: """Generates a formatted ASCII table diff --git a/tests/test_alignments.py b/tests/test_alignments.py index 3e8b874..c1028c1 100644 --- a/tests/test_alignments.py +++ b/tests/test_alignments.py @@ -1,6 +1,6 @@ import pytest -from table2ascii import Alignment, table2ascii as t2a +from table2ascii import Alignment, PresetStyle, table2ascii as t2a from table2ascii.exceptions import AlignmentCountMismatchError, InvalidAlignmentError @@ -97,3 +97,26 @@ def test_alignments_multiline_data(): "╚═══════════════════════════════════════════╝" ) assert text == expected + + +def test_decimal_alignment(): + text = t2a( + header=["1.1.1", "G", "H", "R", "3.8"], + body=[[100.00001, 2, 33, "AB", "1.5"], [10.0001, 22.0, 3, "CD", "3.03"]], + footer=[10000.01, "123", 1, "E", "A"], + alignments=[Alignment.DECIMAL] * 5, + first_col_heading=True, + style=PresetStyle.double_thin_box, + ) + expected = ( + "╔═════════════╦═══════╤════╤════╤═════════╗\n" + "║ 1.1.1 ║ G │ H │ R │ 3.8 ║\n" + "╠═════════════╬═══════╪════╪════╪═════════╣\n" + "║ 100.00001 ║ 2 │ 33 │ AB │ 1.5 ║\n" + "╟─────────────╫───────┼────┼────┼─────────╢\n" + "║ 10.0001 ║ 22.0 │ 3 │ CD │ 3.03 ║\n" + "╠═════════════╬═══════╪════╪════╪═════════╣\n" + "║ 10000.01 ║ 123 │ 1 │ E │ A ║\n" + "╚═════════════╩═══════╧════╧════╧═════════╝" + ) + assert text == expected From a249af18685107a239bcc4d1633a183bc3282a74 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Wed, 28 Dec 2022 01:48:05 -0700 Subject: [PATCH 2/6] center align decimals, update docs --- table2ascii/alignment.py | 27 +++++++---- table2ascii/table_to_ascii.py | 87 ++++++++++++++++++++--------------- tests/test_alignments.py | 26 +++++------ 3 files changed, 82 insertions(+), 58 deletions(-) diff --git a/table2ascii/alignment.py b/table2ascii/alignment.py index 4c6ece9..b298e5c 100644 --- a/table2ascii/alignment.py +++ b/table2ascii/alignment.py @@ -9,24 +9,33 @@ class Alignment(IntEnum): from table2ascii import Alignment, table2ascii table2ascii( - header=["Product", "Category", "Price", "In Stock"], + header=["Product", "Category", "Price", "Rating"], body=[ - ["Milk", "Dairy", "$2.99", "Yes"], - ["Cheese", "Dairy", "$10.99", "No"], - ["Apples", "Produce", "$0.99", "Yes"], + ["Milk", "Dairy", "$2.99", "6.28318"], + ["Cheese", "Dairy", "$10.99", "8.2"], + ["Apples", "Produce", "$0.99", "10.00"], ], - alignments=[Alignment.LEFT, Alignment.CENTER, Alignment.DECIMAL, Alignment.RIGHT], + alignments=[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT, Alignment.DECIMAL], ) \"\"\" ╔════════════════════════════════════════╗ - ║ Product Category Price In Stock ║ + ║ Product Category Price Rating ║ ╟────────────────────────────────────────╢ - ║ Milk Dairy $2.99 Yes ║ - ║ Cheese Dairy $10.99 No ║ - ║ Apples Produce $0.99 Yes ║ + ║ Milk Dairy $2.99 6.28318 ║ + ║ Cheese Dairy $10.99 8.2 ║ + ║ Apples Produce $0.99 10.00 ║ ╚════════════════════════════════════════╝ \"\"\" + + .. note:: + + If the :attr:`DECIMAL` alignment type is used, any cell values that are + not valid decimal numbers will be aligned to the center. + + .. versionchanged:: 1.1.0 + + Added :attr:`DECIMAL` alignment type """ LEFT = 0 diff --git a/table2ascii/table_to_ascii.py b/table2ascii/table_to_ascii.py index 0031a08..abdbed0 100644 --- a/table2ascii/table_to_ascii.py +++ b/table2ascii/table_to_ascii.py @@ -73,9 +73,10 @@ def __init__( if options.alignments and len(options.alignments) != self.__columns: raise AlignmentCountMismatchError(options.alignments, self.__columns) - # keep track of the positions of the decimal points for decimal alignment - # this will be calculated and set when __calculate_column_widths() is called - self.__decimal_positions = [0] * self.__columns + # keep track of the number widths and positions of the decimal points for decimal alignment + decimal_widths, decimal_positions = self.__calculate_decimal_widths_and_positions() + self.__decimal_widths: list[int] = decimal_widths + self.__decimal_positions: list[int] = decimal_positions # calculate or use given column widths self.__column_widths = self.__calculate_column_widths(options.column_widths) @@ -129,32 +130,46 @@ def get_column_width(row: Sequence[SupportsStr], column: int) -> int: header_size = get_column_width(self.__header, i) if self.__header else 0 body_size = max(get_column_width(row, i) for row in self.__body) if self.__body else 0 footer_size = get_column_width(self.__footer, i) if self.__footer else 0 - min_text_width = max(header_size, body_size, footer_size) - # if any columns have Alignment.DECIMAL, calculate based on text before and after the decimal point - if self.__alignments[i] == Alignment.DECIMAL: - # values in the i-th column of header, body, and footer - values = [str(self.__header[i])] if self.__header else [] - values += [str(row[i]) for row in self.__body] if self.__body else [] - values += [str(self.__footer[i])] if self.__footer else [] - # filter out values that are not numbers and split at the decimal point - split_values = [ - self.__split_decimal(value) for value in values if self.__is_number(value) - ] - # get the max digits before and after the decimal point, skip if there are no decimal values - if len(split_values) > 0: - max_before_decimal = max(self.__str_width(parts[0]) for parts in split_values) - max_after_decimal = max(self.__str_width(parts[1]) for parts in split_values) - # add 1 for the decimal point if there are any decimal point values - has_decimal = any(value.count(".") == 1 for value in values) - decimal_size = max_before_decimal + max_after_decimal + int(has_decimal) - # store the max digits before the decimal point for decimal alignment - self.__decimal_positions[i] = max_before_decimal - # update the min text width if the decimal size is larger - min_text_width = max(min_text_width, decimal_size) + min_text_width = max(header_size, body_size, footer_size, self.__decimal_widths[i]) # get the max and add 2 for padding each side with a space depending on cell padding column_widths.append(min_text_width + self.__cell_padding * 2) return column_widths + def __calculate_decimal_widths_and_positions(self) -> tuple[list[int], list[int]]: + """Calculate the positions of the decimal points for decimal alignment. + + Returns: + A tuple of the widths of the decimal numbers in each column + and the positions of the decimal points in each column + """ + decimal_widths: list[int] = [0] * self.__columns + decimal_positions: list[int] = [0] * self.__columns + for i in range(self.__columns): + if self.__alignments[i] != Alignment.DECIMAL: + continue + # list all values in the i-th column of header, body, and footer + values = [str(self.__header[i])] if self.__header else [] + values += [str(row[i]) for row in self.__body] if self.__body else [] + values += [str(self.__footer[i])] if self.__footer else [] + # filter out values that are not numbers and split at the decimal point + split_values = [ + self.__split_decimal(value) for value in values if self.__is_number(value) + ] + # skip if there are no decimal values + if len(split_values) == 0: + continue + # get the max number of digits before and after the decimal point + max_before_decimal = max(self.__str_width(parts[0]) for parts in split_values) + max_after_decimal = max(self.__str_width(parts[1]) for parts in split_values) + # add 1 for the decimal point if there are any decimal point values + has_decimal = any(self.__is_number(value) and "." in value for value in values) + decimal_size = max_before_decimal + max_after_decimal + int(has_decimal) + # store the max digits before the decimal point for decimal alignment + decimal_positions[i] = max_before_decimal + # store the total width of the decimal numbers in the column + decimal_widths[i] = decimal_size + return decimal_widths, decimal_positions + def __calculate_column_widths( self, user_column_widths: Sequence[int | None] | None ) -> list[int]: @@ -208,17 +223,24 @@ def __pad(self, cell_value: SupportsStr, width: int, col_index: int) -> str: """ alignment = self.__alignments[col_index] text = str(cell_value) + # if using decimal alignment, pad such that the decimal point + # is aligned to the column's decimal position + if alignment == Alignment.DECIMAL and self.__is_number(text): + decimal_position = self.__decimal_positions[col_index] + decimal_max_width = self.__decimal_widths[col_index] + text_before_decimal = self.__split_decimal(text)[0] + before = " " * (decimal_position - self.__str_width(text_before_decimal)) + after = " " * (decimal_max_width - self.__str_width(text) - len(before)) + text = f"{before}{text}{after}" + # add minimum cell padding around the text padding = " " * self.__cell_padding padded_text = f"{padding}{text}{padding}" text_width = self.__str_width(padded_text) - # change alignment if decimal alignment is specified and the cell is not a number - if alignment == Alignment.DECIMAL and not self.__is_number(text): - alignment = Alignment.CENTER # pad the text based on the alignment if alignment == Alignment.LEFT: # pad with spaces on the end return padded_text + (" " * (width - text_width)) - elif alignment == Alignment.CENTER: + elif alignment in (Alignment.CENTER, Alignment.DECIMAL): # pad with spaces, half on each side before = " " * floor((width - text_width) / 2) after = " " * ceil((width - text_width) / 2) @@ -226,13 +248,6 @@ def __pad(self, cell_value: SupportsStr, width: int, col_index: int) -> str: elif alignment == Alignment.RIGHT: # pad with spaces at the beginning return (" " * (width - text_width)) + padded_text - elif alignment == Alignment.DECIMAL: - # pad such that the decimal point is aligned to the column's decimal position - decimal_position = self.__decimal_positions[col_index] - text_before_decimal = self.__split_decimal(text)[0] - before = " " * (decimal_position - self.__str_width(text_before_decimal)) - after = " " * (width - text_width - len(before)) - return before + padded_text + after raise InvalidAlignmentError(alignment) def __wrap_long_lines_in_merged_cells( diff --git a/tests/test_alignments.py b/tests/test_alignments.py index c1028c1..bbd7104 100644 --- a/tests/test_alignments.py +++ b/tests/test_alignments.py @@ -101,22 +101,22 @@ def test_alignments_multiline_data(): def test_decimal_alignment(): text = t2a( - header=["1.1.1", "G", "H", "R", "3.8"], - body=[[100.00001, 2, 33, "AB", "1.5"], [10.0001, 22.0, 3, "CD", "3.03"]], - footer=[10000.01, "123", 1, "E", "A"], - alignments=[Alignment.DECIMAL] * 5, + header=["1.1.1", "G", "Long Header", "H", "R", "3.8"], + body=[[100.00001, 2, 3.14, 33, "AB", "1.5"], [10.0001, 22.0, 2.718, 3, "CD", "3.03"]], + footer=[10000.01, "123", 10.0, 1, "E", "A"], + alignments=[Alignment.DECIMAL] * 6, first_col_heading=True, style=PresetStyle.double_thin_box, ) expected = ( - "╔═════════════╦═══════╤════╤════╤═════════╗\n" - "║ 1.1.1 ║ G │ H │ R │ 3.8 ║\n" - "╠═════════════╬═══════╪════╪════╪═════════╣\n" - "║ 100.00001 ║ 2 │ 33 │ AB │ 1.5 ║\n" - "╟─────────────╫───────┼────┼────┼─────────╢\n" - "║ 10.0001 ║ 22.0 │ 3 │ CD │ 3.03 ║\n" - "╠═════════════╬═══════╪════╪════╪═════════╣\n" - "║ 10000.01 ║ 123 │ 1 │ E │ A ║\n" - "╚═════════════╩═══════╧════╧════╧═════════╝" + "╔═════════════╦═══════╤═════════════╤════╤════╤═════════╗\n" + "║ 1.1.1 ║ G │ Long Header │ H │ R │ 3.8 ║\n" + "╠═════════════╬═══════╪═════════════╪════╪════╪═════════╣\n" + "║ 100.00001 ║ 2 │ 3.14 │ 33 │ AB │ 1.5 ║\n" + "╟─────────────╫───────┼─────────────┼────┼────┼─────────╢\n" + "║ 10.0001 ║ 22.0 │ 2.718 │ 3 │ CD │ 3.03 ║\n" + "╠═════════════╬═══════╪═════════════╪════╪════╪═════════╣\n" + "║ 10000.01 ║ 123 │ 10.0 │ 1 │ E │ A ║\n" + "╚═════════════╩═══════╧═════════════╧════╧════╧═════════╝" ) assert text == expected From d61fdb69b690a4b5ff5adf5a677e45c412578d0e Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Wed, 28 Dec 2022 01:52:21 -0700 Subject: [PATCH 3/6] Change for mypy --- table2ascii/table_to_ascii.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/table2ascii/table_to_ascii.py b/table2ascii/table_to_ascii.py index abdbed0..f0da84e 100644 --- a/table2ascii/table_to_ascii.py +++ b/table2ascii/table_to_ascii.py @@ -591,7 +591,10 @@ def __is_number(text: str) -> bool: @staticmethod def __split_decimal(text: str) -> tuple[str, str]: """Splits a string into a tuple of the integer and decimal parts""" - return tuple(text.split(".", 1)) if "." in text else (text, "") + if "." in text: + before, after = text.split(".", 1) + return before, after + return text, "" def to_ascii(self) -> str: """Generates a formatted ASCII table From c53f2a65412bb0e7cc61a864bcb776a98967f545 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Wed, 28 Dec 2022 02:02:48 -0700 Subject: [PATCH 4/6] refactor decimal size --- table2ascii/table_to_ascii.py | 5 ++--- tests/test_alignments.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/table2ascii/table_to_ascii.py b/table2ascii/table_to_ascii.py index f0da84e..0510bd1 100644 --- a/table2ascii/table_to_ascii.py +++ b/table2ascii/table_to_ascii.py @@ -163,11 +163,10 @@ def __calculate_decimal_widths_and_positions(self) -> tuple[list[int], list[int] max_after_decimal = max(self.__str_width(parts[1]) for parts in split_values) # add 1 for the decimal point if there are any decimal point values has_decimal = any(self.__is_number(value) and "." in value for value in values) - decimal_size = max_before_decimal + max_after_decimal + int(has_decimal) + # store the total width of the decimal numbers in the column + decimal_widths[i] = max_before_decimal + max_after_decimal + int(has_decimal) # store the max digits before the decimal point for decimal alignment decimal_positions[i] = max_before_decimal - # store the total width of the decimal numbers in the column - decimal_widths[i] = decimal_size return decimal_widths, decimal_positions def __calculate_column_widths( diff --git a/tests/test_alignments.py b/tests/test_alignments.py index bbd7104..107c32b 100644 --- a/tests/test_alignments.py +++ b/tests/test_alignments.py @@ -103,7 +103,7 @@ def test_decimal_alignment(): text = t2a( header=["1.1.1", "G", "Long Header", "H", "R", "3.8"], body=[[100.00001, 2, 3.14, 33, "AB", "1.5"], [10.0001, 22.0, 2.718, 3, "CD", "3.03"]], - footer=[10000.01, "123", 10.0, 1, "E", "A"], + footer=[10000.01, "123", 10.0, 0, "E", "A"], alignments=[Alignment.DECIMAL] * 6, first_col_heading=True, style=PresetStyle.double_thin_box, @@ -116,7 +116,7 @@ def test_decimal_alignment(): "╟─────────────╫───────┼─────────────┼────┼────┼─────────╢\n" "║ 10.0001 ║ 22.0 │ 2.718 │ 3 │ CD │ 3.03 ║\n" "╠═════════════╬═══════╪═════════════╪════╪════╪═════════╣\n" - "║ 10000.01 ║ 123 │ 10.0 │ 1 │ E │ A ║\n" + "║ 10000.01 ║ 123 │ 10.0 │ 0 │ E │ A ║\n" "╚═════════════╩═══════╧═════════════╧════╧════╧═════════╝" ) assert text == expected From fcc2f9ebf987081563a31906c28c46ae83904d6c Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Wed, 28 Dec 2022 13:58:39 -0700 Subject: [PATCH 5/6] Update docs --- README.md | 50 +++++----- docs/source/usage.rst | 208 +++++++++++++++++++++--------------------- 2 files changed, 133 insertions(+), 125 deletions(-) diff --git a/README.md b/README.md index ffc0261..de9e288 100644 --- a/README.md +++ b/README.md @@ -69,22 +69,26 @@ print(output) from table2ascii import table2ascii, Alignment output = table2ascii( - header=["#", "G", "H", "R", "S"], - body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]], - first_col_heading=True, - column_widths=[5, 5, 5, 5, 5], - alignments=[Alignment.LEFT] + [Alignment.RIGHT] * 4, + header=["Product", "Category", "Price", "Rating"], + body=[ + ["Milk", "Dairy", "$2.99", "6.283"], + ["Cheese", "Dairy", "$10.99", "8.2"], + ["Apples", "Produce", "$0.99", "10.00"], + ], + column_widths=[12, 12, 12, 12], + alignments=[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT, Alignment.DECIMAL], ) print(output) """ -╔═════╦═══════════════════════╗ -║ # ║ G H R S ║ -╟─────╫───────────────────────╢ -║ 1 ║ 30 40 35 30 ║ -║ 2 ║ 30 40 35 30 ║ -╚═════╩═══════════════════════╝ +╔═══════════════════════════════════════════════════╗ +║ Product Category Price Rating ║ +╟───────────────────────────────────────────────────╢ +║ Milk Dairy $2.99 6.283 ║ +║ Cheese Dairy $10.99 8.2 ║ +║ Apples Produce $0.99 10.00 ║ +╚═══════════════════════════════════════════════════╝ """ ``` @@ -199,18 +203,18 @@ All parameters are optional. At least one of `header`, `body`, and `footer` must Refer to the [documentation](https://table2ascii.readthedocs.io/en/stable/api.html#table2ascii) for more information. -| Option | Type | Default | Description | -| :-----------------: | :----------------------------: | :-------------------: | :-------------------------------------------------------------------------------: | -| `header` | `Sequence[SupportsStr]` | `None` | First table row seperated by header row separator. Values should support `str()` | -| `body` | `Sequence[Sequence[Sequence]]` | `None` | 2D List of rows for the main section of the table. Values should support `str()` | -| `footer` | `Sequence[Sequence]` | `None` | Last table row seperated by header row separator. Values should support `str()` | -| `column_widths` | `Sequence[Optional[int]]` | `None` (automatic) | List of column widths in characters for each column | -| `alignments` | `Sequence[Alignment]` | `None` (all centered) | Column alignments
(ex. `[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT]`) | -| `style` | `TableStyle` | `double_thin_compact` | Table style to use for the table\* | -| `first_col_heading` | `bool` | `False` | Whether to add a heading column separator after the first column | -| `last_col_heading` | `bool` | `False` | Whether to add a heading column separator before the last column | -| `cell_padding` | `int` | `1` | The minimum number of spaces to add between the cell content and the cell border | -| `use_wcwidth` | `bool` | `True` | Whether to use [wcwidth][wcwidth] instead of `len()` to calculate cell width | +| Option | Type | Default | Description | +| :-----------------: | :----------------------------: | :-------------------: | :--------------------------------------------------------------------------------------------------: | +| `header` | `Sequence[SupportsStr]` | `None` | First table row seperated by header row separator. Values should support `str()` | +| `body` | `Sequence[Sequence[Sequence]]` | `None` | 2D List of rows for the main section of the table. Values should support `str()` | +| `footer` | `Sequence[Sequence]` | `None` | Last table row seperated by header row separator. Values should support `str()` | +| `column_widths` | `Sequence[Optional[int]]` | `None` (automatic) | List of column widths in characters for each column | +| `alignments` | `Sequence[Alignment]` | `None` (all centered) | Column alignments
(ex. `[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT, Alignment.DECIMAL]`) | +| `style` | `TableStyle` | `double_thin_compact` | Table style to use for the table\* | +| `first_col_heading` | `bool` | `False` | Whether to add a heading column separator after the first column | +| `last_col_heading` | `bool` | `False` | Whether to add a heading column separator before the last column | +| `cell_padding` | `int` | `1` | The minimum number of spaces to add between the cell content and the cell border | +| `use_wcwidth` | `bool` | `True` | Whether to use [wcwidth][wcwidth] instead of `len()` to calculate cell width | [wcwidth]: https://pypi.org/project/wcwidth/ diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 70f8487..823c23e 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -6,115 +6,119 @@ Convert lists to ASCII tables .. code:: py - from table2ascii import table2ascii - - output = table2ascii( - header=["#", "G", "H", "R", "S"], - body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]], - footer=["SUM", "130", "140", "135", "130"], - ) - - print(output) - - """ - ╔═════════════════════════════╗ - ║ # G H R S ║ - ╟─────────────────────────────╢ - ║ 1 30 40 35 30 ║ - ║ 2 30 40 35 30 ║ - ╟─────────────────────────────╢ - ║ SUM 130 140 135 130 ║ - ╚═════════════════════════════╝ - """ + from table2ascii import table2ascii + + output = table2ascii( + header=["#", "G", "H", "R", "S"], + body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]], + footer=["SUM", "130", "140", "135", "130"], + ) + + print(output) + + """ + ╔═════════════════════════════╗ + ║ # G H R S ║ + ╟─────────────────────────────╢ + ║ 1 30 40 35 30 ║ + ║ 2 30 40 35 30 ║ + ╟─────────────────────────────╢ + ║ SUM 130 140 135 130 ║ + ╚═════════════════════════════╝ + """ Set first or last column headings ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: py - from table2ascii import table2ascii + from table2ascii import table2ascii - output = table2ascii( - body=[["Assignment", "30", "40", "35", "30"], ["Bonus", "10", "20", "5", "10"]], - first_col_heading=True, - ) + output = table2ascii( + body=[["Assignment", "30", "40", "35", "30"], ["Bonus", "10", "20", "5", "10"]], + first_col_heading=True, + ) - print(output) + print(output) - """ - ╔════════════╦═══════════════════╗ - ║ Assignment ║ 30 40 35 30 ║ - ║ Bonus ║ 10 20 5 10 ║ - ╚════════════╩═══════════════════╝ - """ + """ + ╔════════════╦═══════════════════╗ + ║ Assignment ║ 30 40 35 30 ║ + ║ Bonus ║ 10 20 5 10 ║ + ╚════════════╩═══════════════════╝ + """ Set column widths and alignments ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: py - from table2ascii import table2ascii, Alignment + from table2ascii import table2ascii, Alignment - output = table2ascii( - header=["#", "G", "H", "R", "S"], - body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]], - first_col_heading=True, - column_widths=[5, 5, 5, 5, 5], - alignments=[Alignment.LEFT] + [Alignment.RIGHT] * 4, - ) + output = table2ascii( + header=["Product", "Category", "Price", "Rating"], + body=[ + ["Milk", "Dairy", "$2.99", "6.283"], + ["Cheese", "Dairy", "$10.99", "8.2"], + ["Apples", "Produce", "$0.99", "10.00"], + ], + column_widths=[12, 12, 12, 12], + alignments=[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT, Alignment.DECIMAL], + ) - print(output) + print(output) - """ - ╔═════╦═══════════════════════╗ - ║ # ║ G H R S ║ - ╟─────╫───────────────────────╢ - ║ 1 ║ 30 40 35 30 ║ - ║ 2 ║ 30 40 35 30 ║ - ╚═════╩═══════════════════════╝ - """ + """ + ╔═══════════════════════════════════════════════════╗ + ║ Product Category Price Rating ║ + ╟───────────────────────────────────────────────────╢ + ║ Milk Dairy $2.99 6.283 ║ + ║ Cheese Dairy $10.99 8.2 ║ + ║ Apples Produce $0.99 10.00 ║ + ╚═══════════════════════════════════════════════════╝ + """ Use a preset style ~~~~~~~~~~~~~~~~~~ .. code:: py - from table2ascii import table2ascii, Alignment, PresetStyle - - output = table2ascii( - header=["First", "Second", "Third", "Fourth"], - body=[["10", "30", "40", "35"], ["20", "10", "20", "5"]], - column_widths=[10, 10, 10, 10], - style=PresetStyle.ascii_box - ) - - print(output) - - """ - +----------+----------+----------+----------+ - | First | Second | Third | Fourth | - +----------+----------+----------+----------+ - | 10 | 30 | 40 | 35 | - +----------+----------+----------+----------+ - | 20 | 10 | 20 | 5 | - +----------+----------+----------+----------+ - """ - - output = table2ascii( - header=["First", "Second", "Third", "Fourth"], - body=[["10", "30", "40", "35"], ["20", "10", "20", "5"]], - style=PresetStyle.plain, - cell_padding=0, - alignments=[Alignment.LEFT] * 4, - ) - - print(output) - - """ - First Second Third Fourth - 10 30 40 35 - 20 10 20 5 - """ + from table2ascii import table2ascii, Alignment, PresetStyle + + output = table2ascii( + header=["First", "Second", "Third", "Fourth"], + body=[["10", "30", "40", "35"], ["20", "10", "20", "5"]], + column_widths=[10, 10, 10, 10], + style=PresetStyle.ascii_box + ) + + print(output) + + """ + +----------+----------+----------+----------+ + | First | Second | Third | Fourth | + +----------+----------+----------+----------+ + | 10 | 30 | 40 | 35 | + +----------+----------+----------+----------+ + | 20 | 10 | 20 | 5 | + +----------+----------+----------+----------+ + """ + + output = table2ascii( + header=["First", "Second", "Third", "Fourth"], + body=[["10", "30", "40", "35"], ["20", "10", "20", "5"]], + style=PresetStyle.plain, + cell_padding=0, + alignments=[Alignment.LEFT] * 4, + ) + + print(output) + + """ + First Second Third Fourth + 10 30 40 35 + 20 10 20 5 + """ Define a custom style ~~~~~~~~~~~~~~~~~~~~~ @@ -123,27 +127,27 @@ Check :ref:`TableStyle` for more info. .. code:: py - from table2ascii import table2ascii, TableStyle + from table2ascii import table2ascii, TableStyle - my_style = TableStyle.from_string("*-..*||:+-+:+ *''*") + my_style = TableStyle.from_string("*-..*||:+-+:+ *''*") - output = table2ascii( - header=["First", "Second", "Third"], - body=[["10", "30", "40"], ["20", "10", "20"], ["30", "20", "30"]], - style=my_style, - ) + output = table2ascii( + header=["First", "Second", "Third"], + body=[["10", "30", "40"], ["20", "10", "20"], ["30", "20", "30"]], + style=my_style, + ) - print(output) + print(output) - """ - *-------.--------.-------* - | First : Second : Third | - +-------:--------:-------+ - | 10 : 30 : 40 | - | 20 : 10 : 20 | - | 30 : 20 : 30 | - *-------'--------'-------* - """ + """ + *-------.--------.-------* + | First : Second : Third | + +-------:--------:-------+ + | 10 : 30 : 40 | + | 20 : 10 : 20 | + | 30 : 20 : 30 | + *-------'--------'-------* + """ Merge adjacent cells ~~~~~~~~~~~~~~~~~~~~ From 1e8ad96d7db5b1d5cd1bb5905c5271742c9b01a1 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Wed, 28 Dec 2022 14:07:20 -0700 Subject: [PATCH 6/6] Update alignment.py --- table2ascii/alignment.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/table2ascii/alignment.py b/table2ascii/alignment.py index b298e5c..2ae487f 100644 --- a/table2ascii/alignment.py +++ b/table2ascii/alignment.py @@ -31,11 +31,15 @@ class Alignment(IntEnum): .. note:: If the :attr:`DECIMAL` alignment type is used, any cell values that are - not valid decimal numbers will be aligned to the center. + not valid decimal numbers will be aligned to the center. Decimal numbers + include integers, floats, and strings containing only + :meth:`decimal ` characters and at most one decimal point. .. versionchanged:: 1.1.0 - Added :attr:`DECIMAL` alignment type + Added :attr:`DECIMAL` alignment -- align decimal numbers such that + the decimal point is aligned with the decimal point of all other numbers + in the same column. """ LEFT = 0