Grid

Description

A Grid is a collection of Cell objects ordered in a grid of rows and columns.

You can define a empty grid like this:

>>> from benker.grid import Grid

>>> Grid()
Grid([])

You can also define a grid from a collection (list, set…) of cells. Cells are ordered according to the total ordering of the cell boxes:

>>> 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)

>>> grid = Grid([red, blue, pink])
>>> for cell in grid:
...     print(cell)
red
pink
blue

Warning

If at least one cell intersect another one, an exception is raised:

>>> Grid([Cell("one"), Cell("two")])
Traceback (most recent call last):
    ...
KeyError: Coord(x=1, y=1)

So, it is important to define the coordinates of the cells.

It’s easy to copy the cells of another grid.

Remember that:

  • cells are copied (not shared between grids),
  • cell contents are shared: two different cells share the same content,
  • cell styles are copied (but not deeply).
>>> grid1 = Grid([red, blue, pink])
>>> grid2 = Grid(grid1)

>>> tuple(id(cell) for cell in grid1) != tuple(id(cell) for cell in grid2)
True
>>> tuple(id(cell.content) for cell in grid1) == tuple(id(cell.content) for cell in grid2)
True
>>> tuple(id(cell.styles) for cell in grid1) != tuple(id(cell.styles) for cell in grid2)
True

You can pretty print a grid:

>>> grid = Grid([red, blue, pink])
>>> print(grid)
+-----------+-----------------------+
|    red    |   pink                |
|           +-----------+-----------+
|           |   blue    |           |
+-----------+-----------+-----------+

Properties

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

>>> grid = Grid()
>>> grid[1, 1] = Cell("red", height=2)
>>> grid[2, 1] = Cell("pink", width=2)
>>> grid[3, 2] = Cell("gray")
>>> print(grid)
+-----------+-----------------------+
|    red    |   pink                |
|           +-----------+-----------+
|           |           |   gray    |
+-----------+-----------+-----------+

>>> grid.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 grid, so None is returned in that case (this behavior is preferable to raising an exception, in order to simplify interactive debugging).

>>> grid = Grid()
>>> grid.bounding_box is None
True

Operations

Contains

You can check if a point, defined by its coordinates (tuple (x, y) or Coord instance), is contained in a Grid.

The rule is simple: a grid contains a point if it exists a Cell of the grid which contains that point. In other words, a point may be contained in the bounding box of a grid but not in any cell if there are some gaps in the grid.

>>> from benker.coord import Coord

>>> 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)
>>> grid = Grid([red, blue, pink])

>>> (1, 1) in grid
True
>>> (3, 1) in grid
True
>>> (4, 1) in grid
False
>>> (3, 2) in grid
False

>>> Coord(1, 2) in grid
True

Set, Get, Delete cells

A grid is a MutableMapping, it works like a dictionary of cells. Keys of the dictionary are coordinates (tuple (x, y) or Coord instance). The coordinates are the top-left coordinates of the cells.

>>> grid = Grid()
>>> grid[1, 1] = Cell("red", height=2)
>>> grid[2, 1] = Cell("pink", width=2)
>>> grid[2, 2] = Cell("blue")
>>> grid[3, 2] = Cell("gray")

>>> print(grid)
+-----------+-----------------------+
|    red    |   pink                |
|           +-----------+-----------+
|           |   blue    |   gray    |
+-----------+-----------+-----------+

Warning

Unlike a dict, you cannot set a cell to a given location if a cell already exist in that location, an exception is raised in that case.

>>> grid[3, 1] = Cell("purple")
Traceback (most recent call last):
    ...
KeyError: Coord(x=3, y=1)

You can get a cell at a given location:

>>> grid[1, 1]
<Cell('red', styles={}, nature=None, x=1, y=1, width=1, height=2)>
>>> grid[3, 1]
<Cell('pink', styles={}, nature=None, x=2, y=1, width=2, height=1)>

You can delete a cell at a given location:

>>> del grid[3, 1]
>>> print(grid)
+-----------+-----------+-----------+
|    red    |           |           |
|           +-----------+-----------+
|           |   blue    |   gray    |
+-----------+-----------+-----------+

Merging/expanding

It is possible to merge several cells in the grid. The merging takes the start coordinates and the end coordinates of the cells to merge.

We can define a content_appender to give the content merging operation to use to merge several cell contents.

>>> grid = Grid()
>>> grid[1, 1] = Cell("red", height=2)
>>> grid[2, 1] = Cell("pink")
>>> grid[3, 1] = Cell("blue")
>>> print(grid)
+-----------+-----------+-----------+
|    red    |   pink    |   blue    |
|           +-----------+-----------+
|           |           |           |
+-----------+-----------+-----------+

>>> grid.merge((2, 1), (3, 1), content_appender=lambda a, b: "/".join([a, b]))
<Cell('pink/blue', styles={}, nature=None, x=2, y=1, width=2, height=1)>
>>> print(grid)
+-----------+-----------------------+
|    red    | pink/blue             |
|           +-----------+-----------+
|           |           |           |
+-----------+-----------+-----------+

Warning

All cells in the bounding box of the merging must be inside of the bounding box. In other words, the bounding box of the merging must not intersect any cell in the grid.

>>> grid.merge((1, 2), (2, 2))
Traceback (most recent call last):
  ...
ValueError: ((1, 2), (2, 2))

Similar to the merging, you can expand the size of a cell;

>>> grid = Grid()
>>> grid[1, 1] = Cell("red", height=2)
>>> grid[2, 1] = Cell("pink")
>>> grid[3, 1] = Cell("blue")
>>> print(grid)
+-----------+-----------+-----------+
|    red    |   pink    |   blue    |
|           +-----------+-----------+
|           |           |           |
+-----------+-----------+-----------+

>>> grid.expand((2, 1), height=1)
<Cell('pink', styles={}, nature=None, x=2, y=1, width=1, height=2)>
>>> print(grid)
+-----------+-----------+-----------+
|    red    |   pink    |   blue    |
|           |           +-----------+
|           |           |           |
+-----------+-----------+-----------+

Iterators

You can iterate the cells of a grid:

>>> grid = Grid()
>>> grid[1, 1] = Cell("red", height=2)
>>> grid[2, 1] = Cell("hot", width=2)
>>> grid[2, 2] = Cell("chili")
>>> grid[3, 2] = Cell("peppers")
>>> grid[1, 3] = Cell("Californication", width=3)

>>> print(grid)
+-----------+-----------------------+
|    red    |    hot                |
|           +-----------+-----------+
|           |   chili   |  peppers  |
+-----------------------------------+
|             Californi             |
+-----------------------------------+

>>> for cell in grid:
...     print(cell)
red
hot
chili
peppers
Californication

You can iterate over the grid rows with the method iter_rows(). Each row is a tuple of cells:

>>> for row in grid.iter_rows():
...     print(" / ".join(cell.content for cell in row))
red / hot
chili / peppers
Californication