Table

Description

A Table is a data structure used to represent Office Open XML tables, CALS tables or HTML tables.

A Table is a Styled object, so you can attach a dictionary of styles and a nature (“body” by default). The nature is used to give a default value to the the row/column views.

>>> from benker.table import Table

>>> Table(styles={'frame': 'all'})
<Table({'frame': 'all'}, None)>

A table can be initialize with a collection of cells. Make sure all cells are disjoints.

>>> from benker.cell import Cell

>>> red = Cell('red', x=1, y=1, height=2)
>>> pink = Cell('pink', x=2, y=1, width=2)
>>> blue = Cell('blue', x=2, y=2)

>>> table = Table([red, pink, blue], nature='header')
>>> table
<Table({}, 'header')>

>>> print(table)
+-----------+-----------------------+
|    red    |   pink                |
|           +-----------+-----------+
|           |   blue    |           |
+-----------+-----------+-----------+

Warning

Make sure all cells are disjoints:

>>> red = Cell('overlap', x=1, y=1, width=2)
>>> pink = Cell('oops!', x=2, y=1)
>>> Table([red, pink])
Traceback (most recent call last):
    ...
KeyError: Coord(x=2, y=1)

Properties

You can use the following properties to extract information from a table:

The bounding box of a table is the bounding box of all cells in the grid:

>>> red = Cell('red', x=1, y=1, height=2)
>>> pink = Cell('pink', x=2, y=1, width=2)
>>> blue = Cell('blue', x=2, y=2)
>>> table = Table([red, pink, blue])

>>> table.bounding_box
Box(min=Coord(x=1, y=1), max=Coord(x=3, y=2))

Important

The bounding box is not defined for an empty table, so None is returned in that case (this behavior is preferable to raising an exception, in order to simplify interactive debugging).

>>> table = Table()
>>> table.bounding_box is None
True

Operations

Cells Insertion

You can insert a row to a table. This row is then used to insert cells.

>>> table = Table()

>>> row = table.rows[1]
>>> row.nature = "header"
>>> row.insert_cell("Astronomer", width=2)
>>> row.insert_cell("Year")
>>> row.insert_cell("Country")

>>> row = table.rows[2]
>>> row.insert_cell("Nicolaus")
>>> row.insert_cell("Copernicus")
>>> row.insert_cell("1473-1543")
>>> row.insert_cell("Royal Prussia")

>>> row = table.rows[3]
>>> row.insert_cell("Charles")
>>> row.insert_cell("Messier")
>>> row.insert_cell("1730-1817")
>>> row.insert_cell("France", height=2)

>>> row = table.rows[4]
>>> row.insert_cell("Jean-Baptiste")
>>> row.insert_cell("Delambre")
>>> row.insert_cell("1749-1822")

>>> print(table)
+-----------------------+-----------+-----------+
| Astronome             |   Year    |  Country  |
+-----------+-----------+-----------+-----------+
| Nicolaus  | Copernicu | 1473-1543 | Royal Pru |
+-----------+-----------+-----------+-----------+
|  Charles  |  Messier  | 1730-1817 |  France   |
+-----------+-----------+-----------|           |
| Jean-Bapt | Delambre  | 1749-1822 |           |
+-----------+-----------+-----------+-----------+

The nature of a cell is inherited from its parent’s row. The first row contains the header, so the cell nature is “header”:

>>> table.rows[1].nature
'header'
>>> [cell.nature for cell in table.rows[1].owned_cells]
['header', 'header', 'header']

The other rows have no nature, so the cell nature is None

>>> table.rows[2].nature is None
True
>>> all(cell.nature is None for cell in table.rows[2].owned_cells)
True

Cells Merging

You can merge cells by giving the coordinates of the cells to merge or by extending the size of a given cell.

>>> table = Table()
>>> letters = "abcdEFGHijklMNOP"
>>> for index, letter in enumerate(letters):
...     table[(1 + index % 4, 1 + index // 4)] = Cell(letter)
>>> print(table)
+-----------+-----------+-----------+-----------+
|     a     |     b     |     c     |     d     |
+-----------+-----------+-----------+-----------+
|     E     |     F     |     G     |     H     |
+-----------+-----------+-----------+-----------+
|     i     |     j     |     k     |     l     |
+-----------+-----------+-----------+-----------+
|     M     |     N     |     O     |     P     |
+-----------+-----------+-----------+-----------+

>>> table.merge((2, 2), (3, 3))
>>> print(table)
+-----------+-----------+-----------+-----------+
|     a     |     b     |     c     |     d     |
+-----------+-----------------------+-----------+
|     E     |   FGjk                |     H     |
+-----------|                       +-----------+
|     i     |                       |     l     |
+-----------+-----------+-----------+-----------+
|     M     |     N     |     O     |     P     |
+-----------+-----------+-----------+-----------+

>>> table.expand((2, 3), height=1)
>>> print(table)
+-----------+-----------+-----------+-----------+
|     a     |     b     |     c     |     d     |
+-----------+-----------------------+-----------+
|     E     |                       |     H     |
+-----------|                       +-----------+
|     i     |  FGjkNO               |     l     |
+-----------|                       +-----------+
|     M     |                       |     P     |
+-----------+-----------------------+-----------+

Owned and caught cells

When a cell is merged into a row group, it is always bound to the top row of this group (the first row). In that case, we say that the first row owns the cell and the other rows catch the cell.

>>> table = Table()

>>> row = table.rows[1]
>>> row.insert_cell("merged", height=2)
>>> row.insert_cell("A")

>>> row = table.rows[2]
>>> row.insert_cell("B")

>>> row = table.rows[3]
>>> row.insert_cell("C")
>>> row.insert_cell("D")
>>> print(table)
+-----------+-----------+
|  merged   |     A     |
|           +-----------+
|           |     B     |
+-----------+-----------+
|     C     |     D     |
+-----------+-----------+

Here are the owned_cells of this table:

>>> for pos, row in enumerate(table.rows, 1):
...     cells = ", ".join("{}".format(cell) for cell in row.owned_cells)
...     print("row #{pos}: {cells}".format(pos=pos, cells=cells))
row #1: merged, A
row #2: B
row #3: C, D

Here are the caught_cells of this table:

>>> for pos, row in enumerate(table.rows, 1):
...     cells = ", ".join("{}".format(cell) for cell in row.caught_cells)
...     print("row #{pos}: {cells}".format(pos=pos, cells=cells))
row #1: merged, A
row #2: merged, B
row #3: C, D

The same applies to columns: if a cell is merged into several columns then it belongs to the first column (left) of the merged column group.

>>> table = Table()

>>> row = table.rows[1]
>>> row.insert_cell("merged", width=2)
>>> row.insert_cell("A")

>>> row = table.rows[2]
>>> row.insert_cell("B")
>>> row.insert_cell("C")
>>> row.insert_cell("D")
>>> print(table)
+-----------------------+-----------+
|  merged               |     A     |
+-----------+-----------+-----------+
|     B     |     C     |     D     |
+-----------+-----------+-----------+

Here are the owned_cells of this table:

>>> for pos, col in enumerate(table.cols, 1):
...     cells = ", ".join("{}".format(cell) for cell in col.owned_cells)
...     print("col #{pos}: {cells}".format(pos=pos, cells=cells))
col #1: merged, merged, B
col #2: C
col #3: A, D

Here are the caught_cells of this table:

>>> for pos, col in enumerate(table.cols, 1):
...     cells = ", ".join("{}".format(cell) for cell in col.caught_cells)
...     print("col #{pos}: {cells}".format(pos=pos, cells=cells))
col #1: merged, merged, B
col #2: merged, merged, C
col #3: A, D