# HG changeset patch # User J. H. Palmieri # Date 1352505965 28800 # Node ID 156d141c09bed201907b97e7836b522931a99a7b # Parent 39fe3e5b10aeccaf3b5db746f388bef1fcfbdb5b Implement a top-level 'table' function. diff --git a/doc/en/reference/misc.rst b/doc/en/reference/misc.rst --- a/doc/en/reference/misc.rst +++ b/doc/en/reference/misc.rst @@ -33,6 +33,7 @@ sage/misc/interpreter sage/misc/functional sage/misc/html + sage/misc/table sage/misc/log sage/misc/persist sage/misc/unknown diff --git a/sage/misc/all.py b/sage/misc/all.py --- a/sage/misc/all.py +++ b/sage/misc/all.py @@ -18,6 +18,8 @@ from html import html +from table import table + from sage_timeit_class import timeit from edit_module import edit, set_edit_template diff --git a/sage/misc/html.py b/sage/misc/html.py --- a/sage/misc/html.py +++ b/sage/misc/html.py @@ -153,12 +153,24 @@ def eval(self, s, globals=None, locals=None): r""" + Return an html representation for an object ``s``. + + If ``s`` has a method ``_html_()``, call that. Otherwise, call + :func:`math_parse` on ``str(s)``, evaluate any variables in + the result, and add some html preamble and postamble. + + In any case, *print* the resulting html string. This method + always *returns* an empty string. + EXAMPLES:: sage: html.eval('
')
'' """ + if hasattr(s, '_html_'): + s._html_() + return '' if globals is None: globals = {} if locals is None: @@ -227,25 +239,6 @@ - sage: html.table(["Functions $f(x)$", sin(x), cos(x)], header = True) - -
- - - - - - - - - - - - -
Functions
-
- - sage: html.table([(x,n(sin(x), digits=2)) for x in [0..3]], header = ["$x$", "$\sin(x)$"])
@@ -277,63 +270,8 @@ """ - import types - from sage.misc.all import latex - from itertools import cycle - if isinstance(x, types.GeneratorType): - x = list(x) - if isinstance(x, (list, tuple)): - rows = len(x) - if rows > 0: - # if the table has less then 100 rows, don't truncate the output in the notebook - if rows <= 100: - print "\n
\n\n" - else: - print "\n
\n
\n" - - if header is True: - header=x[0] - x = list(x[1:]) - - if header is not False: - print "" - self._table_columns(header, True) - print "" - - for row_class, row in zip(cycle(["row-a", "row-b"]), x): - print "" % row_class - self._table_columns(row, False) - print "" - print "\n
\n
\n" - - def _table_columns(self, row, header=False): - r""" - Print the items of a list as the columns of a HTML table. - - TESTS:: - - sage: html._table_columns(["a $x^2$",1, sin(x)]) - a - - - sage: html._table_columns("a", header=True) - a - """ - column_tag = "%s" if header else "%s" - from sage.plot.all import Graphics - import types - if isinstance(row, types.GeneratorType): - row = list(row) - elif not isinstance(row, (list, tuple)): - row = [row] - - for column in xrange(len(row)): - if isinstance(row[column], Graphics): - print column_tag % row[column].show(linkmode = True) - elif isinstance(row[column], str): - print column_tag % math_parse(row[column]) - else: - print column_tag % ('' % latex(row[column])) + from table import table + table(x, header_row=header)._html_() def iframe(self, url, height=400, width=800): r""" diff --git a/sage/misc/table.py b/sage/misc/table.py new file mode 100644 --- /dev/null +++ b/sage/misc/table.py @@ -0,0 +1,709 @@ +r""" +Tables + +Display a rectangular array as a table, either in plain text, LaTeX, +or html. See the documentation for :class:`table` for details and +examples. + +AUTHORS: + +- John H. Palmieri (2012-11) +""" + +from sage.structure.sage_object import SageObject +from sage.misc.cachefunc import cached_method + +class table(SageObject): + r""" + Display a rectangular array as a table, either in plain text, LaTeX, + or html. + + INPUTS: + + - ``array`` - a list of lists (or tuple of tuples, etc.), + containing the data to be displayed. + - ``header_row`` (default False) - if True, first row is highlighted. + - ``header_column`` (default False) - if True, first column is highlighted. + - ``frame`` (default False) - if True, put a box around each cell. + - ``align`` (default 'left') - the alignment of each entry: either + 'left', 'center', or 'right' + + EXAMPLES:: + + sage: array = [['a', 'b', 'c'], [100,2,3], [4,5,60]] + sage: table(array) + a b c + 100 2 3 + 4 5 60 + sage: latex(table(array)) + \begin{tabular}{lll} + a & b & c \\ + $100$ & $2$ & $3$ \\ + $4$ & $5$ & $60$ \\ + \end{tabular} + + If ``header_row`` is ``True``, then the first row is highlighted. If + ``header_column`` is ``True``, then the first column is + highlighted. If ``frame`` is ``True``, then print a box around every + "cell". :: + + sage: table(array, header_row=True) + a b c + +-----+---+----+ + 100 2 3 + 4 5 60 + sage: latex(table(array, header_row=True)) + \begin{tabular}{lll} + a & b & c \\ \hline + $100$ & $2$ & $3$ \\ + $4$ & $5$ & $60$ \\ + \end{tabular} + sage: table(array, frame=True) + +-----+---+----+ + | a | b | c | + +-----+---+----+ + | 100 | 2 | 3 | + +-----+---+----+ + | 4 | 5 | 60 | + +-----+---+----+ + sage: latex(table(array, frame=True)) + \begin{tabular}{|l|l|l|} \hline + a & b & c \\ \hline + $100$ & $2$ & $3$ \\ \hline + $4$ & $5$ & $60$ \\ \hline + \end{tabular} + sage: table(array, header_column=True, frame=True) + +-----++---+----+ + | a || b | c | + +-----++---+----+ + | 100 || 2 | 3 | + +-----++---+----+ + | 4 || 5 | 60 | + +-----++---+----+ + sage: latex(table(array, header_row=True, frame=True)) + \begin{tabular}{|l|l|l|} \hline + a & b & c \\ \hline \hline + $100$ & $2$ & $3$ \\ \hline + $4$ & $5$ & $60$ \\ \hline + \end{tabular} + sage: table(array, header_column=True) + a | b c + 100 | 2 3 + 4 | 5 60 + + The argument ``header_row`` can, instead of being ``True`` or + ``False``, be the contents of the header row, so that ``array`` + consists of the data, while ``header_row`` is the header + information. The same goes for ``header_column``. Passing lists + for both arguments simultaneously is not supported. :: + + sage: table([(x,n(sin(x), digits=2)) for x in [0..3]], header_row=["$x$", "$\sin(x)$"], frame=True) + +-----+-----------+ + | $x$ | $\sin(x)$ | + +=====+===========+ + | 0 | 0.00 | + +-----+-----------+ + | 1 | 0.84 | + +-----+-----------+ + | 2 | 0.91 | + +-----+-----------+ + | 3 | 0.14 | + +-----+-----------+ + sage: table([[x for x in [0..3]], [n(sin(x), digits=2) for x in [0..3]]], header_column=['$x$', '$\sin(x)$'], frame=True) + +-----------++------+------+------+------+ + | $x$ || 0 | 1 | 2 | 3 | + +-----------++------+------+------+------+ + | $\sin(x)$ || 0.00 | 0.84 | 0.91 | 0.14 | + +-----------++------+------+------+------+ + + In either plain text or LaTeX, entries in tables can be aligned to the + left (default), center, or right:: + + sage: table(array, align='left') + a b c + 100 2 3 + 4 5 60 + sage: table(array, align='center') + a b c + 100 2 3 + 4 5 60 + sage: table(array, align='right', frame=True) + +-----+---+----+ + | a | b | c | + +-----+---+----+ + | 100 | 2 | 3 | + +-----+---+----+ + | 4 | 5 | 60 | + +-----+---+----+ + + To print HTML, use either ``table(...)._html_()`` or ``html(table(...))``:: + + sage: html(table([["$x$", "$\sin(x)$"]] + [(x,n(sin(x), digits=2)) for x in [0..3]], header_row=True, frame=True)) + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + Note that if ``array`` is just a list or tuple, not nested, then + it is treated as a single row:: + + sage: table([1,2,3]) + 1 2 3 + + TESTS:: + + sage: TestSuite(table([["$x$", "$\sin(x)$"]] + [(x,n(sin(x), digits=2)) for x in [0..3]], header_row=True, frame=True)).run() + """ + def __init__(self, array, header_row=False, header_column=False, + frame=False, align='left'): + """ + EXAMPLES:: + + sage: table([1,2,3], frame=True) + +---+---+---+ + | 1 | 2 | 3 | + +---+---+---+ + """ + # Set options. + self._options = {} + if header_row is True: + self._options['header_row'] = True + elif header_row: + self._options['header_row'] = True + array = [header_row] + array + else: + self._options['header_row'] = False + if header_column is True: + self._options['header_column'] = True + elif header_column: + self._options['header_column'] = True + array = [(a,) + tuple(x) for (a,x) in zip(header_column, array)] + else: + self._options['header_column'] = False + + self._options['frame'] = frame + self._options['align'] = align + # Store array as a tuple. + if not isinstance(array[0], (list, tuple)): + array = (array,) + self._array = tuple(array) + + def __eq__(self, other): + """ + Two tables are equal if and only if their data arrays and + their options are the same. + + EXAMPLES:: + + sage: array = [['a', 'b', 'c'], [1,plot(sin(x)),3], [4,5,identity_matrix(2)]] + sage: T = table(array, header_row=True) + sage: T2 = table(array, header_row=True) + sage: T is T2 + False + sage: T == T2 + True + sage: T2.options(frame=True) + sage: T == T2 + False + """ + return (self._array == other._array and self.options() == other.options()) + + def options(self, **kwds): + """ + With no arguments, return the dictionary of options for this + table. With arguments, modify options. + + INPUTS: + + - ``header_row`` - if True, first row is highlighted. + - ``header_column`` - if True, first column is highlighted. + - ``frame`` - if True, put a box around each cell. + - ``align`` - the alignment of each entry: either 'left', + 'center', or 'right' + + EXAMPLES:: + + sage: T = table([['a', 'b', 'c'], [1,2,3]]) + sage: T.options()['align'], T.options()['frame'] + ('left', False) + sage: T.options(align='right', frame=True) + sage: T.options()['align'], T.options()['frame'] + ('right', True) + + Note that when first initializing a table, ``header_row`` or + ``header_column`` can be a list. In this case, during the + initialization process, the header is merged with the rest of + the data, so changing the header option later using + ``table.options(...)`` doesn't affect the contents of the + table, just whether the row or column is highlighed. When + using this :meth:`options` method, no merging of data occurs, + so here ``header_row`` and ``header_column`` should just be + ``True`` or ``False``, not a list. :: + + sage: T = table([[1,2,3], [4,5,6]], header_row=['a', 'b', 'c'], frame=True) + sage: T + +---+---+---+ + | a | b | c | + +===+===+===+ + | 1 | 2 | 3 | + +---+---+---+ + | 4 | 5 | 6 | + +---+---+---+ + sage: T.options(header_row=False) + sage: T + +---+---+---+ + | a | b | c | + +---+---+---+ + | 1 | 2 | 3 | + +---+---+---+ + | 4 | 5 | 6 | + +---+---+---+ + + If you do specify a list for ``header_row``, an error is raised:: + + sage: T.options(header_row=['x', 'y', 'z']) + Traceback (most recent call last): + ... + TypeError: header_row should be either True or False. + """ + if kwds: + for option in ['align', 'frame']: + if option in kwds: + self._options[option] = kwds[option] + for option in ['header_row', 'header_column']: + if option in kwds: + if not kwds[option]: + self._options[option] = kwds[option] + elif kwds[option] is True: + self._options[option] = kwds[option] + else: + raise TypeError("%s should be either True or False." % option) + else: + return self._options + + def transpose(self): + """ + Return a table which is the transpose of this one: + rows and columns have been interchanged. Several of the + properties of the original table are preserved: whether a + frame is present and any alignment setting. On the other hand, + header rows are converted to header columns, and vice versa. + + EXAMPLES:: + + sage: T = table([[1,2,3], [4,5,6]]) + sage: T.transpose() + 1 4 + 2 5 + 3 6 + sage: T = table([[1,2,3], [4,5,6]], header_row=['x', 'y', 'z'], frame=True) + sage: T.transpose() + +---++---+---+ + | x || 1 | 4 | + +---++---+---+ + | y || 2 | 5 | + +---++---+---+ + | z || 3 | 6 | + +---++---+---+ + """ + return table(zip(*self._array), + header_row=self._options['header_column'], + header_column=self._options['header_row'], + frame=self._options['frame'], + align=self._options['align']) + + @cached_method + def _widths(self): + """ + The maximum widths for (the string representation of) each + column. Used by the :meth:`_repr_` method. + + EXAMPLES:: + + sage: table([['a', 'bb', 'ccccc'], [10, -12, 0], [1, 2, 3]])._widths() + (2, 3, 5) + """ + nc = len(self._array[0]) + + widths = [0] * nc + for row in self._array: + w = [] + for (idx, x) in zip(range(nc), row): + w.append(max(widths[idx], len(str(x)))) + widths = w + return tuple(widths) + + def _repr_(self): + """ + String representation of a table. + + The class docstring has many examples; here is one more. + + EXAMPLES:: + + sage: table([['a', 'bb', 'ccccc'], [10, -12, 0], [1, 2, 3]], align='right') # indirect doctest + a bb ccccc + 10 -12 0 + 1 2 3 + """ + array = self._array + nc = len(array[0]) + if len(array) == 0 or nc == 0: + return "" + + frame_line = "+" + "+".join("-" * (x+2) for x in self._widths()) + "+\n" + + if self._options['header_column'] and self._options['frame']: + frame_line = "+" + frame_line[1:].replace('+', '++', 1) + + if self._options['frame']: + s = frame_line + else: + s = "" + + if self._options['header_row']: + s += self._str_table_row(array[0], header_row=True) + array = array[1:] + + for row in array: + s += self._str_table_row(row, header_row=False) + return s.strip("\n") + + def _str_table_row(self, row, header_row=False): + """ + String representation of a row of a table. Used by the + :meth:`_repr_` method. + + EXAMPLES:: + + sage: T = table([['a', 'bb', 'ccccc'], [10, -12, 0], [1, 2, 3]], align='right') + sage: T._str_table_row([1,2,3]) + ' 1 2 3\n' + sage: T._str_table_row([1,2,3], True) + ' 1 2 3\n+----+-----+-------+\n' + sage: T.options(header_column=True) + sage: T._str_table_row([1,2,3], True) + ' 1 | 2 3\n+----+-----+-------+\n' + sage: T.options(frame=True) + sage: T._str_table_row([1,2,3], False) + '| 1 || 2 | 3 |\n+----++-----+-------+\n' + """ + frame = self._options['frame'] + widths = self._widths() + frame_line = "+" + "+".join("-" * (x+2) for x in widths) + "+\n" + + align = self._options['align'] + if align == 'right': + align_char = '>' + elif align == 'center': + align_char = '^' + else: + align_char = '<' + + s = "" + if frame: + s += "| " + else: + s += " " + + if self._options['header_column']: + if frame: + frame_line = "+" + frame_line[1:].replace('+', '++', 1) + s += ("{:" + align_char + str(widths[0]) + "}").format(row[0]) + if frame: + s += " || " + else: + s += " | " + row = row[1:] + widths = widths[1:] + + for (entry, width) in zip(row, widths): + s += ("{:" + align_char + str(width) + "}").format(entry) + if frame: + s += " | " + else: + s += " " + s = s.rstrip(' ') + s += "\n" + if frame and header_row: + s += frame_line.replace('-', '=') + elif frame or header_row: + s += frame_line + return s + + def _latex_(self): + r""" + LaTeX representation of a table. + + If an entry is a Sage object, it is replaced by its LaTeX + representation, delimited by dollar signs (i.e., ``x`` is + replaced by ``$latex(x)$``). If an entry is a string, the + dollar signs are not automatically added, so tables can + include both plain text and mathematics. + + EXAMPLES:: + + sage: from sage.misc.table import table + sage: a = [[r'$\sin(x)$', '$x$', 'text'], [1,34342,3], [identity_matrix(2),5,6]] + sage: latex(table(a)) # indirect doctest + \begin{tabular}{lll} + $\sin(x)$ & $x$ & text \\ + $1$ & $34342$ & $3$ \\ + $\left(\begin{array}{rr} + 1 & 0 \\ + 0 & 1 + \end{array}\right)$ & $5$ & $6$ \\ + \end{tabular} + sage: latex(table(a, frame=True, align='center')) + \begin{tabular}{|c|c|c|} \hline + $\sin(x)$ & $x$ & text \\ \hline + $1$ & $34342$ & $3$ \\ \hline + $\left(\begin{array}{rr} + 1 & 0 \\ + 0 & 1 + \end{array}\right)$ & $5$ & $6$ \\ \hline + \end{tabular} + """ + from latex import latex, LatexExpr + import types + + array = self._array + nc = len(array[0]) + if len(array) == 0 or nc == 0: + return "" + + align_char = self._options['align'][0] # 'l', 'c', 'r' + if self._options['frame']: + frame_char = '|' + frame_str = ' \\hline' + else: + frame_char = '' + frame_str = '' + if self._options['header_column']: + head_col_char = '|' + else: + head_col_char = '' + if self._options['header_row']: + head_row_str = ' \\hline' + else: + head_row_str = '' + + # table header + s = "\\begin{tabular}{" + s += frame_char + align_char + frame_char + head_col_char + s += frame_char.join([align_char] * (nc-1)) + s += frame_char + "}" + frame_str + "\n" + # first row + s += " & ".join(LatexExpr(x) if isinstance(x, (str, LatexExpr)) + else '$' + latex(x).strip() + '$' for x in array[0]) + s += " \\\\" + frame_str + head_row_str + "\n" + # other rows + for row in array[1:]: + s += " & ".join(LatexExpr(x) if isinstance(x, (str, LatexExpr)) + else '$' + latex(x).strip() + '$' for x in row) + s += " \\\\" + frame_str + "\n" + s += "\\end{tabular}" + return s + + def _html_(self): + r""" + HTML representation of a table. + + Strings of html will be parsed for math inside dollar and + double-dollar signs. 2D graphics will be displayed in the + cells. Expressions will be latexed. + + The ``align`` option for tables is ignored in HTML + output. Specifying ``header_column=True`` may not have any + visible effect in the Sage notebook, depending on the version + of the notebook. + + EXAMPLES:: + + sage: T = table([[r'$\sin(x)$', '$x$', 'text'], [1,34342,3], [identity_matrix(2),5,6]]) + sage: T._html_() + +
+ + + + + + + + + + + + + + + + + + +
text
+
+ + + Note that calling ``html(table(...))`` has the same effect as ``table(...)._html_()`:: + + sage: T = table([["$x$", "$\sin(x)$"]] + [(x,n(sin(x), digits=2)) for x in [0..3]], header_row=True, frame=True) + sage: T + +-----+-----------+ + | $x$ | $\sin(x)$ | + +=====+===========+ + | 0 | 0.00 | + +-----+-----------+ + | 1 | 0.84 | + +-----+-----------+ + | 2 | 0.91 | + +-----+-----------+ + | 3 | 0.14 | + +-----+-----------+ + sage: html(T) + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+ + """ + import types + from itertools import cycle + array = self._array + header_row = self._options['header_row'] + if self._options['frame']: + frame = 'border="1"' + else: + frame = '' + + if len(array) > 0: + # If the table has < 100 rows, don't truncate the output in the notebook + if len(array) <= 100: + print "\n
\n\n" % frame + else: + print "\n
\n
\n" % frame + + # First row: + if header_row: + print "" + self._html_table_row(array[0], header=header_row) + print "" + array = array[1:] + + # Other rows: + for row_class, row in zip(cycle(["row-a", "row-b"]), array): + print "" % row_class + self._html_table_row(row, header=False) + print "" + print "\n
\n
\n" + + def _html_table_row(self, row, header=False): + r""" + Print the items of a list as one row of an HTML table. Used by + the :meth:`_html_` method. + + INPUTS: + + - ``row`` - a list with the same number of entries as each row + of the table. + - ``header`` (default False) - if True, treat this as a header + row, using ```` instead of ````. + + Strings get printed verbatim unless they seem to be LaTeX + code, in which case they are enclosed in a ``script`` tag + appropriate for MathJax. Sage objects are printed using their + LaTeX representations. + + EXAMPLES:: + + sage: T = table([['a', 'bb', 'ccccc'], [10, -12, 0], [1, 2, 3]]) + sage: T._html_table_row(['a', 2, '$x$']) + a + + + """ + from sage.plot.all import Graphics + from latex import latex + from html import math_parse + import types + + if isinstance(row, types.GeneratorType): + row = list(row) + elif not isinstance(row, (list, tuple)): + row = [row] + + column_tag = "%s" if header else "%s" + + if self._options['header_column']: + first_column_tag = "%s" if header else "%s" + else: + first_column_tag = column_tag + + # First entry of row: + entry = row[0] + if isinstance(entry, Graphics): + print first_column_tag % entry.show(linkmode = True) + elif isinstance(entry, str): + print first_column_tag % math_parse(entry) + else: + print first_column_tag % ('' % latex(entry)) + + # Other entries: + for column in xrange(1,len(row)): + if isinstance(row[column], Graphics): + print column_tag % row[column].show(linkmode = True) + elif isinstance(row[column], str): + print column_tag % math_parse(row[column]) + else: + print column_tag % ('' % latex(row[column]))