Some of you might remember my previous implementations of this idea in C++. The threads can be found here and here. I tried to implemented the same idea, but this time in python.
A summary of the concept again: The idea is to organize data nicely in a table structure that is fully console-based but still highly customizable so that it allows to insert, edit, sort and delete data in a convenient manner.
For example input data like this:
["Germany", "Berlin", "82800000"]
["South Korea", "Seoul", "51446201"]
["China", "Beijing", "1403500365"]
["France", "Paris", "67201000"]
["Australia", "Canberra", "24877800"]
["Netherlands", "Amsterdam", "17200671"]
["Iceland", "Reykjavik", "348580"]
would be structured like this using the DataTable class:
+---------------+-------------+--------------+
| Country | Capital | Population |
+---------------+-------------+--------------+
| Germany | Berlin | 82800000 |
| South Korea | Seoul | 51446201 |
| China | Beijing | 1403500365 |
| France | Paris | 67201000 |
| Australia | Canberra | 24877800 |
| Netherlands | Amsterdam | 17200671 |
| Iceland | Reykjavik | 348580 |
+---------------+-------------+--------------+
DataTable.py
class DataTable:
"""
Defines the padding between the text and each cell in the table
"""
padding = 2
"""
Holds the width of the widest entry of each column in the table
"""
column_widths = []
"""
Holds the description for each column header
"""
table_headers = []
"""
Holds all the rows and their information
"""
table_data = []
def __init__(self, table_headers, cell_padding):
"""
Creates a new data table of a given size.
:param table_headers: Array of table headers
:param cell_padding: The padding between the cell text and the cell border
"""
self.table_headers.append(table_headers)
self.calculate_widths()
self.padding = cell_padding
def calculate_widths(self):
"""
Calculate the correct width to the row and cell borders based on the content length in the data table.
:return: None
"""
# If no headers exist it is not possible to calculate the table widths
if len(self.table_headers) == 0:
raise Exception("Header required to calculate widths")
# If no column widths were set initialize them using the header text width
if len(self.column_widths) == 0:
for i in range(0, len(self.table_headers[0])):
self.column_widths.append(len(self.table_headers[0][i]))
# If the table has no rows the header defines the widths
for i in range(0, len(self.table_headers[0])):
self.column_widths[i] = len(self.table_headers[0][i]) # check if [0] is right thing to use here
# If the table has rows check all for larger widths then the current one
for i in range(0, len(self.table_data)):
for j in range(0, len(self.table_data[i])):
if len(self.table_data[i][j]) > self.column_widths[j]:
self.column_widths[j] = len(self.table_data[i][j])
def add_row(self, values):
"""
Adds a row to the data table.
:param values: Array of row values, count must be equal to data table widths
:return: None
"""
# Ensure correct amount of values is provided to fill exactly one table row
if len(values) != len(self.table_headers[0]):
raise Exception("Value count doesn't match table dimensions")
self.table_data.append(values)
self.calculate_widths()
def insert_row(self, index, values):
"""
Insert a row into the data table at a specific position
:param index: The index in the table where the row gets inserted
:param values: Array of row values, count must be equal to data table widths
:return: None
"""
# Ensure correct amount of values is provided to fill exactly one table row
if len(values) != len(self.table_headers[0]):
raise Exception("Value count doesn't match table dimensions")
self.table_data.insert(index, values)
self.calculate_widths()
def edit_row(self, row_index, column_index, value):
"""
Edit a field in a row in the data table
:param row_index: The index of the row that should be edited
:param column_index: The position of the cell in the selected row that should be edited
:param value: The new value the cell will be updated with
:return: None
"""
# Ensure row_index is valid
if row_index < 0 or row_index > len(self.table_data):
raise Exception("Row index out of range")
# Ensure column index is valid
if column_index < 0 or column_index > len(self.table_data[0]):
raise Exception("Column index out of range")
self.table_data[row_index][column_index] = value
self.calculate_widths()
def delete_row(self, row_index):
"""
Removes a row from the data table
:param row_index: The index of the row that should be deleted
:return: None
"""
if row_index < 0 or row_index > len(self.table_data):
raise Exception("Row index out of range")
del self.table_data[row_index]
self.calculate_widths()
def edit_header(self, column_index, value):
"""
Edit one of the header columns of the data table
:param column_index: The index of the column that should be edited
:param value: The new value that should be written in the selected header field
:return: None
"""
self.table_headers[0][column_index] = value
if len(self.table_headers[0][column_index]) > self.column_widths[column_index]:
self.column_widths[column_index] = len(self.table_headers[0][column_index])
self.calculate_widths()
def print_separator(self):
"""
Generates a separator line that fits the table width
:return: The separator line
"""
output = "+"
for i in range(0, len(self.column_widths)):
output += ("-" * self.column_widths[i]) + ("-" * 2 * self.padding) + "+"
return output
def sort_table(self, column_index, descending):
"""
Sorts the data table based on a column specified by the column index
:param column_index: The index of the column that should be used for sorting
:param descending: If true the table will be sorted in descending order, otherwise in ascending order
:return: None
"""
self.table_data.sort(key=lambda x: x[column_index], reverse=descending)
def print_header(self):
"""
Generates the table header of the data table formatted for printing
:return:
"""
output = ""
for i in range(0, len(self.table_headers[0])):
text = self.table_headers[0][i]
diff = self.column_widths[i] - len(self.table_headers[0][i])
output += ("|" + " " * self.padding + text + diff * " " + " " * self.padding)
return output + "|"
def print_rows(self):
"""
Prints all rows and the intersecting separators of the data table
:return: None
"""
output = ""
for j in range(0, len(self.table_data)):
for i in range(0, len(self.table_data[j])):
text = self.table_data[j][i]
diff = self.column_widths[i] - len(self.table_data[j][i])
output += ("|" + " " * self.padding + text + diff * " " + " " * self.padding)
if j != len(self.table_data) - 1:
output += "|\n"
else:
output += "|"
return output
def display(self):
"""
Displays the formatted data table in text form
:return: None
"""
print(self.print_separator())
print(self.print_header())
print(self.print_separator())
print(self.print_rows())
print(self.print_separator())
Usage example:
from datatable import DataTable
table = DataTable(["Country", "Capital", "Population"], 2)
table.add_row(["Germany", "Berlin", "82800000"])
table.add_row(["South Korea", "Seoul", "51446201"])
table.add_row(["China", "Beijing", "1403500365"])
table.add_row(["France", "Paris", "67201000"])
table.add_row(["Australia", "Canberra", "24877800"])
table.add_row(["Netherlands", "Amsterdam", "17200671"])
table.add_row(["Iceland", "Reykjavik", "348580"])
# Delete a row
table.delete_row(3)
# Update cell with a new value
table.edit_row(1, 1, "SEOUL!!")
# Edit header
table.edit_header(2, "PEOPLE!!")
# Sort table alphabetically by capital
table.sort_table(1, False)
# Show the table
table.display()
Any bad practices or things that I could improve to make it more convenient to use? Also I am open for more functionality/features that could be added to this class.