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

This code formats a number of columns of text into fixed widths. Each column may also be aligned independently. Whitespace is collapsed, though line breaks are retained (they may optionally be ignored).

Python, 90 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
import re

LEFT = '<'
RIGHT = '>'
CENTER = '^'

class FormatColumns:
    '''Format some columns of text with constraints on the widths of the
    columns and the alignment of the text inside the columns.
    '''
    def __init__(self, columns, contents, spacer=' | ', retain_newlines=True):
        '''
        "columns"   is a list of tuples (width in chars, alignment) where
                    alignment is one of LEFT, CENTER or RIGHT.
        "contents"  is a list of chunks of text to format into each of the
                    columns.
        '''
        assert len(columns) == len(contents), \
            'columns and contents must be same length'
        self.columns = columns
        self.num_columns = len(columns)
        self.contents = contents
        self.spacer = spacer
        self.retain_newlines = retain_newlines
        self.positions = [0]*self.num_columns

    def format_line(self, wsre=re.compile(r'\s+')):
        ''' Fill up a single row with data from the contents.
        '''
        l = []
        data = False
        for i, (width, alignment) in enumerate(self.columns):
            content = self.contents[i]
            col = ''
            while self.positions[i] < len(content):
                word = content[self.positions[i]]
                # if we hit a newline, honor it
                if '\n' in word:
                    # chomp
                    self.positions[i] += 1
                    if self.retain_newlines:
                        break
                    word = word.strip()

                # make sure this word fits
                if col and len(word) + len(col) > width:
                    break

                # no whitespace at start-of-line
                if wsre.match(word) and not col:
                    # chomp
                    self.positions[i] += 1
                    continue

                col += word
                # chomp
                self.positions[i] += 1
            if col:
                data = True
            if alignment == CENTER:
                col = '{:^{}}'.format(col.strip(), width)
            elif alignment == RIGHT:
                col = '{:>{}}'.format(col.rstrip(), width)
            else:
                col = '{:<{}}'.format(col.lstrip(), width)
            l.append(col)

        if data:
            return self.spacer.join(l).rstrip()
        # don't return a blank line
        return ''

    def format(self, splitre=re.compile(r'(\n|\r\n|\r|[ \t]|\S+)')):
        # split the text into words, spaces/tabs and newlines
        for i, content in enumerate(self.contents):
            self.contents[i] = splitre.findall(content)

        # now process line by line
        l = []
        line = self.format_line()
        while line:
            l.append(line)
            line = self.format_line()
        return '\n'.join(l)

    def __str__(self):
        return self.format()

def wrap(text, width=75, alignment=LEFT):
    return FormatColumns(((width, alignment),), [text])

Often I've needed to format columns of text going into email messages. Usually, it's to format invoices, or reports from bug trackers. The wrap() method has also been quite handy for formatting plain-text email messages where the body of the message starts out life as a string-format template (ie. "Welcome to %(url)s" etc).

This code differs from the textwrap module in the Python 2.3+ standard library in a couple of ways:

  1. it can format text into multiple colunms (that's a biggie), and
  2. it can align the text to the column center or to the right.

Example usage (note that retention of line breaks is on by default):

>>> from format_columns import *
>>> lorem1 = '''Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
... Duis nibh purus, bibendum sed, condimentum ut, bibendum ut, risus.  Fusce
... pede enim, nonummy sit amet, dapibus a, blandit eget, metus.'''
>>> lorem2 = '''Suspendisse sapien justo, egestas nec, sollicitudin in,
... blandit vel, purus: Nulla facilisi. Class aptent taciti sociosqu ad
... litora torquent per conubia nostra, per inceptos hymenaeos.'''
>>> lorem3 = 'Donec elit elit, pulvinar vitae, viverra vel, suscipit ut.'
>>> print(FormatColumns(((28, LEFT), (25, CENTER), (10, RIGHT)), [lorem1,
... lorem2, lorem3]))
Lorem ipsum dolor sit amet,  | Suspendisse sapien justo, | Donec elit
consectetuer adipiscing      | egestas nec, sollicitudin |      elit,
elit.                        |            in,            |   pulvinar
Duis nibh purus, bibendum    | blandit vel, purus: Nulla |     vitae,
sed, condimentum ut,         |  facilisi. Class aptent   |    viverra
bibendum ut, risus.  Fusce   |    taciti sociosqu ad     |       vel,
pede enim, nonummy sit amet, |    litora torquent per    |   suscipit
dapibus a, blandit eget,     |    conubia nostra, per    |        ut.
metus.                       |    inceptos hymenaeos.    |
>>>

5 comments

anthony baxter 19 years, 8 months ago  # | flag

adding this to the stdlib. Have you considered working up a patch to add this to the stdlib's textwrap module?

Peter Kleiweg 19 years, 8 months ago  # | flag

Args is about input, textwrap is about output. They have very different purposes.

Richard Jones (author) 19 years, 8 months ago  # | flag

Considered? Yes. Found the time to? No.

Tfox 19 years, 7 months ago  # | flag

Combining efforts. George Sakkis submitted a similar recipe ( http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/267662). I gussied it up a bit, added some features and more comments. I think it's quite usable now. (I didn't want to just paste the text into a comment, though). There's likely other simialr recipes as well. If there's talk of adding to the standard lib, how 'bout trying to get the best of all our efforts?

Richard Jones (author) 13 years, 2 months ago  # | flag

I've updated this code to use the format() function provided in modern Python versions. The code is all Python 3 compatible now as well.

Created by Richard Jones on Fri, 27 Aug 2004 (PSF)
Python recipes (4591)
Richard Jones's recipes (2)

Required Modules

Other Information and Tasks