File indexing completed on 2024-04-21 05:44:50

0001 # -*- coding: UTF-8 -*-
0002 
0003 """
0004 Pretty-printing of tabular data.
0005 
0006 @author: Chusslove Illich (Часлав Илић) <caslav.ilic@gmx.net>
0007 @license: GPLv3
0008 """
0009 
0010 import copy
0011 
0012 from pology.colors import ColorString, cjoin
0013 
0014 
0015 def tabulate (data, coln=None, rown=None, dfmt=None, space="  ", none="",
0016               rotated=False, colorize=False, indent="",
0017               colnra=False, rownra=False, colw=0):
0018     """
0019     Tabulate data in plain text.
0020 
0021     All data fields can have missing trailing entries. They will be set to
0022     C{None} according to table extents.
0023 
0024     Examples:
0025 
0026         >>> print T.tabulate(data=((1, 4), (2, ), (3, 6)),
0027         ...                  coln=("c1", "c2", "c3"), rown=("r1", "r2"),
0028         ...                  space="  ", none="-")
0029         -   c1  c2  c3
0030         r1   1   2   3
0031         r2   4   -   6
0032 
0033     @param data: column entries (cells) by column
0034     @type data: [[string*]*]
0035     @param coln: column names
0036     @type coln: [string*]
0037     @param rown: row names
0038     @type rown: [string*]
0039     @param dfmt: format strings per column (e.g. C{"%+.2f"} for floats)
0040     @type dfmt: [string*]
0041     @param space: fill-in for spacing between cells
0042     @type space: string
0043     @param none: fill-in for displaying empty cells (i.e. C{None}-valued)
0044     @type none: string
0045     @param rotated: whether the table should be transposed
0046     @type rotated: bool
0047     @param colorize: whether the table should have color highlighting
0048     @type colorize: bool
0049     @param indent: indent string for the whole table
0050     @type indent: string
0051     @param colnra: right align column names
0052     @type colnra: bool
0053     @param rownra: right align row names
0054     @type rownra: bool
0055     @param colw: minimal column width
0056     @type colw: integer
0057     @returns: plain text representation of the table (no trailing newline)
0058     @rtype: string/L{ColorString<colors.ColorString>}
0059     """
0060 
0061     # Make local copies, to be able to extend to table extents.
0062     _data = []
0063     for col in data:
0064         _data.append(list(col))
0065     _coln = None
0066     if coln: _coln = list(coln)
0067     _rown = None
0068     if rown: _rown = list(rown)
0069     _dfmt = None
0070     if dfmt: _dfmt = list(dfmt)
0071 
0072     # Calculate maximum row and column number.
0073     # ...look at data:
0074     nrows = 0
0075     ncols = 0
0076     for col in _data:
0077         if nrows < len(col):
0078             nrows = len(col)
0079         ncols += 1
0080     # ...look at column and row names:
0081     if _coln is not None:
0082         if ncols < len(_coln):
0083             ncols = len(_coln)
0084     if _rown is not None:
0085         if nrows < len(_rown):
0086             nrows = len(_rown)
0087 
0088     # Index offsets due to column/row names.
0089     ro = 0
0090     if _coln is not None:
0091         ro = 1
0092     co = 0
0093     if _rown is not None:
0094         co = 1
0095 
0096     # Extend all missing table fields.
0097     # ...add columns:
0098     for c in range(len(_data), ncols):
0099         _data.append([])
0100     # ...add rows:
0101     for col in _data:
0102         for r in range(len(col), nrows):
0103             col.append(None)
0104     # ...add column names:
0105     if _coln is not None:
0106         if _rown is not None:
0107             _coln.insert(0, none) # header corner
0108         for c in range(len(_coln), ncols + co):
0109             _coln.append(None)
0110     # ...add row names:
0111     if _rown is not None:
0112         if _coln is not None:
0113             _rown.insert(0, none) # header corner
0114         for r in range(len(_rown), nrows + ro):
0115             _rown.append(None)
0116     # ...add formats:
0117     if _dfmt is None:
0118         _dfmt = []
0119     if _rown is not None:
0120         _dfmt.insert(0, "%s") # header corner
0121     for c in range(len(_dfmt), ncols + co):
0122         _dfmt.append("%s")
0123 
0124     # Stringize data.
0125     # ...nice fat deep assembly of empty stringized table:
0126     sdata = [["" for i in range(nrows + ro)] for j in range(ncols + co)]
0127     # ...table body:
0128     for c in range(ncols):
0129         for r in range(nrows):
0130             if _data[c][r] is not None:
0131                 sdata[c + co][r + ro] = _dfmt[c + co] % (_data[c][r],)
0132             else:
0133                 sdata[c + co][r + ro] = none
0134     # ...column names:
0135     if _coln is not None:
0136         for c in range(ncols + co):
0137             if _coln[c] is not None:
0138                 sdata[c][0] = "%s" % (_coln[c],)
0139     # ...row names:
0140     if _rown is not None:
0141         for r in range(nrows + ro):
0142             if _rown[r] is not None:
0143                 sdata[0][r] = "%s" % (_rown[r],)
0144 
0145     # Rotate needed data for output.
0146     if rotated:
0147         _coln, _rown = _rown, _coln
0148         ncols, nrows = nrows, ncols
0149         co, ro = ro, co
0150         sdata_r = [["" for i in range(nrows + ro)] for j in range(ncols + co)]
0151         for c in range(ncols + co):
0152             for r in range(nrows + ro):
0153                 sdata_r[c][r] = sdata[r][c]
0154         sdata = sdata_r
0155 
0156     # Calculate maximum lengths per screen column.
0157     maxlen = [colw] * (ncols + co)
0158     for c in range(ncols + co):
0159         for r in range(nrows + ro):
0160             l = len(sdata[c][r])
0161             if maxlen[c] < l:
0162                 maxlen[c] = l
0163 
0164     # Reformat strings to maximum length per column.
0165     for c in range(co, ncols + co):
0166         lfmt = "%" + str(maxlen[c]) + "s"
0167         for r in range(ro, nrows + ro):
0168             sdata[c][r] = lfmt % (sdata[c][r],)
0169         # ...but column names aligned as requested:
0170         if _coln is not None:
0171             if colnra:
0172                 lfmt = "%" + str(maxlen[c]) + "s"
0173             else:
0174                 lfmt = "%-" + str(maxlen[c]) + "s"
0175             sdata[c][0] = lfmt % (sdata[c][0],)
0176             if colorize:
0177                 sdata[c][0] = ColorString("<purple>%s</purple>") % sdata[c][0]
0178     # ...but row names aligned as requested:
0179     if _rown is not None:
0180         if rownra:
0181             lfmt = "%" + str(maxlen[0]) + "s"
0182         else:
0183             lfmt = "%-" + str(maxlen[0]) + "s"
0184         for r in range(nrows + ro):
0185             sdata[0][r] = lfmt % (sdata[0][r],)
0186             if colorize:
0187                 sdata[0][r] = ColorString("<blue>%s</blue>") % sdata[0][r]
0188 
0189     # Assemble the table.
0190     lines = []
0191     for r in range(nrows + ro):
0192         cells = []
0193         for c in range(ncols + co):
0194             cells.append(sdata[c][r])
0195         lines.append(indent + cjoin(cells, space))
0196 
0197     return cjoin(lines, "\n")
0198