1use serde::{Deserialize, Serialize};
2use std::borrow::Cow;
3
4use crate::constants::{LAST_COLUMN, LAST_ROW};
5use crate::expressions::types::Area;
6use crate::model::{CellStructure, Model};
7use crate::types::{Alignment, Cell, HorizontalAlignment, MergedCell};
8
9pub(crate) fn merge_across_ranges(range: &Area) -> Result<Vec<Area>, String> {
12 let Area {
13 sheet,
14 row,
15 column,
16 width,
17 height,
18 } = *range;
19 if row < 1 || column < 1 || width < 1 || height < 1 {
20 return Err("Invalid range".to_string());
21 }
22 Ok((row..row + height)
23 .map(|r| Area {
24 sheet,
25 row: r,
26 column,
27 width,
28 height: 1,
29 })
30 .collect())
31}
32
33pub(crate) fn merge_down_ranges(range: &Area) -> Result<Vec<Area>, String> {
36 let Area {
37 sheet,
38 row,
39 column,
40 width,
41 height,
42 } = *range;
43 if row < 1 || column < 1 || width < 1 || height < 1 {
44 return Err("Invalid range".to_string());
45 }
46 Ok((column..column + width)
47 .map(|c| Area {
48 sheet,
49 row,
50 column: c,
51 width: 1,
52 height,
53 })
54 .collect())
55}
56
57#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
59pub enum MergeStructure {
60 None,
62 Anchor {
64 width: i32,
66 height: i32,
68 },
69 Covered {
71 anchor_row: i32,
73 anchor_column: i32,
75 },
76}
77
78impl<'a> Model<'a> {
79 pub fn merge_cells(&mut self, range: &Area) -> Result<(), String> {
98 let Area {
99 sheet,
100 row,
101 column,
102 width,
103 height,
104 } = *range;
105 self.check_merge_range(range)?;
106 let content_cell = self.merge_range_content_cell(range)?;
107 let (source_row, source_column) = content_cell.unwrap_or((row, column));
110 let merged_style = self.get_style_for_cell(sheet, source_row, source_column)?;
111 if (source_row, source_column) != (row, column) {
112 let source_cell = self
116 .workbook
117 .worksheet(sheet)?
118 .cell(source_row, source_column)
119 .map(Cow::into_owned);
120 match source_cell {
121 Some(Cell::CellFormula { .. }) | Some(Cell::ArrayFormula { .. }) => {
122 let content =
125 self.get_localized_cell_content(sheet, source_row, source_column)?;
126 self.set_user_input(sheet, row, column, content)?;
127 }
128 Some(cell) => {
129 self.workbook
130 .worksheet_mut(sheet)?
131 .update_cell(row, column, cell)?;
132 }
133 None => {}
134 }
135 let worksheet = self.workbook.worksheet_mut(sheet)?;
136 if let Some(link) = worksheet.links.remove(&(source_row, source_column)) {
137 worksheet.links.insert((row, column), link);
138 }
139 }
140 self.merge_cells_keep_styles(range)?;
141 for r in row..row + height {
142 for c in column..column + width {
143 let mut style = merged_style.clone();
144 if r != row {
145 style.border.top = None;
146 }
147 if r != row + height - 1 {
148 style.border.bottom = None;
149 }
150 if c != column {
151 style.border.left = None;
152 }
153 if c != column + width - 1 {
154 style.border.right = None;
155 }
156 if self.get_style_for_cell(sheet, r, c)? != style {
157 self.set_cell_style(sheet, r, c, &style)?;
158 }
159 }
160 }
161 Ok(())
162 }
163
164 pub fn merge_cells_center(&mut self, range: &Area) -> Result<(), String> {
167 self.merge_cells(range)?;
168 self.center_merged_range(range)
169 }
170
171 pub fn merge_cells_across(&mut self, range: &Area) -> Result<(), String> {
176 let ranges = merge_across_ranges(range)?;
177 for range in &ranges {
178 self.check_merge(range)?;
179 }
180 for range in &ranges {
181 self.merge_cells(range)?;
182 }
183 Ok(())
184 }
185
186 pub fn merge_cells_down(&mut self, range: &Area) -> Result<(), String> {
191 let ranges = merge_down_ranges(range)?;
192 for range in &ranges {
193 self.check_merge(range)?;
194 }
195 for range in &ranges {
196 self.merge_cells(range)?;
197 }
198 Ok(())
199 }
200
201 pub(crate) fn check_merge(&self, range: &Area) -> Result<(), String> {
205 self.check_merge_range(range)?;
206 self.merge_range_content_cell(range)?;
207 Ok(())
208 }
209
210 fn merge_range_content_cell(&self, range: &Area) -> Result<Option<(i32, i32)>, String> {
215 let Area {
216 sheet,
217 row,
218 column,
219 width,
220 height,
221 } = *range;
222 let worksheet = self.workbook.worksheet(sheet)?;
223 let mut content_cell = None;
224 for r in row..row + height {
225 for c in column..column + width {
226 if matches!(
227 worksheet.cell(r, c).as_deref(),
228 None | Some(Cell::EmptyCell { .. }) | Some(Cell::SpillCell { .. })
229 ) {
230 continue;
231 }
232 if content_cell.is_some() {
233 return Err("Cannot merge cells: more than one cell has content".to_string());
234 }
235 content_cell = Some((r, c));
236 }
237 }
238 Ok(content_cell)
239 }
240
241 pub(crate) fn center_merged_range(&mut self, range: &Area) -> Result<(), String> {
245 let Area {
246 sheet,
247 row,
248 column,
249 width,
250 height,
251 } = *range;
252 for r in row..row + height {
253 for c in column..column + width {
254 let mut style = self.get_style_for_cell(sheet, r, c)?;
255 let alignment = style.alignment.get_or_insert_with(Alignment::default);
256 if alignment.horizontal != HorizontalAlignment::Center {
257 alignment.horizontal = HorizontalAlignment::Center;
258 self.set_cell_style(sheet, r, c, &style)?;
259 }
260 }
261 }
262 Ok(())
263 }
264
265 fn check_merge_range(&self, range: &Area) -> Result<(), String> {
268 let Area {
269 sheet,
270 row,
271 column,
272 width,
273 height,
274 } = *range;
275 if row < 1 || column < 1 || width < 1 || height < 1 {
276 return Err("Invalid range".to_string());
277 }
278 if row + height - 1 > LAST_ROW || column + width - 1 > LAST_COLUMN {
279 return Err("Range is out of bounds".to_string());
280 }
281 if width == 1 && height == 1 {
282 return Err("Cannot merge a single cell".to_string());
283 }
284 let worksheet = self.workbook.worksheet(sheet)?;
285 if let Some(m) = worksheet
286 .merged_cells
287 .iter()
288 .find(|m| m.intersects(row, column, width, height))
289 {
290 return Err(format!(
291 "Range intersects the merged cell at row {}, column {}",
292 m.row, m.column
293 ));
294 }
295 for r in row..row + height {
296 for c in column..column + width {
297 match worksheet.get_cell_structure(r, c)? {
298 CellStructure::ArrayFormula {
299 range: (array_width, array_height),
300 } => {
301 if array_width > 1 || array_height > 1 {
302 return Err(
303 "Cannot merge cells that intersect an array formula".to_string()
304 );
305 }
306 }
307 CellStructure::SpillArray { .. } => {
308 return Err(
309 "Cannot merge cells that intersect an array formula".to_string()
310 );
311 }
312 _ => {}
313 }
314 }
315 }
316 Ok(())
317 }
318
319 pub(crate) fn merge_cells_keep_styles(&mut self, range: &Area) -> Result<(), String> {
325 let Area {
326 sheet,
327 row,
328 column,
329 width,
330 height,
331 } = *range;
332 self.check_merge_range(range)?;
333 for r in row..row + height {
338 for c in column..column + width {
339 if r == row && c == column {
340 continue;
341 }
342 self.prepare_cell_for_user_input(sheet, r, c)?;
343 let worksheet = self.workbook.worksheet_mut(sheet)?;
344 if worksheet.cell(r, c).is_some() {
345 worksheet.cell_clear_contents(r, c)?;
346 }
347 worksheet.links.remove(&(r, c));
348 }
349 }
350 self.workbook
351 .worksheet_mut(sheet)?
352 .merged_cells
353 .push(MergedCell {
354 row,
355 column,
356 width,
357 height,
358 });
359 Ok(())
360 }
361
362 pub fn unmerge_cells(&mut self, range: &Area) -> Result<(), String> {
365 let Area {
366 sheet,
367 row,
368 column,
369 width,
370 height,
371 } = *range;
372 if row < 1 || column < 1 || width < 1 || height < 1 {
373 return Err("Invalid range".to_string());
374 }
375 let worksheet = self.workbook.worksheet_mut(sheet)?;
376 worksheet
377 .merged_cells
378 .retain(|m| !m.intersects(row, column, width, height));
379 Ok(())
380 }
381
382 pub fn get_merged_cells(&self, sheet: u32) -> Result<&[MergedCell], String> {
384 Ok(&self.workbook.worksheet(sheet)?.merged_cells)
385 }
386
387 pub fn get_merge_structure(
390 &self,
391 sheet: u32,
392 row: i32,
393 column: i32,
394 ) -> Result<MergeStructure, String> {
395 let worksheet = self.workbook.worksheet(sheet)?;
396 Ok(match worksheet.merged_cell_containing(row, column) {
397 Some(m) if m.row == row && m.column == column => MergeStructure::Anchor {
398 width: m.width,
399 height: m.height,
400 },
401 Some(m) => MergeStructure::Covered {
402 anchor_row: m.row,
403 anchor_column: m.column,
404 },
405 None => MergeStructure::None,
406 })
407 }
408}