Welcome, guest | Sign In | My Account | Store | Cart

This function will display a nicely formatted textual table for you. Features include: auto-sizing of columns, auto-alignment based on column-type (which it sniffs from the first row), nicely formated centered headings, and most importantly wrapping of cells. Of course, you can manually override pretty much everything in case you don't like the defaults.

Python, 138 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import string
from textwrap import wrap

MIN = 1
UNBIASED = 2

def display_table(rows,             # List of tuples of data
                  headings=[],      # Optional headings for columns
                  col_widths=[],    # Column widths 
                  col_justs=[],     # Column justifications (str.ljust, etc)
                  screen_width=80,  # Width of terminal
                  col_spacer=2,     # Space between columns
                  fill_char=' ',    # Fill character
                  col_sep='=',      # Separator char
                  row_term='\n',    # row terminator (could be <br />)
                  norm_meth=MIN,    # Screen width normailization method
                  ):
 
    _col_justs = list(col_justs)
    _col_widths = list(col_widths)

    # String-ify everything
    rows = [tuple((str(col) for col in row)) for row in rows]
    
    # Compute appropriate col_widths if not given
    if not col_widths:
        if headings:
            _col_widths = [max(row) for row in (map(len, col) 
                           for col in zip(headings, *rows))]
        else:
            _col_widths = [max(row) for row in (map(len, col) 
                           for col in zip(*rows))]

    num_cols = len(_col_widths)
    col_spaces = col_spacer * (num_cols - 1)

    # Compute the size a row in our table would be in chars
    def _get_row_size(cw):
        return sum(cw) + col_spaces
    
    row_size = _get_row_size(_col_widths)
    
    def _unbiased_normalization():
        """ Normalize keeping the ratio of column sizes the same """
        __col_widths = [int(col_width * 
                           (float(screen_width - col_spaces) / row_size))
                       for col_width in _col_widths]

        # Distribute epsilon underage to the the columns
        for x in xrange(screen_width - _get_row_size(__col_widths)):
            __col_widths[x % num_cols] += 1
        return __col_widths

    def _min_normalization():
        """ Bring all columns up to the minimum """
        __col_widths = _unbiased_normalization()

        # A made up heuristic -- hope it looks good
        col_min = int(0.5 * min(row_size, screen_width) / float(num_cols))
       
        # Bring all the columns up to the minimum
        norm_widths = []
        for col_width, org_width in zip(__col_widths, _col_widths):
            if col_width < col_min: 
                col_width = min(org_width, col_min)
            norm_widths.append(col_width) 

        # Distribute epsilon overage to the the columns
        count = _get_row_size(norm_widths) - screen_width
        x = 0
        while count > 0:
            if norm_widths[x % num_cols] > col_min:
                norm_widths[x % num_cols] -= 1
                count -= 1
            x += 1

        return norm_widths

    if not col_widths:
        # Normalize columns to screen size
        if row_size > screen_width:
            if norm_meth is UNBIASED:
                _col_widths = _unbiased_normalization()
            else:
                _col_widths = _min_normalization()

    row_size = _get_row_size(_col_widths)

    # If col_justs are not specified then guess the justification from
    # the appearence of the first row of data
    # Numbers and money are right justified, alpha beginning strings are left
    if not _col_justs:
        for col_datum in rows[0]:
            if isinstance(col_datum, str):
                if col_datum.startswith(tuple(string.digits + '$')):
                    _col_justs.append(str.rjust)
                else:
                    _col_justs.append(str.ljust)
            else:
                _col_justs.append(str.rjust)

    # Calculate the minimum screen width needed based on col_spacer and number
    # of columns
    min_screen_width = num_cols + col_spaces
    
    assert screen_width >= min_screen_width, "Screen Width is set too small, must be >= %d" % min_screen_width

    row_size = _get_row_size(_col_widths)

    def _display_wrapped_row(row, heading=False):
        """ Take a row, wrap it, and then display in proper tabular format
        """
        wrapped_row = [wrap(col_datum, col_width)
                        for col_datum, col_width in zip(row, _col_widths)]
        row_lines = []
        for cols in map(None, *wrapped_row):
            if heading:
                partial = (str.center((partial_col or ''), col_width, fill_char)
                        for partial_col, col_width in zip(cols, _col_widths))
            else:
                partial = (col_just((partial_col or ''), col_width, fill_char)
                        for partial_col, col_width, col_just in zip(cols, 
                                                                    _col_widths,
                                                                    _col_justs))
            row_lines.append((fill_char * col_spacer).join(partial))

        print row_term.join(row_lines)

    if headings:
        # Print out the headings
        _display_wrapped_row(headings, heading=True)

        # Print separator
        print col_sep * row_size

    # Print out the rows of data
    for row in rows:
        _display_wrapped_row(row)

I shouldn't have to tell you, tables are useful! I originally had a very naive table display function that I decided to beef up. The code ain't the prettiest; so, I'd like to clean that up some time. As for bugs, my test suite of 5 types of tables (hardly comprehensive... I know) passes. If you find something that isn't working just let me know.

Since you need fixed fonts to appreciate this, I'll just drop off some demo code so you can play with this in python:

def demo1():

headings = ["Description", "Acct-Code", "Total"]
rows = [("Catalytic Converter", "10-1000", "$100.23"),
        ("V-8, Fuel Injected, Hemispherically shaped head, custom valve springs, with a competition Torsion rotor", "10-1202", "$6300.00"),
        ("Power Clutch", "11-3440", "$330.32"),
        ("6-Speed Manual Transmission with paddle shifters, tungsten carbide syncronizers and dual-in-line throw-out bearings with reinforced steel armatures", "11-1000", "$3420.00")]
display_table(rows, headings)

def demo2():

rows = [(
"Now is the time for all good men to come to the aid of their country. "*8,
"This is a test of the emergency broadcast system. " * 5,
"In Xanadu did Kublah Khan a pleasure dome decree. " * 7,
)]

display_table(rows)

def demo3(): headings = ["Rank", "Movie Name"] rows = [(1, "Usual Suspects"), (2, "Memento"), (3, "There's Something About Mary"), (4, "Airplane"), (5, "The Godfather")]

display_table(rows, headings)

3 comments

Pythy Python-breeder 14 years, 7 months ago  # | flag

python2.5 only. Note that this recipe is working only for Python-2.5

Richard Harris (author) 14 years, 7 months ago  # | flag

Generator expressions etc. Now that I have 2.5, everything I write has a generator expression or a ternary operator somewhere in it.

I think if you replaced these, it should work on 2.4.

Karen Law 14 years, 2 months ago  # | flag

Make code work with Python 2.4. Replace the "if col_datum.startswith(tuple(string.digits + '$')):" loop with a plain "_col_justs.append(str.rjust)" It has worked for me.