Skip to main content

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}