5
\$\begingroup\$

When answering python tuition calculator v2, I got a little carried away, and started building a print_table() function which I don't like the resulting code for, and I would love guidance into improving (and/or enhancing) my code.

My ambitions were as follows:

  • Have a print_table() with as little formatting as possible, whilst still maintaining a similar output
  • Have an easy way to change the column layout, in case I later on decide to change the titles, number of columns, width of columns
  • Ensure proper alignment in columns, i.e. titles are left justified, whilst numbers are right justified. If currency I would also like for the currency sign to be correctly inserted (just in front of the number for $

The code works, but I don't like the look of it, and I would like for it to be improved in the following aspects:

  • It doesn't add the $ in front of the numbers
  • It is not dynamic in number of columns (or width of columns)
  • I would love for a better way of templating the column format, for use both in the header and the actual lines of the table, but got into issues related to partially expanding the format string

Here is my current code:

YEAR_V1 = [
    (2015, 2520.00, 120.00),
    (2016, 2646.00, 126.00),
    (2017, 2778.30, 132.30),
    (2018, 2917.22, 138.92) ]

YEAR_V2 = [
    (2015, 42000.00, 2000.00),
    (2016, 44100.00, 2100.00),
    (2017, 46305.00, 2205.00),
    (2018, 48620.00, 2315.25) ]


def print_columns(year, tuition, increase,
                  header=False, column_separator="   "):
    COL1_WIDTH = 15
    COL2_WIDTH = 12
    COL3_WIDTH = 12

    COLUMN_FORMAT = '{year:{col_year}}' + column_separator + \
                    '{tuition:{col_tuition}}' + column_separator + \
                    '{increase:{col_increase}}'

    if header:
        print(COLUMN_FORMAT.format(year=year,
                                   col_year='<{}'.format(COL1_WIDTH),
                                   tuition=tuition,
                                   col_tuition='<{}'.format(COL2_WIDTH),
                                   increase=increase,
                                   col_increase='<{}'.format(COL2_WIDTH)))
        print(COLUMN_FORMAT.format(year="-" * COL1_WIDTH,
                                   col_year='<{}'.format(COL1_WIDTH),
                                   tuition = "-" * COL2_WIDTH,
                                   col_tuition='<{}'.format(COL2_WIDTH),
                                   increase="-" * COL3_WIDTH,
                                   col_increase='<{}'.format(COL3_WIDTH)))
    else:
        print(COLUMN_FORMAT.format(year=year,
                                   col_year='<{}'.format(COL1_WIDTH),
                                   tuition=tuition,
                                   col_tuition='>{},.2f'.format(COL2_WIDTH),
                                   increase=increase,
                                   col_increase='>{},.2f'.format(COL3_WIDTH)))


def print_table(academic_year, column_separator="   "):
    print_columns("Academic year", "Tuition", "Increase",
                  header=True, column_separator=column_separator)

    sum_interest = 0
    for (year, tuition, interest) in academic_year:
        sum_interest += interest
        print_columns('{}-{}'.format(year, year+1), tuition, interest,
                      column_separator = column_separator)

    print('{:>30}   ${:11,.2f} '.format('Total tuition increase:', sum_interest))


print_table(YEAR_V1)
print
print_table(YEAR_V2, column_separator = "|")

Which produces the following output:

Academic year     Tuition        Increase    
---------------   ------------   ------------
2015-2016             2,520.00         120.00
2016-2017             2,646.00         126.00
2017-2018             2,778.30         132.30
2018-2019             2,917.22         138.92
       Total tuition increase:   $     517.22 

Academic year  |Tuition     |Increase    
---------------|------------|------------
2015-2016      |   42,000.00|    2,000.00
2016-2017      |   44,100.00|    2,100.00
2017-2018      |   46,305.00|    2,205.00
2018-2019      |   48,620.00|    2,315.25
       Total tuition increase:   $   8,620.25 
\$\endgroup\$
  • 1
    \$\begingroup\$ Minor typo in line col_increase='<{}'.format(COL2_WIDTH))) -- shouldn't that be COL3_WIDTH? \$\endgroup\$ – Snowbody Sep 30 '15 at 20:19
1
\$\begingroup\$

After fumbling along for some time I have come up with the class PrettyTable to help me in my formatting quest. This class extends the functionality with:

  • Dynamic number of columns, through use of addColumns() and a list input to print_line()
  • Separate specification of header and column alignment, as well as a specific setting of the value_format. This allows for specifying stuff like '${:,.2f}', which is a float printed with 2 decimals, and with a leading $ sign
  • It is more flexible in the format settings as the header and column alignment is built on top of string alignments and these are added to tables before being joined before output operations (and for the line this joining is cached)
  • Allows for change of column separator, line printing and some replacement within the column separator...

Here is the base class and example as given in question:

YEAR_V1 = [
           (2015, 2520.00, 120.00),
           (2016, 2646.00, 126.00),
           (2017, 2778.30, 132.30),
           (2018, 2917.22, 138.92) ]

YEAR_V2 = [
            (2015, 42000.00, 2000.00),
            (2016, 44100.00, 2100.00),
            (2017, 46305.00, 2205.00),
            (2018, 48620.00, 2315.25) ]

class PrettyTable():
    """Helper class to print pretty tables with proper alignment


    After initialising the class, one adds column definitions to
    the table by using add_column(). This method builds definitions
    to be used later when you start printing the table using:

      * print_header()
      * print_separator()
      * and multiple print_line()

    To customize the output of each column add formatting option to
    the add_column() statement. You can control width and alignment of
    both header and columns. Specify alignment with a single character:
      * left '<', center '=', and right '>'
      * header_alignment defaults to left '<'
      * column_alignment defaults to right '>'

    In addition you can specify the formatting of the actual column value 
    using the value_format parameter which is applied before the column 
    alignment.

    Default separator is a line consisting of '-' matching column widths,
    and a column separator of '   ' (three spaces). These can however
    be changed through the use of column_separator and replace_space_char.
    Try calling with column_separator=' | ', and replace_space_char='-'.
    """
    HEADER_PART = '{0:{1}{2}}'
    ROW_PART = '{{:{0}{1}}}'

    def __init__(self,
                 column_separator = '   ',
                 line_char = '-',
                 replace_space_char=None):

        self._column_separator = column_separator

        if replace_space_char == None:
            self._replaced_column_separator = column_separator
        else:
            self._replaced_column_separator = column_separator.replace(' ', replace_space_char)
        self._line_char = line_char

        self._header_text = []
        self._header_line = []
        self._row_format = []
        self._column_formats = []
        self._joined_row_format = None


    def add_column(self, title, width,
                   header_alignment='<',
                   column_alignment='>',
                   value_format = '{}'):
        """ Adds a new column to the table

        Extends the current header_text, header_line and 
        row_format with proper text and alignment. Pushes the 
        format to used for values in this column to column_formats for
        later use in print_line()
        """

        self._header_text.append(self.HEADER_PART
                                     .format(title,
                                             header_alignment,
                                             width))

        self._header_line.append(self.HEADER_PART
                                     .format(self._line_char * width,
                                             header_alignment,
                                             width))

        self._row_format.append(self.ROW_PART
                                    .format(column_alignment,
                                            width))

        self._column_formats.append(value_format)

    def print_header(self):
        """ Prints a header line, generated in add_column()"""
        print(self._column_separator.join(self._header_text))


    def print_separator(self):
        """ Prints a separator line, generated in add_column()"""
        print(self._replaced_column_separator.join(self._header_line))


    def print_line(self, *columns):
        """ Print a line in the table

        First we build a list of the formatted version of all column values,
        before printing this list using the row format
        """
        if self._joined_row_format == None:
            self._joined_row_format = self._column_separator.join(self._row_format)

        formatted_values = [self._column_formats[i].format(value)
                                 for (i, value) in enumerate(columns)]
        print(self._joined_row_format.format(*formatted_values))


def print_table(academic_year,
                column_separator="   ",
                replace_space_char = None):

    table = PrettyTable(column_separator = column_separator,
                        replace_space_char = replace_space_char)

    table.add_column('Academic year', 15,
                     column_alignment='<')
    table.add_column('Tuition', 12,
                     header_alignment='^', value_format='${:,.2f}')
    table.add_column('Interest', 12,
                     header_alignment='>', value_format='${:,.2f}')

    table.print_header()
    table.print_separator()

    sum_interest = 0
    for (year, tuition, interest) in academic_year:
        sum_interest += interest
        table.print_line('{}-{}'.format(year, year+1), tuition, interest)

    print('{:>30}   ${:11,.2f} '.format('Total tuition increase:', sum_interest))


print_table(YEAR_V1)
print
print_table(YEAR_V2, column_separator = " | ", replace_space_char='-')

Output from this one is:

Academic year       Tuition          Interest
---------------   ------------   ------------
2015-2016            $2,520.00        $120.00
2016-2017            $2,646.00        $126.00
2017-2018            $2,778.30        $132.30
2018-2019            $2,917.22        $138.92
       Total tuition increase:   $     517.22 

Academic year   |   Tuition    |     Interest
----------------|--------------|-------------
2015-2016       |   $42,000.00 |    $2,000.00
2016-2017       |   $44,100.00 |    $2,100.00
2017-2018       |   $46,305.00 |    $2,205.00
2018-2019       |   $48,620.00 |    $2,315.25
       Total tuition increase:   $   8,620.25 

And here is a slightly extended example displaying some of the more esoteric options available using this class. It also shows how neat the output can be, and gives a clear separation of presentation versus the logic to print the table.

def extended_print_example():
    MY_DATA = [
      ( 1, 10, 20, 'pi', 3.1415926),
      ( 2, -10, -20, 'e', 2.71828),
      ( 3, 12, 2999, 'plank', 6.626)]

    table = PrettyTable()
    table.add_column('Idx', 3)
    table.add_column('Int1', 5)
    # Notice how the column width is 10, but the values is formatted to
    # max width of 5 and padded with '*' and always showing sign character
    # and finally this formatted value is left aligned...
    table.add_column('Int2', 10, column_alignment='<', value_format='{:*=+#5}')
    table.add_column('Name', 7, column_alignment='^')
    table.add_column('Value', 7, value_format='{:,.4f}')

    table.print_header()
    table.print_separator()

    for line in MY_DATA:
        table.print_line(*line)

extended_print_example()

And the output from the extended example is:

Idx   Int1    Int2         Name      Value  
---   -----   ----------   -------   -------
  1      10   +**20          pi       3.1416
  2     -10   -**20           e       2.7183
  3      12   +2999         plank     6.6260
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.