Files
tableprint/tableprint.py
2017-05-25 11:40:22 -07:00

331 lines
9.2 KiB
Python

# -*- coding: utf-8 -*-
"""
Tableprint
A module to print and display formatted tables of data
Usage
-----
>>> data = np.random.randn(10, 3)
>>> headers = ['Column A', 'Column B', 'Column C']
>>> tableprint.table(data, headers)
"""
from __future__ import print_function, unicode_literals
from metadata import __version__
from six import string_types
from collections import namedtuple
from numbers import Number
import sys
import re
import numpy as np
__all__ = ('table', 'header', 'row', 'hr', 'top', 'bottom',
'banner', 'dataframe', 'humantime', 'styles')
# set up table styles
LineStyle = namedtuple('LineStyle', ('begin', 'hline', 'sep', 'end'))
TableStyle = namedtuple('TableStyle', ('top', 'below_header', 'bottom', 'row'))
styles = {
'grid': TableStyle(
top=LineStyle('+', '-', '+', '+'),
below_header=LineStyle('+', '-', '+', '+'),
bottom=LineStyle('+', '-', '+', '+'),
row=LineStyle('|', '', '|', '|'),
),
'fancy_grid': TableStyle(
top=LineStyle('', '', '', ''),
below_header=LineStyle('', '', '', ''),
bottom=LineStyle("", "", "", ""),
row=LineStyle('', '', '', ''),
),
'clean': TableStyle(
top=LineStyle(' ', '', ' ', ' '),
below_header=LineStyle(' ', '', ' ', ' '),
bottom=LineStyle(" ", "", " ", " "),
row=LineStyle(' ', '', ' ', ' '),
),
'round': TableStyle(
top=LineStyle('', '', '', ''),
below_header=LineStyle('', '', '', ''),
bottom=LineStyle('', '', '', ''),
row=LineStyle('', '', '', ''),
),
'banner': TableStyle(
top=LineStyle('', '', '', ''),
below_header=LineStyle("", "", "", ""),
bottom=LineStyle("", "", "", ""),
row=LineStyle('', '', '', ''),
),
'block': TableStyle(
top=LineStyle('', '', '', ''),
below_header=LineStyle(' ', '', '', ' '),
bottom=LineStyle('', '', '', ''),
row=LineStyle(' ', '', ' ', ' '),
),
}
STYLE = 'round'
WIDTH = 11
FMT = '5g'
def table(data, headers=None, format_spec=FMT, width=WIDTH, style=STYLE, out=sys.stdout):
"""Print a table with the given data
Parameters
----------
data : array_like
An (m x n) array containing the data to print (m rows of n columns)
headers : list, optional
A list of n strings consisting of the header of each of the n columns (Default: None)
format_spec : string, optional
Format specification for formatting numbers (Default: '5g')
width : int, optional
The width of each column in the table (Default: 11)
style : string or tuple, optional
A formatting style. (Default: 'fancy_grid')
out : writer, optional
A file handle or object that has write() and flush() methods (Default: sys.stdout)
"""
ncols = len(data[0]) if headers is None else len(headers)
tablestyle = styles[style]
# Initialize with a hr or the header
tablestr = [hr(ncols, width, tablestyle.top)] \
if headers is None else [header(headers, width, style)]
# parse each row
tablestr += [row(d, width, format_spec, style) for d in data]
# only add the final border if there was data in the table
if len(data) > 0:
tablestr += [hr(ncols, width, tablestyle.bottom)]
# print the table
out.write('\n'.join(tablestr) + '\n')
out.flush()
def header(headers, width=WIDTH, style=STYLE, add_hr=True):
"""Returns a formatted row of column header strings
Parameters
----------
headers : list of strings
A list of n strings, the column headers
width : int
The width of each column (Default: 11)
style : string or tuple, optional
A formatting style (see styles)
Returns
-------
headerstr : string
A string consisting of the full header row to print
"""
tablestyle = styles[style]
# string formatter
data = map(lambda x: ('{:^%d}' % (width + _ansi_len(x))).format(x), headers)
# build the formatted str
headerstr = _format_line(data, tablestyle.row)
if add_hr:
upper = hr(len(headers), width, tablestyle.top)
lower = hr(len(headers), width, tablestyle.below_header)
headerstr = '\n'.join([upper, headerstr, lower])
return headerstr
def row(values, width=WIDTH, format_spec=FMT, style=STYLE):
"""Returns a formatted row of data
Parameters
----------
values : array_like
An iterable array of data (numbers or strings), each value is printed in a separate column
width : int
The width of each column (Default: 11)
format_spec : string
The precision format string used to format numbers in the values array (Default: '5g')
style : namedtuple, optional
A line formatting style
Returns
-------
rowstr : string
A string consisting of the full row of data to print
"""
tablestyle = styles[style]
assert isinstance(format_spec, string_types) | (type(format_spec) is list), \
"format_spec must be a string or list of strings"
if isinstance(format_spec, string_types):
format_spec = [format_spec] * len(list(values))
# mapping function for string formatting
def mapdata(val):
# unpack
datum, prec = val
if isinstance(datum, string_types):
return ('{:>%i}' % (width + _ansi_len(datum))).format(datum)
elif isinstance(datum, Number):
return ('{:>%i.%s}' % (width, prec)).format(datum)
else:
raise ValueError('Elements in the values array must be strings, ints, or floats')
# string formatter
data = map(mapdata, zip(values, format_spec))
# build the row string
return _format_line(data, tablestyle.row)
def hr(n, width=WIDTH, linestyle=LineStyle('', '', '', '')):
"""Returns a formatted string used as a border between table rows
Parameters
----------
n : int
The number of columns in the table
width : int
The width of each column (Default: 11)
linestyle : tuple
A LineStyle namedtuple containing the characters for (begin, hr, sep, end).
(Default: ('', '', '', ''))
Returns
-------
rowstr : string
A string consisting of the row border to print
"""
hrstr = linestyle.sep.join([('{:%s^%i}' % (linestyle.hline, width)).format('')] * n)
return linestyle.begin + hrstr + linestyle.end
def top(n, width=WIDTH, style=STYLE):
"""Prints the top row of a table"""
return hr(n, width, linestyle=styles[style].top)
def bottom(n, width=WIDTH, style=STYLE):
"""Prints the top row of a table"""
return hr(n, width, linestyle=styles[style].bottom)
def banner(message, width=30, style='banner', out=sys.stdout):
"""Prints a banner message
Parameters
----------
message : string
The message to print in the banner
width : int
The minimum width of the banner (Default: 30)
style : string
A line formatting style (Default: 'banner')
out : writer
An object that has write() and flush() methods (Default: sys.stdout)
"""
out.write(header([message], max(width, len(message)), style) + '\n')
out.flush()
def dataframe(df, **kwargs):
"""Print table with data from the given pandas DataFrame
Parameters
----------
df : DataFrame
A pandas DataFrame with the table to print
"""
table(np.array(df), list(df.columns), **kwargs)
def humantime(t):
"""Converts a time in seconds to a reasonable human readable time
Parameters
----------
t : float
The number of seconds
Returns
-------
time : string
The human readable formatted value of the given time
"""
try:
t = float(t)
except (ValueError, TypeError):
raise ValueError("Input must be numeric")
# weeks
if t >= 7*60*60*24:
weeks = np.floor(t / (7.*60.*60.*24.))
timestr = "{:g} weeks, ".format(weeks) + humantime(t % (7*60*60*24))
# days
elif t >= 60*60*24:
days = np.floor(t / (60.*60.*24.))
timestr = "{:g} days, ".format(days) + humantime(t % (60*60*24))
# hours
elif t >= 60*60:
hours = np.floor(t / (60.*60.))
timestr = "{:g} hours, ".format(hours) + humantime(t % (60*60))
# minutes
elif t >= 60:
minutes = np.floor(t / 60.)
timestr = "{:g} min., ".format(minutes) + humantime(t % 60)
# seconds
elif (t >= 1) | (t == 0):
timestr = "{:g} s".format(t)
# milliseconds
elif t >= 1e-3:
timestr = "{:g} ms".format(t*1e3)
# microseconds
elif t >= 1e-6:
timestr = "{:g} \u03BCs".format(t*1e6)
# nanoseconds or smaller
else:
timestr = "{:g} ns".format(t*1e9)
return timestr
def _ansi_len(string):
"""Extra length due to any ANSI sequences in the string."""
return len(string) - len(re.compile(r'\x1b[^m]*m').sub('', string))
def _format_line(data, linestyle):
"""Formats a list of elements using the given line style"""
return linestyle.begin + linestyle.sep.join(data) + linestyle.end