unparser_shared/grid.rs
1//! Where a table's cells sit on its grid.
2//!
3//! Every parser in the family records a merged cell once, on the cell that owns it: the
4//! positions it covers — the tail of a horizontal span, and the columns a vertical span occupies
5//! in the rows below — have no cell of their own. So a cell's index in its row is not its grid
6//! column once a vertical span from a row above sits to its left, and a row's cell count is not
7//! the table's width once a cell spans columns. [`place`] walks the spans to recover both — the
8//! same walk whatever the document format was, done here once.
9//!
10//! `std`-only, like the rest of the crate root.
11
12/// How many grid rows and columns a cell owns, its own position included. A span of 0 is read
13/// as 1.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub struct Span {
16 /// Rows the cell owns.
17 pub rows: u32,
18 /// Columns the cell owns.
19 pub cols: u32,
20}
21
22impl Span {
23 /// A cell owning `rows` × `cols` grid positions.
24 pub fn new(rows: u32, cols: u32) -> Self {
25 Self { rows, cols }
26 }
27
28 /// A cell owning only its own position.
29 pub fn single() -> Self {
30 Self::new(1, 1)
31 }
32}
33
34/// Where each cell of a table sits: see [`place`].
35#[derive(Debug, Clone, PartialEq, Eq, Default)]
36pub struct Placement {
37 /// The grid column each cell starts in, one `Vec` per row, parallel to the row's cells.
38 pub columns: Vec<Vec<usize>>,
39 /// How many grid columns the table occupies: the furthest any cell reaches.
40 pub width: usize,
41}
42
43/// The grid column each cell starts in, and the table's width, for rows given as their cells'
44/// spans in reading order.
45///
46/// A cell starts at the first column, from where the row has got to, that no cell merged down
47/// from a row above still covers. The width is the furthest any cell reaches — which can be
48/// further than a row's own spans add up to, since a vertical span from above pushes the row's
49/// cells rightward.
50///
51/// ```
52/// use unparser_shared::grid::{place, Span};
53///
54/// // ┌────────┬───────────┐
55/// // │ Region │ Sales │
56/// // │ ├─────┬─────┤
57/// // │ │2024 │2025 │
58/// // └────────┴─────┴─────┘
59/// let placement = place([
60/// vec![Span::new(2, 1), Span::new(1, 2)],
61/// vec![Span::single(), Span::single()],
62/// ]);
63/// assert_eq!(placement.columns, vec![vec![0, 1], vec![1, 2]]);
64/// assert_eq!(placement.width, 3);
65/// ```
66pub fn place<R, C>(rows: R) -> Placement
67where
68 R: IntoIterator<Item = C>,
69 C: IntoIterator<Item = Span>,
70{
71 // For each column, how many further rows a cell merged down from above still covers.
72 let mut covered: Vec<usize> = Vec::new();
73 let mut placement = Placement::default();
74 for row in rows {
75 let mut columns = Vec::new();
76 let mut col = 0;
77 for span in row {
78 // Pass the columns a cell from above still covers; this row uses them up once.
79 while covered.get(col).is_some_and(|&n| n > 0) {
80 covered[col] -= 1;
81 col += 1;
82 }
83 let (rows, cols) = (span.rows.max(1) as usize, span.cols.max(1) as usize);
84 columns.push(col);
85 if covered.len() < col + cols {
86 covered.resize(col + cols, 0);
87 }
88 for c in &mut covered[col..col + cols] {
89 *c = rows - 1;
90 }
91 col += cols;
92 placement.width = placement.width.max(col);
93 }
94 // Columns past the row's last cell that a cell above still covers belong to this row.
95 for c in covered.iter_mut().skip(col) {
96 *c = c.saturating_sub(1);
97 }
98 placement.columns.push(columns);
99 }
100 placement
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 fn spans(rows: &[&[(u32, u32)]]) -> Vec<Vec<Span>> {
108 rows.iter()
109 .map(|r| {
110 r.iter()
111 .map(|&(rows, cols)| Span::new(rows, cols))
112 .collect()
113 })
114 .collect()
115 }
116
117 #[test]
118 fn plain_cells_sit_at_their_index() {
119 let p = place(spans(&[&[(1, 1), (1, 1)], &[(1, 1), (1, 1)]]));
120 assert_eq!(p.columns, vec![vec![0, 1], vec![0, 1]]);
121 assert_eq!(p.width, 2);
122 }
123
124 /// A first row that merges across: its cell count is not the table's width.
125 #[test]
126 fn a_horizontal_span_in_the_first_row_widens_the_table() {
127 let p = place(spans(&[&[(2, 1), (1, 2)], &[(1, 1), (1, 1)]]));
128 assert_eq!(p.width, 3);
129 }
130
131 #[test]
132 fn a_vertical_span_pushes_the_rows_below_rightward() {
133 let p = place(spans(&[
134 &[(3, 1), (1, 1)],
135 &[(1, 1)],
136 &[(1, 1)],
137 &[(1, 1), (1, 1)],
138 ]));
139 assert_eq!(p.columns, vec![vec![0, 1], vec![1], vec![1], vec![0, 1]]);
140 }
141
142 #[test]
143 fn a_cell_merged_down_at_the_row_end_is_used_up_by_the_rows_it_covers() {
144 let p = place(spans(&[&[(1, 1), (2, 1)], &[(1, 1)], &[(1, 1), (1, 1)]]));
145 assert_eq!(p.columns, vec![vec![0, 1], vec![0], vec![0, 1]]);
146 assert_eq!(p.width, 2);
147 }
148
149 #[test]
150 fn a_block_merge_covers_rows_and_columns() {
151 let p = place(spans(&[
152 &[(2, 2), (1, 1)],
153 &[(1, 1)],
154 &[(1, 1), (1, 1), (1, 1)],
155 ]));
156 assert_eq!(p.columns, vec![vec![0, 2], vec![2], vec![0, 1, 2]]);
157 assert_eq!(p.width, 3);
158 }
159
160 #[test]
161 fn a_zero_span_is_read_as_one() {
162 let p = place(spans(&[&[(0, 0), (0, 0)]]));
163 assert_eq!(p.columns, vec![vec![0, 1]]);
164 assert_eq!(p.width, 2);
165 }
166
167 #[test]
168 fn an_empty_table_has_no_columns() {
169 assert_eq!(place(Vec::<Vec<Span>>::new()), Placement::default());
170 }
171}