Skip to main content

xlsbye_xml/
worksheet.rs

1use crate::shared_strings::write_text_node;
2use crate::writer::{Result, XmlWriter};
3use std::io::Write;
4use xlsbye_core::types::{
5    Cell, CellValue, ColumnInfo, MergeCells, PaneInfo, RangeRef, RichTextRun, RowInfo,
6    SelectionInfo, SheetFormatInfo, SheetViewInfo,
7};
8use xlsbye_core::xml_names::{RELATIONSHIPS_NS, SPREADSHEET_NS};
9
10#[derive(Debug, Clone, PartialEq)]
11pub struct SheetRow {
12    pub info: RowInfo,
13    pub cells: Vec<Cell>,
14}
15
16#[derive(Debug, Clone, PartialEq, Default)]
17pub struct SheetData {
18    pub dimension: Option<RangeRef>,
19    pub sheet_views: Vec<SheetViewInfo>,
20    pub sheet_format: Option<SheetFormatInfo>,
21    pub columns: Vec<ColumnInfo>,
22    pub rows: Vec<SheetRow>,
23    pub merge_cells: MergeCells,
24}
25
26pub fn write_worksheet(writer: impl Write, sheet_data: &SheetData) -> Result<()> {
27    let mut writer = XmlWriter::new(writer);
28    writer.write_xml_declaration()?;
29    writer.write_start_element_with_ns(
30        "worksheet",
31        [
32            ("", SPREADSHEET_NS),
33            ("r", RELATIONSHIPS_NS),
34            ("x14ac", "http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac"),
35        ],
36        std::iter::empty::<(&str, &str)>(),
37    )?;
38
39    if let Some(dimension) = &sheet_data.dimension {
40        writer.write_empty_element("dimension", [("ref", range_ref_to_a1(dimension))])?;
41    }
42
43    if !sheet_data.sheet_views.is_empty() {
44        writer.write_start_element("sheetViews", std::iter::empty::<(&str, &str)>())?;
45        for sheet_view in &sheet_data.sheet_views {
46            write_sheet_view(&mut writer, sheet_view)?;
47        }
48        writer.write_end_element("sheetViews")?;
49    }
50
51    if let Some(sheet_format) = &sheet_data.sheet_format {
52        let mut attrs = vec![("defaultRowHeight".to_string(), trim_float(sheet_format.default_row_height))];
53        if sheet_format.outline_level_row != 0 {
54            attrs.push(("outlineLevelRow".to_string(), sheet_format.outline_level_row.to_string()));
55        }
56        if sheet_format.outline_level_col != 0 {
57            attrs.push(("outlineLevelCol".to_string(), sheet_format.outline_level_col.to_string()));
58        }
59        attrs.push(("x14ac:dyDescent".to_string(), "0.2".to_string()));
60        writer.write_empty_element("sheetFormatPr", attrs)?;
61    }
62
63    if !sheet_data.columns.is_empty() {
64        writer.write_start_element("cols", std::iter::empty::<(&str, &str)>())?;
65        for column in &sheet_data.columns {
66            let mut attrs = vec![
67                ("min".to_string(), column.min.to_string()),
68                ("max".to_string(), column.max.to_string()),
69                ("width".to_string(), column.width.to_string()),
70            ];
71            if column.style_index != 0 {
72                attrs.push(("style".to_string(), column.style_index.to_string()));
73            }
74            if column.hidden {
75                attrs.push(("hidden".to_string(), "1".to_string()));
76            }
77            if column.best_fit {
78                attrs.push(("bestFit".to_string(), "1".to_string()));
79            }
80            if column.custom_width {
81                attrs.push(("customWidth".to_string(), "1".to_string()));
82            }
83            if column.outline_level != 0 {
84                attrs.push(("outlineLevel".to_string(), column.outline_level.to_string()));
85            }
86            if column.collapsed {
87                attrs.push(("collapsed".to_string(), "1".to_string()));
88            }
89            writer.write_empty_element("col", attrs)?;
90        }
91        writer.write_end_element("cols")?;
92    }
93
94    writer.write_start_element("sheetData", std::iter::empty::<(&str, &str)>())?;
95    for row in &sheet_data.rows {
96        write_row(&mut writer, row, sheet_data.sheet_format.as_ref())?;
97    }
98    writer.write_end_element("sheetData")?;
99
100    if !sheet_data.merge_cells.is_empty() {
101        writer.write_start_element(
102            "mergeCells",
103            [("count", sheet_data.merge_cells.len().to_string())],
104        )?;
105        for merge in &sheet_data.merge_cells {
106            writer.write_empty_element("mergeCell", [("ref", range_ref_to_a1(&merge.range))])?;
107        }
108        writer.write_end_element("mergeCells")?;
109    }
110
111    writer.write_end_element("worksheet")?;
112    Ok(())
113}
114
115fn write_row<W: Write>(writer: &mut XmlWriter<W>, row: &SheetRow, sheet_format: Option<&SheetFormatInfo>) -> Result<()> {
116    let mut attrs = vec![("r".to_string(), row.info.row.to_string())];
117    if let Some((min_col, max_col)) = row.cells.first().zip(row.cells.last()).map(|(first, last)| (first.col, last.col)) {
118        attrs.push(("spans".to_string(), format!("{min_col}:{max_col}")));
119    }
120
121    let default_row_height = sheet_format.map(|format| format.default_row_height).unwrap_or(15.0);
122    if row.info.custom_height || (row.info.height - default_row_height).abs() > 0.0001 {
123        attrs.push(("ht".to_string(), trim_float(row.info.height)));
124    }
125    if row.info.custom_height {
126        attrs.push(("customHeight".to_string(), "1".to_string()));
127    }
128    if row.info.style_index != 0 {
129        attrs.push(("s".to_string(), row.info.style_index.to_string()));
130        attrs.push(("customFormat".to_string(), "1".to_string()));
131    }
132    if row.info.hidden {
133        attrs.push(("hidden".to_string(), "1".to_string()));
134    }
135    if row.info.outline_level != 0 {
136        attrs.push(("outlineLevel".to_string(), row.info.outline_level.to_string()));
137    }
138    if row.info.collapsed {
139        attrs.push(("collapsed".to_string(), "1".to_string()));
140    }
141    if row.info.thick_top {
142        attrs.push(("thickTop".to_string(), "1".to_string()));
143    }
144    if row.info.thick_bottom {
145        attrs.push(("thickBot".to_string(), "1".to_string()));
146    }
147    attrs.push(("x14ac:dyDescent".to_string(), "0.2".to_string()));
148
149    writer.write_start_element("row", attrs)?;
150    for cell in &row.cells {
151        write_cell(writer, row.info.row, cell)?;
152    }
153    writer.write_end_element("row")
154}
155
156fn trim_float(value: f64) -> String {
157    if value.fract() == 0.0 {
158        format!("{value:.0}")
159    } else {
160        let text = value.to_string();
161        text.trim_end_matches('0').trim_end_matches('.').to_string()
162    }
163}
164
165fn write_sheet_view<W: Write>(writer: &mut XmlWriter<W>, sheet_view: &SheetViewInfo) -> Result<()> {
166    let mut attrs = vec![("workbookViewId".to_string(), sheet_view.workbook_view_id.to_string())];
167    if !sheet_view.show_grid_lines {
168        attrs.push(("showGridLines".to_string(), "0".to_string()));
169    }
170    if sheet_view.tab_selected {
171        attrs.push(("tabSelected".to_string(), "1".to_string()));
172    }
173    if sheet_view.top_left_row != 0 || sheet_view.top_left_col != 0 {
174        attrs.push((
175            "topLeftCell".to_string(),
176            cell_ref_zero_based_to_a1(sheet_view.top_left_row, sheet_view.top_left_col),
177        ));
178    }
179    if let Some(zoom_scale) = sheet_view.zoom_scale {
180        attrs.push(("zoomScale".to_string(), zoom_scale.to_string()));
181    }
182    if let Some(zoom_scale_normal) = sheet_view.zoom_scale_normal {
183        attrs.push(("zoomScaleNormal".to_string(), zoom_scale_normal.to_string()));
184    }
185
186    writer.write_start_element("sheetView", attrs)?;
187    if let Some(pane) = &sheet_view.pane {
188        write_pane(writer, pane)?;
189    }
190    for selection in &sheet_view.selections {
191        write_selection(writer, selection)?;
192    }
193    writer.write_end_element("sheetView")
194}
195
196fn write_pane<W: Write>(writer: &mut XmlWriter<W>, pane: &PaneInfo) -> Result<()> {
197    let mut attrs = vec![
198        ("xSplit".to_string(), split_to_string(pane.x_split)),
199        ("ySplit".to_string(), split_to_string(pane.y_split)),
200        (
201            "topLeftCell".to_string(),
202            cell_ref_zero_based_to_a1(pane.top_left_row, pane.top_left_col),
203        ),
204        ("activePane".to_string(), pane.active_pane.clone()),
205        ("state".to_string(), pane.state.clone()),
206    ];
207    attrs.retain(|(_, value)| !value.is_empty());
208    writer.write_empty_element("pane", attrs)
209}
210
211fn write_selection<W: Write>(writer: &mut XmlWriter<W>, selection: &SelectionInfo) -> Result<()> {
212    let mut attrs = Vec::new();
213    if let Some(pane) = &selection.pane {
214        attrs.push(("pane".to_string(), pane.clone()));
215    }
216    attrs.push(("activeCell".to_string(), selection.active_cell.clone()));
217    attrs.push(("sqref".to_string(), selection.sqref.clone()));
218    writer.write_empty_element("selection", attrs)
219}
220
221fn split_to_string(value: f64) -> String {
222    if value.fract() == 0.0 {
223        format!("{value:.0}")
224    } else {
225        value.to_string()
226    }
227}
228
229fn cell_ref_zero_based_to_a1(row: u32, col: u32) -> String {
230    format!("{}{}", col_to_name(col + 1), row + 1)
231}
232
233fn write_cell<W: Write>(writer: &mut XmlWriter<W>, row: u32, cell: &Cell) -> Result<()> {
234    let mut attrs = vec![
235        ("r".to_string(), cell_ref_to_a1(row, cell.col)),
236        ("s".to_string(), cell.style_index.to_string()),
237    ];
238
239    let mut inline_string_runs: Option<&[RichTextRun]> = None;
240    let mut inline_string_text: Option<&str> = None;
241    let mut value_text: Option<String> = None;
242
243    match &cell.value {
244        CellValue::Blank => {}
245        CellValue::Bool(v) => {
246            attrs.push(("t".to_string(), "b".to_string()));
247            value_text = Some(if *v { "1".to_string() } else { "0".to_string() });
248        }
249        CellValue::Number(v) => {
250            value_text = Some(v.to_string());
251        }
252        CellValue::Error(err) => {
253            attrs.push(("t".to_string(), "e".to_string()));
254            value_text = Some(err.as_str().to_string());
255        }
256        CellValue::String(text) => {
257            if cell.formula.is_some() || cell.shared_formula_index.is_some() {
258                attrs.push(("t".to_string(), "str".to_string()));
259                value_text = Some(text.clone());
260            } else {
261                attrs.push(("t".to_string(), "inlineStr".to_string()));
262                inline_string_text = Some(text.as_str());
263            }
264        }
265        CellValue::SharedString(index) => {
266            attrs.push(("t".to_string(), "s".to_string()));
267            value_text = Some(index.to_string());
268        }
269        CellValue::RichString(runs) => {
270            attrs.push(("t".to_string(), "inlineStr".to_string()));
271            inline_string_runs = Some(runs);
272        }
273    }
274
275    if cell.formula.is_none()
276        && cell.shared_formula_index.is_none()
277        && inline_string_runs.is_none()
278        && inline_string_text.is_none()
279        && value_text.is_none()
280    {
281        writer.write_empty_element("c", attrs)?;
282        return Ok(());
283    }
284
285    writer.write_start_element("c", attrs)?;
286
287    if cell.formula.is_some() || cell.shared_formula_index.is_some() {
288        let mut formula_attrs = Vec::new();
289        if let Some(shared_index) = cell.shared_formula_index {
290            formula_attrs.push(("t".to_string(), "shared".to_string()));
291            formula_attrs.push(("si".to_string(), shared_index.to_string()));
292            if let Some(shared_ref) = &cell.shared_formula_ref {
293                formula_attrs.push(("ref".to_string(), range_ref_to_a1(shared_ref)));
294            }
295        }
296
297        if let Some(formula) = &cell.formula {
298            writer.write_text_element("f", formula_attrs, formula)?;
299        } else {
300            writer.write_empty_element("f", formula_attrs)?;
301        }
302    }
303
304    if let Some(text) = inline_string_text {
305        writer.write_start_element("is", std::iter::empty::<(&str, &str)>())?;
306        write_text_node(writer, "t", text)?;
307        writer.write_end_element("is")?;
308    } else if let Some(runs) = inline_string_runs {
309        writer.write_start_element("is", std::iter::empty::<(&str, &str)>())?;
310        for run in runs {
311            writer.write_start_element("r", std::iter::empty::<(&str, &str)>())?;
312            if let Some(font_index) = run.font_index {
313                writer.write_start_element("rPr", std::iter::empty::<(&str, &str)>())?;
314                writer.write_empty_element("rFont", [("val", format!("font{}", font_index))])?;
315                writer.write_end_element("rPr")?;
316            }
317            write_text_node(writer, "t", &run.text)?;
318            writer.write_end_element("r")?;
319        }
320        writer.write_end_element("is")?;
321    } else if let Some(v) = value_text {
322        writer.write_text_element("v", std::iter::empty::<(&str, &str)>(), &v)?;
323    }
324
325    writer.write_end_element("c")
326}
327
328fn range_ref_to_a1(range: &RangeRef) -> String {
329    format!(
330        "{}:{}",
331        cell_ref_to_a1(range.first_row, range.first_col),
332        cell_ref_to_a1(range.last_row, range.last_col)
333    )
334}
335
336fn cell_ref_to_a1(row: u32, col: u32) -> String {
337    format!("{}{}", col_to_name(col), row)
338}
339
340fn col_to_name(mut col: u32) -> String {
341    let mut letters = Vec::new();
342    while col > 0 {
343        let rem = ((col - 1) % 26) as u8;
344        letters.push((b'A' + rem) as char);
345        col = (col - 1) / 26;
346    }
347    letters.iter().rev().collect()
348}