pub(crate) const MAX_COL_SPAN: u32 = 1000;
pub(crate) const MAX_ROW_SPAN: u32 = 65534;
pub(crate) struct SpanCell {
pub content: String,
pub row_span: u32,
pub col_span: u32,
}
impl SpanCell {
pub(crate) fn new(content: impl Into<String>, row_span: u32, col_span: u32) -> Self {
Self {
content: content.into(),
row_span,
col_span,
}
}
}
pub(crate) fn resolve_span_grid<C>(
rows: &[Vec<C>],
col_span: impl Fn(&C) -> u32,
row_span: impl Fn(&C) -> u32,
mut place: impl FnMut(u32, u32, &C),
) -> u32 {
let mut occupied_until: Vec<u32> = Vec::new();
for (row_idx, row) in rows.iter().enumerate() {
let row_idx = row_idx as u32;
let mut col = 0usize;
for cell in row {
while col < occupied_until.len() && occupied_until[col] > row_idx {
col += 1;
}
let end_row = row_idx.saturating_add(row_span(cell).clamp(1, MAX_ROW_SPAN));
let span = col_span(cell).clamp(1, MAX_COL_SPAN) as usize;
for c in col..col + span {
if c >= occupied_until.len() {
occupied_until.resize(c + 1, 0);
}
occupied_until[c] = end_row;
}
place(row_idx, col as u32, cell);
col += span;
}
}
occupied_until.len() as u32
}
pub(crate) fn flatten_spanned_rows(rows: &[Vec<SpanCell>]) -> Vec<Vec<String>> {
let mut placed: Vec<(u32, u32, String)> = Vec::new();
let num_cols = resolve_span_grid(
rows,
|c| c.col_span,
|c| c.row_span,
|row_idx, col, cell| placed.push((row_idx, col, cell.content.clone())),
) as usize;
let num_rows = rows.len();
let mut grid = vec![vec![String::new(); num_cols]; num_rows];
for (r, c, content) in placed {
if (r as usize) < num_rows && (c as usize) < num_cols {
grid[r as usize][c as usize] = content;
}
}
grid
}
pub(crate) fn flatten_positioned_cells(
num_rows: usize,
cells: impl Iterator<Item = (u32, u32, u32, String)>,
) -> Vec<Vec<String>> {
let mut rows: Vec<Vec<SpanCell>> = (0..num_rows.max(1)).map(|_| Vec::new()).collect();
for (row, row_span, col_span, content) in cells {
let r = row as usize;
if r >= rows.len() {
rows.resize_with(r + 1, Vec::new);
}
rows[r].push(SpanCell::new(content, row_span, col_span));
}
flatten_spanned_rows(&rows)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rowspan_reserves_column() {
let rows = vec![
vec![
SpanCell::new("Alpha", 2, 1),
SpanCell::new("Alice", 1, 1),
SpanCell::new("10", 1, 1),
],
vec![SpanCell::new("Bob", 1, 1), SpanCell::new("20", 1, 1)],
];
let grid = flatten_spanned_rows(&rows);
assert_eq!(grid[0], vec!["Alpha", "Alice", "10"]);
assert_eq!(grid[1], vec!["", "Bob", "20"]);
}
#[test]
fn colspan_widens_grid() {
let rows = vec![
vec![SpanCell::new("Fuse", 1, 2), SpanCell::new("Circuit", 1, 1)],
vec![
SpanCell::new("101", 1, 1),
SpanCell::new("40A", 1, 1),
SpanCell::new("Blower", 1, 1),
],
];
let grid = flatten_spanned_rows(&rows);
assert_eq!(grid[0], vec!["Fuse", "", "Circuit"]);
assert_eq!(grid[1], vec!["101", "40A", "Blower"]);
}
#[test]
fn colspan_three_widens_grid_exactly() {
let rows = vec![
vec![SpanCell::new("Header", 1, 3)],
vec![
SpanCell::new("a", 1, 1),
SpanCell::new("b", 1, 1),
SpanCell::new("c", 1, 1),
],
];
let grid = flatten_spanned_rows(&rows);
assert_eq!(grid[0], vec!["Header", "", ""]);
assert_eq!(grid[1], vec!["a", "b", "c"]);
}
#[test]
fn colspan_at_cap_is_unaffected() {
let rows = vec![vec![
SpanCell::new("wide", 1, MAX_COL_SPAN),
SpanCell::new("next", 1, 1),
]];
let mut placed = Vec::new();
let cols = resolve_span_grid(
&rows,
|c| c.col_span,
|c| c.row_span,
|row_idx, col, cell| placed.push((row_idx, col, cell.content.clone())),
);
assert_eq!(cols, MAX_COL_SPAN + 1);
assert_eq!(placed[1], (0, MAX_COL_SPAN, "next".to_string()));
}
#[test]
fn colspan_one_over_cap_is_clamped_to_cap() {
let rows = vec![vec![
SpanCell::new("wide", 1, MAX_COL_SPAN + 1),
SpanCell::new("next", 1, 1),
]];
let mut placed = Vec::new();
let cols = resolve_span_grid(
&rows,
|c| c.col_span,
|c| c.row_span,
|row_idx, col, cell| placed.push((row_idx, col, cell.content.clone())),
);
assert_eq!(
cols,
MAX_COL_SPAN + 1,
"one over cap must clamp down to exactly MAX_COL_SPAN columns"
);
assert_eq!(placed[1], (0, MAX_COL_SPAN, "next".to_string()));
}
#[test]
fn colspan_bomb_is_clamped_not_allocated() {
let rows = vec![vec![SpanCell::new("bomb", 1, u32::MAX), SpanCell::new("next", 1, 1)]];
let mut placed = Vec::new();
let cols = resolve_span_grid(
&rows,
|c| c.col_span,
|c| c.row_span,
|row_idx, col, cell| placed.push((row_idx, col, cell.content.clone())),
);
assert_eq!(
cols,
MAX_COL_SPAN + 1,
"bomb cell clamps to MAX_COL_SPAN columns, plus the trailing cell"
);
assert_eq!(placed[0], (0, 0, "bomb".to_string()));
assert_eq!(
placed[1],
(0, MAX_COL_SPAN, "next".to_string()),
"the following cell must land right after the clamped span, not after the raw u32::MAX request"
);
}
#[test]
fn rowspan_bomb_does_not_panic_or_overflow() {
let rows = vec![
vec![SpanCell::new("bomb", u32::MAX, 1)],
vec![SpanCell::new("still-reserved", 1, 1)],
];
let mut placed = Vec::new();
resolve_span_grid(
&rows,
|c| c.col_span,
|c| c.row_span,
|row_idx, col, cell| placed.push((row_idx, col, cell.content.clone())),
);
assert_eq!(
placed[1],
(1, 1, "still-reserved".to_string()),
"row 1's column 0 is still reserved by the clamped rowspan, so the second cell must be pushed to column 1"
);
}
#[test]
fn overflow_row_grows_grid_instead_of_clamping() {
let cells = vec![
(0u32, 1u32, 1u32, "Header".to_string()),
(2u32, 1u32, 1u32, "Orphan".to_string()),
];
let grid = flatten_positioned_cells(1, cells.into_iter());
assert_eq!(grid.len(), 3, "overflow row must grow the grid, not clamp into row 0");
assert_eq!(grid[0], vec!["Header"]);
assert_eq!(grid[1], vec![""]);
assert_eq!(grid[2], vec!["Orphan"]);
}
#[test]
fn no_spans_is_identity() {
let rows = vec![
vec![SpanCell::new("a", 1, 1), SpanCell::new("b", 1, 1)],
vec![SpanCell::new("c", 1, 1), SpanCell::new("d", 1, 1)],
];
let grid = flatten_spanned_rows(&rows);
assert_eq!(grid, vec![vec!["a", "b"], vec!["c", "d"]]);
}
}