1#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct MergeRegion {
9 pub row: usize,
10 pub col: usize,
11 pub row_span: usize,
12 pub col_span: usize,
13}
14
15impl MergeRegion {
16 pub fn new(row: usize, col: usize, row_span: usize, col_span: usize) -> Result<Self, String> {
18 if row_span == 0 || col_span == 0 {
19 return Err("Span must be at least 1".to_string());
20 }
21 Ok(Self { row, col, row_span, col_span })
22 }
23
24 pub fn last_row(&self) -> usize {
26 self.row + self.row_span - 1
27 }
28
29 pub fn last_col(&self) -> usize {
31 self.col + self.col_span - 1
32 }
33
34 pub fn contains(&self, r: usize, c: usize) -> bool {
36 r >= self.row && r <= self.last_row() && c >= self.col && c <= self.last_col()
37 }
38
39 pub fn is_anchor(&self, r: usize, c: usize) -> bool {
41 r == self.row && c == self.col
42 }
43
44 pub fn overlaps(&self, other: &MergeRegion) -> bool {
46 self.row <= other.last_row()
47 && other.row <= self.last_row()
48 && self.col <= other.last_col()
49 && other.col <= self.last_col()
50 }
51}
52
53#[derive(Clone, Debug, PartialEq, Eq)]
55pub enum CellMergeState {
56 Normal,
58 Anchor { row_span: usize, col_span: usize },
60 HMerge,
62 VMerge,
64}
65
66impl CellMergeState {
67 pub fn to_xml_attrs(&self) -> String {
69 match self {
70 CellMergeState::Normal => String::new(),
71 CellMergeState::Anchor { row_span, col_span } => {
72 let mut attrs = String::new();
73 if *col_span > 1 {
74 attrs.push_str(&format!(r#" gridSpan="{}""#, col_span));
75 }
76 if *row_span > 1 {
77 attrs.push_str(&format!(r#" rowSpan="{}""#, row_span));
78 }
79 attrs
80 }
81 CellMergeState::HMerge => r#" hMerge="1""#.to_string(),
82 CellMergeState::VMerge => r#" vMerge="1""#.to_string(),
83 }
84 }
85
86 pub fn is_merged_away(&self) -> bool {
87 matches!(self, CellMergeState::HMerge | CellMergeState::VMerge)
88 }
89}
90
91#[derive(Clone, Debug, Default)]
93pub struct TableMergeMap {
94 regions: Vec<MergeRegion>,
95 rows: usize,
96 cols: usize,
97}
98
99impl TableMergeMap {
100 pub fn new(rows: usize, cols: usize) -> Self {
102 Self { regions: Vec::new(), rows, cols }
103 }
104
105 pub fn add_merge(&mut self, region: MergeRegion) -> Result<(), String> {
107 if region.last_row() >= self.rows {
108 return Err(format!(
109 "Merge region row {}-{} exceeds table rows {}",
110 region.row, region.last_row(), self.rows
111 ));
112 }
113 if region.last_col() >= self.cols {
114 return Err(format!(
115 "Merge region col {}-{} exceeds table cols {}",
116 region.col, region.last_col(), self.cols
117 ));
118 }
119 for existing in &self.regions {
120 if existing.overlaps(®ion) {
121 return Err(format!(
122 "Merge region ({},{}) {}x{} overlaps with ({},{}) {}x{}",
123 region.row, region.col, region.row_span, region.col_span,
124 existing.row, existing.col, existing.row_span, existing.col_span,
125 ));
126 }
127 }
128 self.regions.push(region);
129 Ok(())
130 }
131
132 pub fn merge_cells(&mut self, row: usize, col: usize, row_span: usize, col_span: usize) -> Result<(), String> {
134 let region = MergeRegion::new(row, col, row_span, col_span)?;
135 self.add_merge(region)
136 }
137
138 pub fn cell_state(&self, r: usize, c: usize) -> CellMergeState {
140 for region in &self.regions {
141 if region.is_anchor(r, c) {
142 return CellMergeState::Anchor {
143 row_span: region.row_span,
144 col_span: region.col_span,
145 };
146 }
147 if region.contains(r, c) {
148 if r == region.row {
150 return CellMergeState::HMerge;
151 }
152 return CellMergeState::VMerge;
154 }
155 }
156 CellMergeState::Normal
157 }
158
159 pub fn regions(&self) -> &[MergeRegion] {
161 &self.regions
162 }
163
164 pub fn len(&self) -> usize {
166 self.regions.len()
167 }
168
169 pub fn is_empty(&self) -> bool {
170 self.regions.is_empty()
171 }
172
173 pub fn dimensions(&self) -> (usize, usize) {
175 (self.rows, self.cols)
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn test_merge_region_new() {
185 let r = MergeRegion::new(0, 0, 2, 3).unwrap();
186 assert_eq!(r.row, 0);
187 assert_eq!(r.col, 0);
188 assert_eq!(r.row_span, 2);
189 assert_eq!(r.col_span, 3);
190 }
191
192 #[test]
193 fn test_merge_region_zero_span() {
194 assert!(MergeRegion::new(0, 0, 0, 1).is_err());
195 assert!(MergeRegion::new(0, 0, 1, 0).is_err());
196 }
197
198 #[test]
199 fn test_merge_region_last() {
200 let r = MergeRegion::new(1, 2, 3, 4).unwrap();
201 assert_eq!(r.last_row(), 3);
202 assert_eq!(r.last_col(), 5);
203 }
204
205 #[test]
206 fn test_merge_region_contains() {
207 let r = MergeRegion::new(1, 1, 2, 2).unwrap();
208 assert!(r.contains(1, 1));
209 assert!(r.contains(2, 2));
210 assert!(!r.contains(0, 0));
211 assert!(!r.contains(3, 1));
212 }
213
214 #[test]
215 fn test_merge_region_is_anchor() {
216 let r = MergeRegion::new(1, 2, 2, 3).unwrap();
217 assert!(r.is_anchor(1, 2));
218 assert!(!r.is_anchor(1, 3));
219 assert!(!r.is_anchor(2, 2));
220 }
221
222 #[test]
223 fn test_merge_region_overlaps() {
224 let a = MergeRegion::new(0, 0, 2, 2).unwrap();
225 let b = MergeRegion::new(1, 1, 2, 2).unwrap();
226 assert!(a.overlaps(&b));
227 assert!(b.overlaps(&a));
228
229 let c = MergeRegion::new(2, 2, 1, 1).unwrap();
230 assert!(!a.overlaps(&c));
231 }
232
233 #[test]
234 fn test_merge_region_adjacent_no_overlap() {
235 let a = MergeRegion::new(0, 0, 2, 2).unwrap();
236 let b = MergeRegion::new(0, 2, 2, 2).unwrap();
237 assert!(!a.overlaps(&b));
238 }
239
240 #[test]
241 fn test_cell_merge_state_normal() {
242 let s = CellMergeState::Normal;
243 assert_eq!(s.to_xml_attrs(), "");
244 assert!(!s.is_merged_away());
245 }
246
247 #[test]
248 fn test_cell_merge_state_anchor() {
249 let s = CellMergeState::Anchor { row_span: 2, col_span: 3 };
250 let xml = s.to_xml_attrs();
251 assert!(xml.contains(r#"gridSpan="3""#));
252 assert!(xml.contains(r#"rowSpan="2""#));
253 assert!(!s.is_merged_away());
254 }
255
256 #[test]
257 fn test_cell_merge_state_anchor_col_only() {
258 let s = CellMergeState::Anchor { row_span: 1, col_span: 3 };
259 let xml = s.to_xml_attrs();
260 assert!(xml.contains(r#"gridSpan="3""#));
261 assert!(!xml.contains("rowSpan"));
262 }
263
264 #[test]
265 fn test_cell_merge_state_hmerge() {
266 let s = CellMergeState::HMerge;
267 assert!(s.to_xml_attrs().contains("hMerge"));
268 assert!(s.is_merged_away());
269 }
270
271 #[test]
272 fn test_cell_merge_state_vmerge() {
273 let s = CellMergeState::VMerge;
274 assert!(s.to_xml_attrs().contains("vMerge"));
275 assert!(s.is_merged_away());
276 }
277
278 #[test]
279 fn test_table_merge_map_new() {
280 let m = TableMergeMap::new(5, 4);
281 assert!(m.is_empty());
282 assert_eq!(m.dimensions(), (5, 4));
283 }
284
285 #[test]
286 fn test_table_merge_map_add() {
287 let mut m = TableMergeMap::new(5, 4);
288 assert!(m.merge_cells(0, 0, 2, 2).is_ok());
289 assert_eq!(m.len(), 1);
290 }
291
292 #[test]
293 fn test_table_merge_map_out_of_bounds() {
294 let mut m = TableMergeMap::new(3, 3);
295 assert!(m.merge_cells(2, 2, 2, 1).is_err()); assert!(m.merge_cells(0, 2, 1, 2).is_err()); }
298
299 #[test]
300 fn test_table_merge_map_overlap_detection() {
301 let mut m = TableMergeMap::new(5, 5);
302 m.merge_cells(0, 0, 2, 2).unwrap();
303 assert!(m.merge_cells(1, 1, 2, 2).is_err());
304 }
305
306 #[test]
307 fn test_table_merge_map_adjacent_ok() {
308 let mut m = TableMergeMap::new(5, 5);
309 m.merge_cells(0, 0, 2, 2).unwrap();
310 assert!(m.merge_cells(0, 2, 2, 2).is_ok());
311 assert_eq!(m.len(), 2);
312 }
313
314 #[test]
315 fn test_table_merge_map_cell_state() {
316 let mut m = TableMergeMap::new(4, 4);
317 m.merge_cells(0, 0, 2, 3).unwrap();
318
319 assert_eq!(
321 m.cell_state(0, 0),
322 CellMergeState::Anchor { row_span: 2, col_span: 3 }
323 );
324 assert_eq!(m.cell_state(0, 1), CellMergeState::HMerge);
326 assert_eq!(m.cell_state(0, 2), CellMergeState::HMerge);
327 assert_eq!(m.cell_state(1, 0), CellMergeState::VMerge);
329 assert_eq!(m.cell_state(1, 1), CellMergeState::VMerge);
330 assert_eq!(m.cell_state(1, 2), CellMergeState::VMerge);
331 assert_eq!(m.cell_state(0, 3), CellMergeState::Normal);
333 assert_eq!(m.cell_state(2, 0), CellMergeState::Normal);
334 }
335
336 #[test]
337 fn test_table_merge_map_multiple_regions() {
338 let mut m = TableMergeMap::new(4, 4);
339 m.merge_cells(0, 0, 1, 2).unwrap();
340 m.merge_cells(2, 2, 2, 2).unwrap();
341
342 assert_eq!(
343 m.cell_state(0, 0),
344 CellMergeState::Anchor { row_span: 1, col_span: 2 }
345 );
346 assert_eq!(m.cell_state(0, 1), CellMergeState::HMerge);
347 assert_eq!(
348 m.cell_state(2, 2),
349 CellMergeState::Anchor { row_span: 2, col_span: 2 }
350 );
351 assert_eq!(m.cell_state(3, 3), CellMergeState::VMerge);
352 assert_eq!(m.cell_state(1, 1), CellMergeState::Normal);
353 }
354}