"""
This module exports two classes: 'RowTable' and 'ColTable'. A 'RowTable' is a
table that is filled row by row, a 'ColTable' is filled col by
col. After all values have been entered into the table, you can get an
external representation of the table in three formats: LaTeX,
HTML and CSV. HTML and CSV are not fully supported, some operations make
only sense for LaTeX.
Example::
tab = RowTable()
tab.addColSeparator(0)
tab.addRow(1,2,3)
tab.addRowSeparator()
tab.addRow(4,5,6)
latexOutput = tab.getAsLatex()
The code above would produce a table like that:
1 | 2 3
--+-------
4 | 5 6
For use with LaTeX you should put something like this into your document:
\usepackage{dcolumn}
% the 1st arg is the decimal separator used in the .tex file,
% the 2nd arg is the decimal separator that should be used in the .dvi
% file
% the 3rd specifies the number of digits to the left and to the
% right of the decimal separator
% Remember: the .tex file may also contain grouping separators
\newcolumntype{d}[0]{D{.}{.}{3}}
@author: Stefan Wehr
@url: http://www.stefanwehr.de
@license: LGPL, http://www.gnu.org/licenses/lgpl.txt
@exports: ColTable, RowTable
@version: 2005-03-15
"""
from types import *
import locale
def formatNumber(f, ndec=3):
if type(f) not in [FloatType, IntType, LongType]: return f
tr = 9999
if f > tr:
format = '%.5E'
elif type(f) == FloatType:
format = '%%.%df' % ndec
else:
format = '%d'
s = locale.format(format, f, grouping=1)
if type(f) != FloatType or f > tr:
return s
while ndec > 1:
if s[-1] == '0':
s = s[:-1]
ndec -= 1
return s
TableError = 'TableError'
def addRowToCols(row, cols):
"""
Adds a 'row' to the 'cols' of a table. A 'row' is a list of values. The
parameter 'cols' is a list containing the cols of the table.
"""
if len(row) != len(cols):
raise TableError, 'Row has length %d, but there are %d columns' % \
(len(row), len(cols))
for (r,c) in zip(row, cols):
c.append(r)
class AbstractTable:
def __init__(self, caption=None):
self.cols = []
self.headers = []
self.rowSeparators = []
self.colSeparators = []
self.colFormats = {}
self.colTextFormats = {}
self.caption = caption
self.ndec = 3
self.withColSeparators = 0
self.defaultColFormat = 'd'
# records how much cols the cell at (row,col) occupies
self.colspan = {}
self.longtable = 0
# useless for a column based table. only needed for RowTable
self.lastHeaderRow = -1
self.label = ''
self.fontsize = None
def isLongtable(self, bool):
"""
Specifies if the latex package 'longtable' should be used for
this table.
"""
self.longtable = bool
def getColspan(self, rowIndex, colIndex):
return self.colspan.get( (rowIndex, colIndex), 1)
def setColspan(self, rowIndex, colIndex, value):
assert value >= 1
self.colspan[(rowIndex, colIndex)] = value
def getAsCsv(self, colsep=';'):
"""
Returns a string with the table in CSV (comma separated values)
format. Some information might be lost when exporting the table
to the CSV format.
"""
s = ';'.join(self.headers) + '\n'
s += self.formatBodyForCsv(colsep)
return s
def formatBodyForCsv(self, colsep):
s = ''
n = len(self.cols)
for i in range(len(self.cols[0])):
offset = 0
for j in range(n):
if j+offset >= n:
# remove the trailing colsep
s = s[:-1]
break
rowIndex = i
colIndex = j+offset
content = self.cols[colIndex][rowIndex]
colspan = self.getColspan(rowIndex, colIndex)
s += str(content) + colspan*colsep
offset += colspan-1
s += '\n'
return s
def getAsHtml(self):
"""
Returns a string with the table in HTML format. Some
information might be lost when exporting the table to HTML
format.
"""
str = '\n'
if self.caption:
str += '%s\n' % self.caption
str += '\n\n'
str += self.formatHeaderForHtml()
str += self.formatBodyForHtml()
str += '
\n\n'
return str
def formatHeaderForHtml(self):
if not self.headers: return ''
s = ''
for h in self.headers:
s += '%s | ' % h
s += '
\n'
return s
def formatBodyForHtml(self):
s = ''
n = len(self.cols)
for i in range(len(self.cols[0])):
offset = 0
s += '\n'
for j in range(n):
if j+offset >= n: break
content,colspan = self.formatCellContentForHtml(i, j+offset)
s += '%s | ' % (colspan, content)
offset += colspan-1
s += '
\n'
return s
def formatCellContentForHtml(self, rowIndex, colIndex):
content = self.cols[colIndex][rowIndex]
colspan = self.getColspan(rowIndex, colIndex)
if type(content) in [FloatType, IntType, LongType]:
s = str(formatNumber(content, ndec=self.ndec))
else:
s = content
if not s: s = ' '
return (s, colspan)
def getAsLatex(self):
"""
Returns a string with the table in LaTeX format.
"""
format = ''
for i in range(0, len(self.cols)):
format += self.colFormats.get(i, self.defaultColFormat) + \
self.getColSeparatorForLatex(i)
str = ''
if not self.longtable and self.caption:
str += '\\begin{table}\n'
if self.fontsize:
str += '{\\' + self.fontsize + '\n'
if self.longtable:
str += '\\begin{longtable}{' + format + '}\n'
else:
str += '\\begin{tabular}{' + format + '}\n'
header = self.formatHeaderForLatex()
if header: str += header + '\\hline\n'
if self.longtable: str += '\endhead'
str += self.formatBodyForLatex()
if not self.longtable:
str += '\\end{tabular}\n'
if self.caption:
str += '\\caption{'
if self.label:
str += '\\label{%s}' % self.label
str += self.caption + '}\n'
if self.longtable:
str += '\\end{longtable}\n'
if self.fontsize:
str += '}\n'
if not self.longtable and self.caption:
str += '\\end{table}\n'
return str
def getColSeparatorForLatex(self, colIndex):
if self.withColSeparators or colIndex in self.colSeparators: return '|'
else: return ''
def formatHeaderForLatex(self):
colsep = ' & '
linesep = ' \\\\\n'
if not self.headers: return ''
import string
hs = []
for i in range(0, len(self.headers)):
s, colspan = self.formatCellContentForLatex2(self.headers[i], i)
hs.append(s)
return string.join(hs, colsep) + linesep
def formatCellContentForLatex(self, rowIndex, colIndex):
content = self.cols[colIndex][rowIndex]
colspan = self.getColspan(rowIndex, colIndex)
return self.formatCellContentForLatex2(content, colIndex, colspan)
def formatCellContentForLatex2(self, content, colIndex, colspan=1):
if type(content) in [FloatType, IntType, LongType]:
s = str(formatNumber(content, ndec=self.ndec))
if colspan > 1:
s = '\\multicolumn{%d}{%s}{%s}' % \
(colspan, self.defaultColFormat, s)
else:
format = self.colTextFormats.get(colIndex, 'c') + \
self.getColSeparatorForLatex(colIndex+colspan-1)
s = '\\multicolumn{%d}{%s}{%s}' % (colspan, format, content)
return (s, colspan)
def formatBodyForLatex(self):
colsep = ' & '
linesep = ' \\\\\n'
s = ''
n = len(self.cols)
m = len(self.cols[0])
for i in range(m):
offset = 0
for j in range(n):
if j+offset >= n: break
content,colspan = self.formatCellContentForLatex(i, j+offset)
s += content
offset += colspan-1
if j+offset < n-1:
s += colsep
if i < m-1:
s += linesep
if i in self.rowSeparators:
if i == m-1: s += linesep
s += '\\hline\n'
if i == self.lastHeaderRow and self.longtable:
s += '\endhead\n'
return s
class ColTable(AbstractTable):
def __init__(self, caption=None):
"""
Creates a new 'ColTable' with an optional 'caption'.
A 'ColTable' is a table that is constructed by adding cols to the table,
i.e. the table is built col by col.
"""
AbstractTable.__init__(self, caption)
def addCol(self, col, header=''):
"""
Adds a new 'col' to the table. 'header' is the header of the 'col'.
The first col of the table determines the length of all other cols.
If the new col is not the first col and its length is the same as the
the first col's length, a 'TableError' is raised.
"""
if self.cols and len(col) != len(self.cols[0]):
msg = 'Illegal length for new column.\n'
msg += 'New column has length %d, but reference ' \
'column has length %d.\n' % (len(col), len(self.cols[0]))
msg += header + ': ' + str(col)
raise TableError, msg
self.cols.append(col)
self.headers.append(header)
def addRowSeparator(self, afterRow):
"""
Adds a separator after row 'afterRow'. The first row has index 0.
"""
self.rowSeparators.append(afterRow)
def addColSeparator(self, afterCol=-1):
"""
Adds a separator after col 'afterCol'.
The first col has index 0.
If 'afterCol' is negative, a separator is inserted after the most
recently added col.
"""
if afterCol < 0:
if (len(self.cols) == 0):
raise 'Illegal state: no cols have been added, ' \
'no afterCol is given.'
afterCol = len(self.cols)-1
self.colSeparators.append(afterCol)
def setColFormat(self, type, colNo=-1):
"""
Sets the (latex) format for column at index colNo (first col has
index 0). If 'colNo' is negative, the format for the most
recently added col is set. The format only applies to cells
with numerical content. Use 'setColTextFormat' for setting the
format of cells with textual content.
"""
if colNo < 0:
if (len(self.cols) == 0):
raise 'Illegal state: no cols have been added, no colNo '\
'is given.'
colNo = len(self.cols)-1
self.colFormats[colNo] = type
def setColTextFormat(self, type, colNo=-1):
"""
Sets the (latex) format for column at index colNo (first col has
index 0). If 'colNo' is negative, the format for the most
recently added col is set. The format only applies to cells
with textual content. Use 'setColTextFormat' for setting the
format of cells with numerical content.
"""
if colNo < 0:
if (len(self.cols) == 0):
raise 'Illegal state: no cols have been added, no colNo '\
'is given.'
colNo = len(self.cols)-1
self.colTextFormats[colNo] = type
class RowTable(AbstractTable):
def __init__(self, caption=None):
"""
Creates a new 'RowTable' with an optional 'caption'.
A 'RowTable' is a table that is constructed by adding rows to the
table, i.e. the table is built row by row.
"""
AbstractTable.__init__(self, caption)
def addRow(self, row):
"""
'row' is a list that contains the values for one row. If
there are too many values in the list (i.e. a row with
less cells than this has been added before) a exception is
raised. If there are not enough values the rest is filled with
the empty string.
A value can occupy several cells of a row. You can specify the
number of cells a value occupies by inserting the tuple
(colspan, value) into the row-list. 'colspan' is the number
of cells 'value' occupies.
Example::
addRow(['first', (2, 'hello world!'), 'fourth'])
inserts a row that is 4 cells wide into the table. The first
cell contains the value 'first', the second and third cell
together contain the value 'hello world!' and the fourth cell
contains the value 'fourth'.
"""
t = self
# check for cells that have a colspan value associated with,
# replace the (colspan, value) tuples with the value and
# insert dummy values as necessary. Example: ['first', (2,
# 'hello world!'), 'forth'] --> ['first', 'hello world!',
# None, 'forth']
rowIndex = 0
if t.cols: rowIndex = len(t.cols[0])
tmpRow = []
colIndex = 0
for c in row:
if type(c) == TupleType:
colspan = c[0]
content = c[1]
t.setColspan(rowIndex, colIndex, colspan)
tmpRow.append(content)
tmpRow += (colspan-1)*[None]
colIndex += colspan
else:
tmpRow.append(c)
colIndex += 1
row = tmpRow
if not t.cols:
for cell in row:
t.cols.append([cell])
else:
refLen = len(t.cols)
n = len(row)
if n > refLen:
raise 'Row is two long. First row has length %d, this row ' \
'has length %d' % (refLen, n)
elif n < refLen:
row = row + (refLen-n)*['']
addRowToCols(row, t.cols)
def addRowSeparator(self, afterRow=-1):
"""
Adds a separator after row 'afterRow'. The first row has index 0.
If 'afterRow' is negative, a separator is inserted after the most
recently added row.
"""
if afterRow < 0:
if (len(self.cols) == 0):
raise 'Illegal state: no cols have been added, no afterCol ' \
'is given.'
afterRow = len(self.cols[0])-1
self.rowSeparators.append(afterRow)
def addColSeparator(self, afterCol):
"""
Adds a separator after col 'afterCol'. The first col has index 0.
"""
self.colSeparators.append(afterCol)
def setColFormat(self, type, colNo=-1):
"""
Sets the (latex) format for column at index colNo (first col
has index 0). The format only applies to cells with numerical
content. Use 'setColTextFormat' for setting the format of
cells with textual content.
"""
self.colFormats[colNo] = type
def setColTextFormat(self, type, colNo=-1):
"""
Sets the (latex) format for column at index colNo (first col
has index 0). The format only applies to cells with textual
content. Use 'setColTextFormat' for setting the format of
cells with numerical content.
"""
self.colTextFormats[colNo] = type
def setLastHeaderRow(self, index=-1):
"""
Marks the 'index'-th row as the last header row. If 'index < 0',
the most recently added row is marked as the last header row
(in case no row has been added, an exception is raised).
"""
if index < 0:
if (len(self.cols) == 0):
raise 'Illegal state: no cols have been added, no index ' \
'is given.'
index = len(self.cols[0])-1
self.lastHeaderRow = index
def _setupTestTable():
t = RowTable()
t.addRow(['Col 1', 'Col 2', 'Col 3'])
t.setLastHeaderRow()
t.addRow([1,2,3])
t.addRow([(2, '12'), 3])
t.addRow([1, (2, '23')])
t.addRow([(3, '123')])
return t
if __name__ == '__main__':
t = _setupTestTable()
print t.getAsCsv()