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

A function for pretty-printing a table.

Python, 146 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
139
140
141
142
143
144
145
146
import cStringIO,operator

def indent(rows, hasHeader=False, headerChar='-', delim=' | ', justify='left',
           separateRows=False, prefix='', postfix='', wrapfunc=lambda x:x):
    """Indents a table by column.
       - rows: A sequence of sequences of items, one sequence per row.
       - hasHeader: True if the first row consists of the columns' names.
       - headerChar: Character to be used for the row separator line
         (if hasHeader==True or separateRows==True).
       - delim: The column delimiter.
       - justify: Determines how are data justified in their column. 
         Valid values are 'left','right' and 'center'.
       - separateRows: True if rows are to be separated by a line
         of 'headerChar's.
       - prefix: A string prepended to each printed row.
       - postfix: A string appended to each printed row.
       - wrapfunc: A function f(text) for wrapping text; each element in
         the table is first wrapped by this function."""
    # closure for breaking logical rows to physical, using wrapfunc
    def rowWrapper(row):
        newRows = [wrapfunc(item).split('\n') for item in row]
        return [[substr or '' for substr in item] for item in map(None,*newRows)]
    # break each logical row into one or more physical ones
    logicalRows = [rowWrapper(row) for row in rows]
    # columns of physical rows
    columns = map(None,*reduce(operator.add,logicalRows))
    # get the maximum of each column by the string length of its items
    maxWidths = [max([len(str(item)) for item in column]) for column in columns]
    rowSeparator = headerChar * (len(prefix) + len(postfix) + sum(maxWidths) + \
                                 len(delim)*(len(maxWidths)-1))
    # select the appropriate justify method
    justify = {'center':str.center, 'right':str.rjust, 'left':str.ljust}[justify.lower()]
    output=cStringIO.StringIO()
    if separateRows: print >> output, rowSeparator
    for physicalRows in logicalRows:
        for row in physicalRows:
            print >> output, \
                prefix \
                + delim.join([justify(str(item),width) for (item,width) in zip(row,maxWidths)]) \
                + postfix
        if separateRows or hasHeader: print >> output, rowSeparator; hasHeader=False
    return output.getvalue()

# written by Mike Brown
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/148061
def wrap_onspace(text, width):
    """
    A word-wrap function that preserves existing line breaks
    and most spaces in the text. Expects that existing line
    breaks are posix newlines (\n).
    """
    return reduce(lambda line, word, width=width: '%s%s%s' %
                  (line,
                   ' \n'[(len(line[line.rfind('\n')+1:])
                         + len(word.split('\n',1)[0]
                              ) >= width)],
                   word),
                  text.split(' ')
                 )

import re
def wrap_onspace_strict(text, width):
    """Similar to wrap_onspace, but enforces the width constraint:
       words longer than width are split."""
    wordRegex = re.compile(r'\S{'+str(width)+r',}')
    return wrap_onspace(wordRegex.sub(lambda m: wrap_always(m.group(),width),text),width)

import math
def wrap_always(text, width):
    """A simple word-wrap function that wraps text on exactly width characters.
       It doesn't split the text in words."""
    return '\n'.join([ text[width*i:width*(i+1)] \
                       for i in xrange(int(math.ceil(1.*len(text)/width))) ])
    
if __name__ == '__main__':
    labels = ('First Name', 'Last Name', 'Age', 'Position')
    data = \
    '''John,Smith,24,Software Engineer
       Mary,Brohowski,23,Sales Manager
       Aristidis,Papageorgopoulos,28,Senior Reseacher'''
    rows = [row.strip().split(',')  for row in data.splitlines()]

    print 'Without wrapping function\n'
    print indent([labels]+rows, hasHeader=True)
    # test indent with different wrapping functions
    width = 10
    for wrapper in (wrap_always,wrap_onspace,wrap_onspace_strict):
        print 'Wrapping function: %s(x,width=%d)\n' % (wrapper.__name__,width)
        print indent([labels]+rows, hasHeader=True, separateRows=True,
                     prefix='| ', postfix=' |',
                     wrapfunc=lambda x: wrapper(x,width))
    
    # output:
    #
    #Without wrapping function
    #
    #First Name | Last Name        | Age | Position         
    #-------------------------------------------------------
    #John       | Smith            | 24  | Software Engineer
    #Mary       | Brohowski        | 23  | Sales Manager    
    #Aristidis  | Papageorgopoulos | 28  | Senior Reseacher 
    #
    #Wrapping function: wrap_always(x,width=10)
    #
    #----------------------------------------------
    #| First Name | Last Name  | Age | Position   |
    #----------------------------------------------
    #| John       | Smith      | 24  | Software E |
    #|            |            |     | ngineer    |
    #----------------------------------------------
    #| Mary       | Brohowski  | 23  | Sales Mana |
    #|            |            |     | ger        |
    #----------------------------------------------
    #| Aristidis  | Papageorgo | 28  | Senior Res |
    #|            | poulos     |     | eacher     |
    #----------------------------------------------
    #
    #Wrapping function: wrap_onspace(x,width=10)
    #
    #---------------------------------------------------
    #| First Name | Last Name        | Age | Position  |
    #---------------------------------------------------
    #| John       | Smith            | 24  | Software  |
    #|            |                  |     | Engineer  |
    #---------------------------------------------------
    #| Mary       | Brohowski        | 23  | Sales     |
    #|            |                  |     | Manager   |
    #---------------------------------------------------
    #| Aristidis  | Papageorgopoulos | 28  | Senior    |
    #|            |                  |     | Reseacher |
    #---------------------------------------------------
    #
    #Wrapping function: wrap_onspace_strict(x,width=10)
    #
    #---------------------------------------------
    #| First Name | Last Name  | Age | Position  |
    #---------------------------------------------
    #| John       | Smith      | 24  | Software  |
    #|            |            |     | Engineer  |
    #---------------------------------------------
    #| Mary       | Brohowski  | 23  | Sales     |
    #|            |            |     | Manager   |
    #---------------------------------------------
    #| Aristidis  | Papageorgo | 28  | Senior    |
    #|            | poulos     |     | Reseacher |
    #---------------------------------------------

The quick-and-dirty way of printing table data uses a delimiter - usually tab or space - to indent elements by column. This works fine as long as the elements in each column have the same (or similar) lengths; if not, the output can get pretty messy.

This recipe finds the max required length for each column and uses it to indent its items. Several optional parameters can be set to customize the output.

7 comments

Raymond Hettinger 17 years, 10 months ago  # | flag

Nits. Nice recipe.

One small improvement would be to simplify the maxwidth calculation:

maxWidths = [max([len(str(item)) for item in column]) for column in columns]

Also, the justify logic can be simplified with a dictionary:

dispatch = dict(center=str.center, right=str.rjust, left=str.ljust)
 . . .
justify = dispatch[justify.lower()]
Attila Vásárhelyi 17 years, 9 months ago  # | flag

something similar for restructured text. I am using a similar function to output tables in restructured text format (I generate lots of reports in rst). I brushed it up a little, added comments ... Here it is for the case someone needs something like this.

def toRSTtable(self,rows, header=True, vdelim="  ", padding=1, justify='right'):
        """ Outputs a list of lists as a Restructured Text Table

        - rows - list of lists
        - header - if True the first row is treated as a table header
        - vdelim - vertical delimiter betwee columns
        - padding - padding nr. of spaces are left around the longest element in the
          column
        - justify - may be left,center,right
        """
        border="=" # character for drawing the border
        justify = {'left':string.ljust,'center':string.center, 'right':string.rjust}[justify.lower()]

        # calculate column widhts (longest item in each col
        # plus "padding" nr of spaces on both sides)
        cols = zip(*rows)
        colWidths = [max([len(str(item))+2*padding for item in col]) for col in cols]

        # the horizontal border needed by rst
        borderline = vdelim.join([w*border for w in colWidths])

        # outputs table in rst format
        print borderline
        for row in rows:
            print vdelim.join([justify(str(item),width) for (item,width) in zip(row,colWidths)])
            if header: print borderline; header=False
        print borderline
George Sakkis (author) 17 years, 9 months ago  # | flag

Thanks Raymond :) I included your suggestions in v1.1

Danny G 13 years, 4 months ago  # | flag

Need to get rid of 'self' in the def if you want this to be a standalone function:

def toRSTtable(rows, header=True, vdelim="  ", padding=1, justify='right'):

also to allow for different sized row lists instead of zip(*rows) use:

cols = map(lambda *row: [elem or ' ' for elem in row], *rows)

I got this from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/410687

It will add a blank space for every missing cell.

Grzegorz 10 years, 9 months ago  # | flag

If someone like me has trouble with proper displaying values that are a multibyte string, can use unicode object instead str.

maxWidths = [max([len(unicode(item, 'utf-8')) for item in column]) for column in columns]
justify = {'center':unicode.center, 'right':unicode.rjust, 'left':unicode.ljust}[justify.lower()]
+ delim.join([justify(unicode(item, 'utf-8'), width).encode('utf-8') for (item,width) in zip(row,maxWidths)]) \
Geoff Bache 10 years, 6 months ago  # | flag

This function does not appear to work for tables with one column only.

The reason seems to be the use of map(None, *lists) which returns different types depending on how many items are in "lists". (See docs for 'map') I fixed this by creating the following function, and using it in the places where map(None, ...) is currently used.

def transpose(iterables):
    if len(iterables) > 1:
        return map(None, *iterables)
    else:
        return [ (item,) for item in iterables[0] ]
Alfonso Rodriguez 5 years, 9 months ago  # | flag

I am a beginner in scripting. Can you show me where and how you created those columns. Second question, if I wanted to write a script with a column and outputs. Will there be a way to write a script to run the first time with outputs, then follow it with the same scripts but to have no data in the column.