Skip to main content

excelize_rs/
merge.rs

1//! Merge-cell API.
2//!
3//! This module corresponds to `merge.go` in the Go implementation.
4
5use crate::cell::find_cell_mut;
6use crate::errors::Result;
7use crate::errors::{ErrParameterInvalid, ErrSheetNotExist};
8use crate::file::File;
9use crate::lib_util::{
10    cell_name_to_coordinates, coordinates_to_cell_name, coordinates_to_range_ref, sort_coordinates,
11};
12
13impl File {
14    /// Merge a range of cells.
15    pub fn merge_cell(&self, sheet: &str, top_left: &str, bottom_right: &str) -> Result<()> {
16        let range = normalize_range(top_left, bottom_right)?;
17        let path = self
18            .get_sheet_xml_path(sheet)
19            .ok_or_else(|| ErrSheetNotExist {
20                sheet_name: sheet.to_string(),
21            })?;
22        let mut ws = self.work_sheet_reader(sheet)?;
23        ensure_merge_cells(&mut ws);
24        let merges = ws.merge_cells.as_mut().unwrap();
25        // Remove any overlapping existing merges.
26        merges.cells.retain(|m| {
27            if let Some(existing) = &m.r#ref {
28                !ranges_overlap(existing, &range)
29            } else {
30                true
31            }
32        });
33        merges.cells.push(crate::xml::worksheet::XlsxMergeCell {
34            r#ref: Some(range.clone()),
35        });
36        merges.count = Some(merges.cells.len() as i64);
37        self.sheet.insert(path, ws);
38        Ok(())
39    }
40
41    /// Unmerge a range of cells.
42    pub fn unmerge_cell(&self, sheet: &str, top_left: &str, bottom_right: &str) -> Result<()> {
43        let range = normalize_range(top_left, bottom_right)?;
44        let path = self
45            .get_sheet_xml_path(sheet)
46            .ok_or_else(|| ErrSheetNotExist {
47                sheet_name: sheet.to_string(),
48            })?;
49        let mut ws = self.work_sheet_reader(sheet)?;
50        if let Some(merges) = ws.merge_cells.as_mut() {
51            merges.cells.retain(|m| m.r#ref.as_deref() != Some(&range));
52            merges.count = Some(merges.cells.len() as i64);
53            if merges.cells.is_empty() {
54                ws.merge_cells = None;
55            }
56        }
57        self.sheet.insert(path, ws);
58        Ok(())
59    }
60
61    /// Get all merged cell ranges in a worksheet.
62    pub fn get_merge_cells(&self, sheet: &str) -> Result<Vec<String>> {
63        let ws = self.work_sheet_reader(sheet)?;
64        let mut out = Vec::new();
65        if let Some(merges) = ws.merge_cells {
66            for m in merges.cells {
67                if let Some(r) = m.r#ref {
68                    out.push(r);
69                }
70            }
71        }
72        Ok(out)
73    }
74}
75
76/// Add a merged-cell range directly to a worksheet in memory.
77///
78/// Equivalent to [`File::merge_cell`] but without writing the worksheet back to
79/// the file cache, so callers that already hold a mutable worksheet can use it.
80pub(crate) fn add_merge(
81    ws: &mut crate::xml::worksheet::XlsxWorksheet,
82    top_left: &str,
83    bottom_right: &str,
84) -> Result<()> {
85    let range = normalize_range(top_left, bottom_right)?;
86    ensure_merge_cells(ws);
87    let merges = ws.merge_cells.as_mut().unwrap();
88    merges.cells.retain(|m| {
89        if let Some(existing) = &m.r#ref {
90            !ranges_overlap(existing, &range)
91        } else {
92            true
93        }
94    });
95    merges.cells.push(crate::xml::worksheet::XlsxMergeCell {
96        r#ref: Some(range.clone()),
97    });
98    merges.count = Some(merges.cells.len() as i64);
99
100    // Clear values/formulas from every cell in the range except the top-left.
101    let parts: Vec<&str> = range.split(':').collect();
102    let (tl, br) = (parts[0], parts[1]);
103    let mut coords = cell_refs_to_coordinates(tl, br)?;
104    sort_coordinates(&mut coords)?;
105    let (min_col, min_row, max_col, max_row) = (coords[0], coords[1], coords[2], coords[3]);
106    for col in min_col..=max_col {
107        for row in min_row..=max_row {
108            if col == min_col && row == min_row {
109                continue;
110            }
111            let cell_ref = coordinates_to_cell_name(col, row, false)?;
112            if let Some(c) = find_cell_mut(ws, &cell_ref) {
113                c.t = None;
114                c.v = None;
115                c.f = None;
116                c.is = None;
117            }
118        }
119    }
120    Ok(())
121}
122
123// ------------------------------------------------------------------
124// Internal helpers
125// ------------------------------------------------------------------
126
127fn ensure_merge_cells(ws: &mut crate::xml::worksheet::XlsxWorksheet) {
128    if ws.merge_cells.is_none() {
129        ws.merge_cells = Some(crate::xml::worksheet::XlsxMergeCells::default());
130    }
131}
132
133fn normalize_range(top_left: &str, bottom_right: &str) -> Result<String> {
134    let mut coords = cell_refs_to_coordinates(top_left, bottom_right)?;
135    sort_coordinates(&mut coords)?;
136    Ok(coordinates_to_range_ref(&coords, false)?)
137}
138
139fn cell_refs_to_coordinates(first: &str, last: &str) -> Result<Vec<i32>> {
140    let mut coords = vec![0; 4];
141    let (col, row) = cell_name_to_coordinates(first)?;
142    coords[0] = col;
143    coords[1] = row;
144    let (col, row) = cell_name_to_coordinates(last)?;
145    coords[2] = col;
146    coords[3] = row;
147    Ok(coords)
148}
149
150fn ranges_overlap(a: &str, b: &str) -> bool {
151    let Ok(ca) = normalize_single_range(a) else {
152        return true;
153    };
154    let Ok(cb) = normalize_single_range(b) else {
155        return true;
156    };
157    if ca[2] < cb[0] || ca[0] > cb[2] || ca[3] < cb[1] || ca[1] > cb[3] {
158        return false;
159    }
160    true
161}
162
163fn normalize_single_range(r: &str) -> Result<Vec<i32>> {
164    let parts: Vec<&str> = r.split(':').collect();
165    if parts.len() != 2 {
166        return Err(ErrParameterInvalid.to_string().into());
167    }
168    let mut coords = cell_refs_to_coordinates(parts[0], parts[1])?;
169    sort_coordinates(&mut coords)?;
170    Ok(coords)
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn merge_and_unmerge() {
179        let f = File::new_with_options(crate::options::Options::default());
180        f.merge_cell("Sheet1", "A1", "B2").unwrap();
181        assert_eq!(f.get_merge_cells("Sheet1").unwrap(), vec!["A1:B2"]);
182        f.merge_cell("Sheet1", "C1", "C3").unwrap();
183        assert!(
184            f.get_merge_cells("Sheet1")
185                .unwrap()
186                .contains(&"C1:C3".to_string())
187        );
188        f.unmerge_cell("Sheet1", "A1", "B2").unwrap();
189        assert!(
190            !f.get_merge_cells("Sheet1")
191                .unwrap()
192                .contains(&"A1:B2".to_string())
193        );
194    }
195}