1use crate::error::{Error, Result};
6use crate::utils::cell_ref::cell_name_to_coordinates;
7use sheetkit_xml::worksheet::{MergeCell, MergeCells, WorksheetXml};
8
9fn parse_range(reference: &str) -> Result<(u32, u32, u32, u32)> {
13 let parts: Vec<&str> = reference.split(':').collect();
14 if parts.len() != 2 {
15 return Err(Error::InvalidCellReference(format!(
16 "expected range like 'A1:C3', got '{reference}'"
17 )));
18 }
19 let (c1, r1) = cell_name_to_coordinates(parts[0])?;
20 let (c2, r2) = cell_name_to_coordinates(parts[1])?;
21 let min_col = c1.min(c2);
22 let max_col = c1.max(c2);
23 let min_row = r1.min(r2);
24 let max_row = r1.max(r2);
25 Ok((min_col, min_row, max_col, max_row))
26}
27
28fn ranges_overlap(a: (u32, u32, u32, u32), b: (u32, u32, u32, u32)) -> bool {
30 let (a_min_col, a_min_row, a_max_col, a_max_row) = a;
31 let (b_min_col, b_min_row, b_max_col, b_max_row) = b;
32 a_min_col <= b_max_col
33 && a_max_col >= b_min_col
34 && a_min_row <= b_max_row
35 && a_max_row >= b_min_row
36}
37
38pub fn merge_cells(ws: &mut WorksheetXml, top_left: &str, bottom_right: &str) -> Result<()> {
43 let (tl_col, tl_row) = cell_name_to_coordinates(top_left)?;
44 let (br_col, br_row) = cell_name_to_coordinates(bottom_right)?;
45
46 let min_col = tl_col.min(br_col);
47 let max_col = tl_col.max(br_col);
48 let min_row = tl_row.min(br_row);
49 let max_row = tl_row.max(br_row);
50 let new_range = (min_col, min_row, max_col, max_row);
51
52 let reference = format!("{top_left}:{bottom_right}");
53
54 if let Some(ref mc) = ws.merge_cells {
56 for existing in &mc.merge_cells {
57 let existing_range = parse_range(&existing.reference)?;
58 if ranges_overlap(new_range, existing_range) {
59 return Err(Error::MergeCellOverlap {
60 new: reference,
61 existing: existing.reference.clone(),
62 });
63 }
64 }
65 }
66
67 let merge_cells = ws.merge_cells.get_or_insert_with(|| MergeCells {
69 count: None,
70 merge_cells: Vec::new(),
71 });
72 merge_cells.merge_cells.push(MergeCell { reference });
73 merge_cells.count = Some(merge_cells.merge_cells.len() as u32);
74
75 Ok(())
76}
77
78pub fn unmerge_cell(ws: &mut WorksheetXml, reference: &str) -> Result<()> {
83 let mc = ws
84 .merge_cells
85 .as_mut()
86 .ok_or_else(|| Error::MergeCellNotFound(reference.to_string()))?;
87
88 let initial_len = mc.merge_cells.len();
89 mc.merge_cells.retain(|m| m.reference != reference);
90
91 if mc.merge_cells.len() == initial_len {
92 return Err(Error::MergeCellNotFound(reference.to_string()));
93 }
94
95 if mc.merge_cells.is_empty() {
96 ws.merge_cells = None;
97 } else {
98 mc.count = Some(mc.merge_cells.len() as u32);
99 }
100
101 Ok(())
102}
103
104pub fn get_merge_cells(ws: &WorksheetXml) -> Vec<String> {
108 ws.merge_cells
109 .as_ref()
110 .map(|mc| mc.merge_cells.iter().map(|m| m.reference.clone()).collect())
111 .unwrap_or_default()
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 fn new_ws() -> WorksheetXml {
119 WorksheetXml::default()
120 }
121
122 #[test]
123 fn test_merge_cells_basic() {
124 let mut ws = new_ws();
125 merge_cells(&mut ws, "A1", "B2").unwrap();
126 let merged = get_merge_cells(&ws);
127 assert_eq!(merged, vec!["A1:B2"]);
128 assert_eq!(ws.merge_cells.as_ref().unwrap().count, Some(1));
129 }
130
131 #[test]
132 fn test_merge_cells_multiple() {
133 let mut ws = new_ws();
134 merge_cells(&mut ws, "A1", "B2").unwrap();
135 merge_cells(&mut ws, "D1", "F3").unwrap();
136 merge_cells(&mut ws, "A5", "C7").unwrap();
137 let merged = get_merge_cells(&ws);
138 assert_eq!(merged.len(), 3);
139 assert_eq!(merged[0], "A1:B2");
140 assert_eq!(merged[1], "D1:F3");
141 assert_eq!(merged[2], "A5:C7");
142 assert_eq!(ws.merge_cells.as_ref().unwrap().count, Some(3));
143 }
144
145 #[test]
146 fn test_merge_cells_overlap_detection() {
147 let mut ws = new_ws();
148 merge_cells(&mut ws, "A1", "C3").unwrap();
149
150 let err = merge_cells(&mut ws, "A1", "C3").unwrap_err();
152 assert!(err.to_string().contains("overlaps"));
153
154 let err = merge_cells(&mut ws, "B2", "D4").unwrap_err();
156 assert!(err.to_string().contains("overlaps"));
157
158 let err = merge_cells(&mut ws, "B2", "B2").unwrap_err();
160 assert!(err.to_string().contains("overlaps"));
161
162 merge_cells(&mut ws, "D1", "F3").unwrap();
164 }
165
166 #[test]
167 fn test_merge_cells_overlap_adjacent_no_overlap() {
168 let mut ws = new_ws();
169 merge_cells(&mut ws, "A1", "B2").unwrap();
170 merge_cells(&mut ws, "C1", "D2").unwrap();
172 merge_cells(&mut ws, "A3", "B4").unwrap();
174 assert_eq!(get_merge_cells(&ws).len(), 3);
175 }
176
177 #[test]
178 fn test_unmerge_cell() {
179 let mut ws = new_ws();
180 merge_cells(&mut ws, "A1", "B2").unwrap();
181 merge_cells(&mut ws, "D1", "F3").unwrap();
182
183 unmerge_cell(&mut ws, "A1:B2").unwrap();
184 let merged = get_merge_cells(&ws);
185 assert_eq!(merged, vec!["D1:F3"]);
186 assert_eq!(ws.merge_cells.as_ref().unwrap().count, Some(1));
187 }
188
189 #[test]
190 fn test_unmerge_cell_last_removes_element() {
191 let mut ws = new_ws();
192 merge_cells(&mut ws, "A1", "B2").unwrap();
193 unmerge_cell(&mut ws, "A1:B2").unwrap();
194 assert!(ws.merge_cells.is_none());
195 assert!(get_merge_cells(&ws).is_empty());
196 }
197
198 #[test]
199 fn test_unmerge_cell_not_found() {
200 let mut ws = new_ws();
201 let err = unmerge_cell(&mut ws, "A1:B2").unwrap_err();
202 assert!(err.to_string().contains("not found"));
203
204 merge_cells(&mut ws, "A1", "B2").unwrap();
206 let err = unmerge_cell(&mut ws, "C1:D2").unwrap_err();
207 assert!(err.to_string().contains("not found"));
208 }
209
210 #[test]
211 fn test_get_merge_cells_empty() {
212 let ws = new_ws();
213 assert!(get_merge_cells(&ws).is_empty());
214 }
215
216 #[test]
217 fn test_merge_cells_invalid_reference() {
218 let mut ws = new_ws();
219 let err = merge_cells(&mut ws, "!!!", "B2").unwrap_err();
220 assert!(err.to_string().contains("invalid cell reference"));
221
222 let err = merge_cells(&mut ws, "A1", "ZZZ").unwrap_err();
223 assert!(err.to_string().contains("no row number"));
224 }
225
226 #[test]
227 fn test_parse_range_valid() {
228 let (c1, r1, c2, r2) = parse_range("A1:C3").unwrap();
229 assert_eq!((c1, r1, c2, r2), (1, 1, 3, 3));
230 }
231
232 #[test]
233 fn test_parse_range_reversed() {
234 let (c1, r1, c2, r2) = parse_range("C3:A1").unwrap();
236 assert_eq!((c1, r1, c2, r2), (1, 1, 3, 3));
237 }
238
239 #[test]
240 fn test_parse_range_invalid() {
241 assert!(parse_range("A1").is_err());
242 assert!(parse_range("A1:B2:C3").is_err());
243 assert!(parse_range("").is_err());
244 }
245
246 #[test]
247 fn test_ranges_overlap_function() {
248 assert!(ranges_overlap((1, 1, 3, 3), (2, 2, 4, 4)));
250 assert!(ranges_overlap((1, 1, 3, 3), (1, 1, 3, 3)));
252 assert!(ranges_overlap((1, 1, 5, 5), (2, 2, 3, 3)));
254 assert!(!ranges_overlap((1, 1, 2, 2), (3, 1, 4, 2)));
256 assert!(!ranges_overlap((1, 1, 2, 2), (1, 3, 2, 4)));
258 assert!(!ranges_overlap((1, 1, 2, 2), (5, 5, 6, 6)));
260 }
261}