##
Default result

```
#let dat = csv("test.csv")
#repr(dat)
#set table(align: center + horizon)
#table(columns: (3cm,) * 3, ..dat.flatten())
```

First attempt:

```
#let dat = csv("test.csv")
#set table(align: center + horizon)
#{
// show table.cell.where(y: 0, x: 1): set table.cell(colspan: 2)
// show table.cell.where(y: 2, x: 0): set table.cell(rowspan: 2)
let colspan-cells = ((0, 1),) // (y, x)
let rowspan-cells = ((2, 0),) // (y, x)
let data = dat
for ((y, x)) in colspan-cells {
data.at(y) = {
let row = data.at(y)
let _ = row.remove(x + 1)
row
}
}
for ((y, x)) in rowspan-cells {
data.at(y + 1) = {
let row = data.at(y + 1)
let _ = row.remove(x)
row
}
}
let bonus-coloring = (fill: green.transparentize(70%))
table(columns: (3cm,) * 3, ..data
.enumerate()
.map(((y, row)) => row.enumerate().map(((x, cell)) =>
if x == 1 and y == 0 {
table.cell(colspan: 2, cell, ..bonus-coloring)
} else if x == 0 and y == 2 {
table.cell(rowspan: 2, cell, ..bonus-coloring)
} else {
cell
}))
.flatten())
}
```

Second attempt:

```
#let dat = csv("test.csv")
#set table(align: center + horizon)
/// Removes elements which will be overwritten by a `colspan`/`rowspan`.
/// * `data` a 2D array of future cell content elements
/// * `colspan-cells` an array of tuples `(y, x, length)` describing each colspan cell
/// * `rowspan-cells` an array of tuples `(y, x, length)` describing each rowspan cell
/// Returns a prepared 2D `array`.
///
/// Note that length of each row can differ.
#let prepare-data(data, colspan-cells, rowspan-cells) = {
for ((y, x, length)) in colspan-cells {
data.at(y) = {
let row = data.at(y)
for i in range(length - 1) {
assert(
(x + 1) < row.len(),
message: "Column span out of bounds: " + repr((y, x, length)),
)
let _ = row.remove(x + 1)
}
row
}
}
for ((y, x, length)) in rowspan-cells {
for i in range(length - 1) {
data.at(y + 1 + i) = {
assert(
(y + 1 + i) < data.len(),
message: "Row span out of bounds: " + repr((y, x, length)),
)
let row = data.at(y + 1 + i)
let _ = row.remove(x)
row
}
}
}
data
}
/// Adds `colspan`/`rowspan` field to specified cells.
/// * `cells` a 2D array of cells (content)
/// * `colspan-cells` an array of tuples `(y, x, length)` describing each colspan cell
/// * `rowspan-cells` an array of tuples `(y, x, length)` describing each rowspan cell
/// Returns flatten `array` of patched cells.
///
/// Note that this is not tested for when cell already wrapped in `cell()`.
#let patch-span-cells(cells, colspan-cells, rowspan-cells) = {
let fill = (fill: green.transparentize(70%))
for ((y, x, length)) in colspan-cells {
cells.at(y).at(x) = table.cell(colspan: length, cells.at(y).at(x), ..fill)
}
for ((y, x, length)) in rowspan-cells {
cells.at(y).at(x) = table.cell(rowspan: length, cells.at(y).at(x), ..fill)
}
cells.flatten()
}
#{
// show table.cell.where(y: 0, x: 1): set table.cell(colspan: 2)
// show table.cell.where(y: 2, x: 0): set table.cell(rowspan: 2)
let colspan-cells = ((0, 1, 2),) // (y, x, length), min_length = 2 (no-op for "< 2")
let rowspan-cells = ((2, 0, 2),) // (y, x, length), min_length = 2 (no-op for "< 2")
let data = prepare-data(dat, colspan-cells, rowspan-cells)
table(
columns: (3cm,) * 3,
..patch-span-cells(data, colspan-cells, rowspan-cells),
)
}
```

You can merge or rewrite the 2 helper functions however you see fit. If you donâ€™t need to do anything in between, then you donâ€™t really need two of them. But they can also be easily merged by making a 3rd wrapper function:

```
#let patch-spans(data, colspan-cells, rowspan-cells) = {
patch-span-cells(
prepare-data(dat, colspan-cells, rowspan-cells),
colspan-cells,
rowspan-cells,
)
}
#table(
// A small bonus (can only use (the initial) unmodified
// 2D array here).
columns: dat.at(0).len(),
..patch-spans(dat, colspan-cells, rowspan-cells),
)
```