Skip to main content

excelize_rs/
row.rs

1//! Row-level API.
2//!
3//! This module corresponds to `rows.go` in the Go implementation.
4
5use std::collections::BTreeMap;
6
7use crate::adjust::AdjustDirection;
8use crate::constants::{DEFAULT_ROW_HEIGHT, MAX_CELL_STYLES, MAX_ROW_HEIGHT, TOTAL_ROWS};
9use crate::errors::Result;
10use crate::errors::{ErrMaxRowHeight, ErrMaxRows, ErrSheetNotExist};
11use crate::file::File;
12use crate::lib_util::{
13    cell_name_to_coordinates, coordinates_to_cell_name, coordinates_to_range_ref,
14    range_ref_to_coordinates, sort_coordinates,
15};
16use crate::options::Options;
17use crate::xml::worksheet::{XlsxConditionalFormatting, XlsxDataValidation, XlsxWorksheet};
18
19impl File {
20    /// Set the height of a row.
21    pub fn set_row_height(&self, sheet: &str, row: i32, height: f64) -> Result<()> {
22        if height > MAX_ROW_HEIGHT as f64 {
23            return Err(Box::new(ErrMaxRowHeight));
24        }
25        let path = self
26            .get_sheet_xml_path(sheet)
27            .ok_or_else(|| ErrSheetNotExist {
28                sheet_name: sheet.to_string(),
29            })?;
30        let mut ws = self.work_sheet_reader(sheet)?;
31        let r = get_or_make_row(&mut ws, row);
32        r.ht = Some(height);
33        r.custom_height = Some(true);
34        self.sheet.insert(path, ws);
35        Ok(())
36    }
37
38    /// Get the height of a row.
39    pub fn get_row_height(&self, sheet: &str, row: i32) -> Result<f64> {
40        let ws = self.work_sheet_reader(sheet)?;
41        for r in &ws.sheet_data.row {
42            if r.r == Some(row as i64) {
43                return Ok(r.ht.unwrap_or(DEFAULT_ROW_HEIGHT));
44            }
45        }
46        if let Some(fmt) = &ws.sheet_format_pr {
47            return Ok(fmt.default_row_height);
48        }
49        Ok(DEFAULT_ROW_HEIGHT)
50    }
51
52    /// Set the visibility of a row.
53    pub fn set_row_visible(&self, sheet: &str, row: i32, visible: bool) -> Result<()> {
54        let path = self
55            .get_sheet_xml_path(sheet)
56            .ok_or_else(|| ErrSheetNotExist {
57                sheet_name: sheet.to_string(),
58            })?;
59        let mut ws = self.work_sheet_reader(sheet)?;
60        let r = get_or_make_row(&mut ws, row);
61        r.hidden = Some(!visible);
62        self.sheet.insert(path, ws);
63        Ok(())
64    }
65
66    /// Get the visibility of a row.
67    pub fn get_row_visible(&self, sheet: &str, row: i32) -> Result<bool> {
68        let ws = self.work_sheet_reader(sheet)?;
69        for r in &ws.sheet_data.row {
70            if r.r == Some(row as i64) {
71                return Ok(!r.hidden.unwrap_or(false));
72            }
73        }
74        Ok(true)
75    }
76
77    /// Set the outline level of a row.
78    pub fn set_row_outline_level(&self, sheet: &str, row: i32, level: u8) -> Result<()> {
79        if level == 0 || level > 7 {
80            return Err(Box::new(crate::errors::ErrOutlineLevel));
81        }
82        let path = self
83            .get_sheet_xml_path(sheet)
84            .ok_or_else(|| ErrSheetNotExist {
85                sheet_name: sheet.to_string(),
86            })?;
87        let mut ws = self.work_sheet_reader(sheet)?;
88        let r = get_or_make_row(&mut ws, row);
89        r.outline_level = Some(level);
90        self.sheet.insert(path, ws);
91        Ok(())
92    }
93
94    /// Get the outline level of a row.
95    pub fn get_row_outline_level(&self, sheet: &str, row: i32) -> Result<u8> {
96        let ws = self.work_sheet_reader(sheet)?;
97        for r in &ws.sheet_data.row {
98            if r.r == Some(row as i64) {
99                return Ok(r.outline_level.unwrap_or(0));
100            }
101        }
102        Ok(0)
103    }
104
105    /// Insert `n` rows before `row`.
106    pub fn insert_rows(&self, sheet: &str, row: i32, n: usize) -> Result<()> {
107        if n == 0 {
108            return Ok(());
109        }
110        let path = self
111            .get_sheet_xml_path(sheet)
112            .ok_or_else(|| ErrSheetNotExist {
113                sheet_name: sheet.to_string(),
114            })?;
115        let mut ws = self.work_sheet_reader(sheet)?;
116        shift_rows_down(&mut ws, row as i64, n as i64);
117        self.sheet.insert(path, ws);
118        Ok(())
119    }
120
121    /// Remove a row.
122    pub fn remove_row(&self, sheet: &str, row: i32) -> Result<()> {
123        if row < 1 {
124            return Ok(());
125        }
126        let path = self
127            .get_sheet_xml_path(sheet)
128            .ok_or_else(|| ErrSheetNotExist {
129                sheet_name: sheet.to_string(),
130            })?;
131        let mut ws = self.work_sheet_reader(sheet)?;
132
133        // Remove formulas from every cell in the deleted row so that the calc
134        // chain and shared-formula siblings stay consistent.
135        let indices: Vec<(usize, usize)> = ws
136            .sheet_data
137            .row
138            .iter()
139            .enumerate()
140            .filter(|(_, r)| r.r == Some(row as i64))
141            .flat_map(|(ri, r)| r.c.iter().enumerate().map(move |(ci, _)| (ri, ci)))
142            .collect();
143        for (ri, ci) in indices {
144            let mut cell = std::mem::take(&mut ws.sheet_data.row[ri].c[ci]);
145            self.remove_formula(&mut cell, &mut ws, sheet)?;
146            ws.sheet_data.row[ri].c[ci] = cell;
147        }
148
149        ws.sheet_data.row.retain(|r| r.r != Some(row as i64));
150        self.sheet.insert(path, ws);
151        self.adjust_helper(sheet, AdjustDirection::Rows, row, -1)?;
152        Ok(())
153    }
154
155    /// Duplicate a row.
156    pub fn duplicate_row(&self, sheet: &str, row: i32) -> Result<()> {
157        self.duplicate_row_to(sheet, row, row + 1)
158    }
159
160    /// Duplicate a row to a target position, shifting existing rows down.
161    ///
162    /// Equivalent to Go `DuplicateRowTo`.
163    pub fn duplicate_row_to(&self, sheet: &str, row: i32, row2: i32) -> Result<()> {
164        if row < 1 || row2 < 1 || row == row2 {
165            return Ok(());
166        }
167        let path = self
168            .get_sheet_xml_path(sheet)
169            .ok_or_else(|| ErrSheetNotExist {
170                sheet_name: sheet.to_string(),
171            })?;
172        let ws = self.work_sheet_reader(sheet)?;
173        let source = ws
174            .sheet_data
175            .row
176            .iter()
177            .find(|r| r.r == Some(row as i64))
178            .cloned();
179
180        // Shift existing rows and adjust references (formulas, merged cells,
181        // conditional formatting, data validations, calc chain, ...).
182        self.sheet.insert(path.clone(), ws);
183        self.adjust_helper(sheet, AdjustDirection::Rows, row2, 1)?;
184
185        let mut ws = self.work_sheet_reader(sheet)?;
186        if let Some(mut source) = source {
187            source.r = Some(row2 as i64);
188            let offset = row2 - row;
189            for c in &mut source.c {
190                if let Some(name) = &c.r {
191                    if let Ok((col, old_row)) = cell_name_to_coordinates(name) {
192                        if let Ok(new_name) = coordinates_to_cell_name(col, old_row + offset, false)
193                        {
194                            c.r = Some(new_name);
195                        }
196                    }
197                }
198            }
199            self.adjust_single_row_formulas(sheet, sheet, &mut source, row, offset, true)?;
200
201            if let Some(idx) = ws
202                .sheet_data
203                .row
204                .iter()
205                .position(|r| r.r == Some(row2 as i64))
206            {
207                ws.sheet_data.row[idx] = source;
208            } else {
209                ws.sheet_data.row.push(source);
210            }
211            ws.sheet_data.row.sort_by_key(|r| r.r.unwrap_or(0));
212
213            // Copy worksheet decorations that belong to the source row.
214            self.duplicate_row_conditional_formats(&mut ws, row, row2)?;
215            self.duplicate_row_data_validations(&mut ws, row, row2)?;
216            self.duplicate_row_merge_cells(&mut ws, row, row2)?;
217        }
218        self.sheet.insert(path, ws);
219        Ok(())
220    }
221
222    /// Copy conditional formatting rules that apply only to `row` so that they
223    /// also apply to `row2`.
224    fn duplicate_row_conditional_formats(
225        &self,
226        ws: &mut XlsxWorksheet,
227        row: i32,
228        row2: i32,
229    ) -> Result<()> {
230        let mut new_cfs: Vec<XlsxConditionalFormatting> = Vec::new();
231        for cf in &ws.conditional_formatting {
232            let mut sqrefs = Vec::new();
233            if let Some(sqref) = &cf.sqref {
234                for r#ref in sqref.split_whitespace() {
235                    if let Some(new_ref) = duplicate_sq_ref_helper(row, row2, r#ref)? {
236                        sqrefs.push(new_ref);
237                    }
238                }
239            }
240            if !sqrefs.is_empty() {
241                let mut cf_copy = cf.clone();
242                cf_copy.sqref = Some(sqrefs.join(" "));
243                new_cfs.push(cf_copy);
244            }
245        }
246        ws.conditional_formatting.extend(new_cfs);
247        Ok(())
248    }
249
250    /// Copy data validation rules that apply only to `row` so that they also
251    /// apply to `row2`.
252    fn duplicate_row_data_validations(
253        &self,
254        ws: &mut XlsxWorksheet,
255        row: i32,
256        row2: i32,
257    ) -> Result<()> {
258        let dvs = match ws.data_validations.as_mut() {
259            Some(dvs) => dvs,
260            None => return Ok(()),
261        };
262        let mut new_dvs: Vec<XlsxDataValidation> = Vec::new();
263        for dv in &dvs.data_validation {
264            let mut sqrefs = Vec::new();
265            for r#ref in dv.sqref.split_whitespace() {
266                if let Some(new_ref) = duplicate_sq_ref_helper(row, row2, r#ref)? {
267                    sqrefs.push(new_ref);
268                }
269            }
270            if !sqrefs.is_empty() {
271                let mut dv_copy = dv.clone();
272                dv_copy.sqref = sqrefs.join(" ");
273                new_dvs.push(dv_copy);
274            }
275        }
276        dvs.data_validation.extend(new_dvs);
277        dvs.count = Some(dvs.data_validation.len() as i64);
278        Ok(())
279    }
280
281    /// Copy single-row merged cells from `row` to `row2`.
282    fn duplicate_row_merge_cells(&self, ws: &mut XlsxWorksheet, row: i32, row2: i32) -> Result<()> {
283        if ws.merge_cells.is_none() {
284            return Ok(());
285        }
286        let mut source_row = row;
287        if source_row > row2 {
288            source_row += 1;
289        }
290
291        // If the target row sits inside an existing multi-row merge, abort.
292        if let Some(merges) = ws.merge_cells.as_ref() {
293            for m in &merges.cells {
294                if let Some(ref_) = &m.r#ref {
295                    if let Ok(coords) = range_ref_to_coordinates(ref_) {
296                        if coords[1] < row2 && row2 < coords[3] {
297                            return Ok(());
298                        }
299                    }
300                }
301            }
302        }
303
304        let refs: Vec<Option<String>> = ws
305            .merge_cells
306            .as_ref()
307            .unwrap()
308            .cells
309            .iter()
310            .map(|m| m.r#ref.clone())
311            .collect();
312        for ref_ in refs.into_iter().flatten() {
313            let mut coords = range_ref_to_coordinates(&ref_)?;
314            sort_coordinates(&mut coords)?;
315            let (x1, y1, x2, y2) = (coords[0], coords[1], coords[2], coords[3]);
316            if y1 == y2 && y1 == source_row {
317                let from = coordinates_to_cell_name(x1, row2, false)?;
318                let to = coordinates_to_cell_name(x2, row2, false)?;
319                crate::merge::add_merge(ws, &from, &to)?;
320            }
321        }
322        Ok(())
323    }
324
325    /// Apply a style index to every cell in a range of rows.
326    ///
327    /// This overwrites existing row and cell styles in the range; it does not
328    /// merge with existing styles. The `start` and `end` row numbers are
329    /// inclusive and will be swapped if `end` is less than `start`.
330    pub fn set_row_style(
331        &self,
332        sheet: &str,
333        mut start: i32,
334        mut end: i32,
335        style_id: i32,
336    ) -> Result<()> {
337        if end < start {
338            std::mem::swap(&mut start, &mut end);
339        }
340        if start < 1 {
341            return Err(crate::errors::new_invalid_row_number_error(start).into());
342        }
343        if end > crate::constants::TOTAL_ROWS {
344            return Err(Box::new(crate::errors::ErrMaxRows));
345        }
346        if style_id < 0 {
347            return Err(crate::errors::new_invalid_style_id_error(style_id).into());
348        }
349        let style_count = self
350            .styles_reader()?
351            .cell_xfs
352            .as_ref()
353            .map(|x| x.xf.len())
354            .unwrap_or(1) as i32;
355        if style_id >= style_count {
356            return Err(crate::errors::new_invalid_style_id_error(style_id).into());
357        }
358        let path = self
359            .get_sheet_xml_path(sheet)
360            .ok_or_else(|| ErrSheetNotExist {
361                sheet_name: sheet.to_string(),
362            })?;
363        let mut ws = self.work_sheet_reader(sheet)?;
364        for row in start..=end {
365            let _ = get_or_make_row(&mut ws, row);
366        }
367        for r in &mut ws.sheet_data.row {
368            if let Some(row_num) = r.r {
369                if row_num >= start as i64 && row_num <= end as i64 {
370                    r.s = Some(style_id as i64);
371                    r.custom_format = Some(true);
372                    for c in &mut r.c {
373                        if let Some(ref cell_name) = c.r {
374                            if let Ok((_, cell_row)) =
375                                crate::lib_util::cell_name_to_coordinates(cell_name)
376                            {
377                                if cell_row == row_num as i32 {
378                                    c.s = Some(style_id as i64);
379                                }
380                            }
381                        } else {
382                            c.s = Some(style_id as i64);
383                        }
384                    }
385                }
386            }
387        }
388        self.sheet.insert(path, ws);
389        Ok(())
390    }
391}
392
393// ------------------------------------------------------------------
394// Row iterator
395// ------------------------------------------------------------------
396
397/// Row formatting attributes returned by `Rows::get_row_opts`.
398#[derive(Debug, Default, Clone, PartialEq)]
399pub struct RowOpts {
400    /// Row height in points.
401    pub height: f64,
402    /// Whether the row is hidden.
403    pub hidden: bool,
404    /// Default style index applied to the row.
405    pub style_id: i32,
406}
407
408/// Streaming row iterator.
409#[derive(Debug)]
410pub struct Rows<'a> {
411    file: &'a File,
412    ws: XlsxWorksheet,
413    cur_row: i32,
414    seek_row: i32,
415    max_row: i32,
416    err: Option<String>,
417    row_opts: RowOpts,
418}
419
420impl<'a> Rows<'a> {
421    /// Advance to the next row. Returns `false` when there are no more rows.
422    pub fn next(&mut self) -> bool {
423        self.seek_row += 1;
424        if self.cur_row >= self.seek_row {
425            return self.cur_row > 0 && self.cur_row <= self.max_row;
426        }
427        if self.seek_row > self.max_row {
428            return false;
429        }
430        self.cur_row = self.seek_row;
431        self.row_opts = self.extract_row_opts();
432        true
433    }
434
435    /// Return the formatting options of the current row.
436    pub fn get_row_opts(&self) -> RowOpts {
437        self.row_opts.clone()
438    }
439
440    /// Return the first error that occurred during iteration, if any.
441    pub fn error(&self) -> Option<&str> {
442        self.err.as_deref()
443    }
444
445    /// Close the iterator. This is a no-op for the in-memory iterator, but it
446    /// matches the Go API.
447    pub fn close(&self) -> Result<()> {
448        if let Some(e) = &self.err {
449            return Err(e.clone().into());
450        }
451        Ok(())
452    }
453
454    /// Return the values of the current row's cells as strings.
455    pub fn columns(&self, opts: Options) -> Result<Vec<String>> {
456        if self.cur_row < 1 || self.cur_row > self.max_row {
457            return Ok(Vec::new());
458        }
459        let row = &self.ws.sheet_data.row[(self.cur_row - 1) as usize];
460        let raw = opts.raw_cell_value;
461        let mut cells: Vec<String> = Vec::new();
462        let mut cell_col: i32 = 0;
463        for c in &row.c {
464            cell_col += 1;
465            if let Some(name) = &c.r {
466                cell_col = cell_name_to_coordinates(name)?.0;
467            }
468            let val = cell_value_string(self.file, c, raw);
469            if !val.is_empty() || c.f.is_some() {
470                let blank = cell_col - cells.len() as i32;
471                for _ in 1..blank {
472                    cells.push(String::new());
473                }
474                cells.push(val);
475            }
476        }
477        Ok(cells)
478    }
479
480    fn extract_row_opts(&self) -> RowOpts {
481        if let Some(row) = self.ws.sheet_data.row.get((self.cur_row - 1) as usize) {
482            RowOpts {
483                height: row.ht.unwrap_or(DEFAULT_ROW_HEIGHT),
484                hidden: row.hidden.unwrap_or(false),
485                style_id: row
486                    .s
487                    .filter(|&s| s > 0 && s < MAX_CELL_STYLES as i64)
488                    .unwrap_or(0) as i32,
489            }
490        } else {
491            RowOpts::default()
492        }
493    }
494}
495
496impl File {
497    /// Return a streaming iterator over the rows of a worksheet.
498    pub fn rows(&self, sheet: &str) -> Result<Rows<'_>> {
499        let mut ws = self.work_sheet_reader(sheet)?;
500        prepare_rows(&mut ws);
501        if ws
502            .sheet_data
503            .row
504            .iter()
505            .any(|r| r.r.unwrap_or(0) > TOTAL_ROWS as i64)
506        {
507            return Err(Box::new(ErrMaxRows));
508        }
509        let max_row = ws
510            .sheet_data
511            .row
512            .last()
513            .map(|r| r.r.unwrap_or(0) as i32)
514            .unwrap_or(0);
515        Ok(Rows {
516            file: self,
517            ws,
518            cur_row: 0,
519            seek_row: 0,
520            max_row,
521            err: None,
522            row_opts: RowOpts::default(),
523        })
524    }
525
526    /// Return all rows in a worksheet as a two-dimensional vector of strings.
527    pub fn get_rows(&self, sheet: &str, opts: Options) -> Result<Vec<Vec<String>>> {
528        let mut rows = self.rows(sheet)?;
529        let mut results = Vec::new();
530        while rows.next() {
531            results.push(rows.columns(opts.clone())?);
532        }
533        Ok(results)
534    }
535}
536
537// ------------------------------------------------------------------
538// Internal helpers
539// ------------------------------------------------------------------
540
541fn get_or_make_row(
542    ws: &mut crate::xml::worksheet::XlsxWorksheet,
543    row: i32,
544) -> &mut crate::xml::worksheet::XlsxRow {
545    if let Some(idx) = ws
546        .sheet_data
547        .row
548        .iter()
549        .position(|r| r.r == Some(row as i64))
550    {
551        return &mut ws.sheet_data.row[idx];
552    }
553    ws.sheet_data.row.push(crate::xml::worksheet::XlsxRow {
554        r: Some(row as i64),
555        ..Default::default()
556    });
557    let last = ws.sheet_data.row.len() - 1;
558    &mut ws.sheet_data.row[last]
559}
560
561/// Rewrite a single-row reference that points to `row` so that it points to
562/// `row2`, preserving absolute references.
563fn duplicate_sq_ref_helper(row: i32, row2: i32, r#ref: &str) -> Result<Option<String>> {
564    let abs = r#ref.contains('$');
565    let mut expanded = r#ref.to_string();
566    if !expanded.contains(':') {
567        expanded.push(':');
568        expanded.push_str(r#ref);
569    }
570    let coords = range_ref_to_coordinates(&expanded)?;
571    let (x1, y1, x2, y2) = (coords[0], coords[1], coords[2], coords[3]);
572    if y1 == y2 && y1 == row {
573        Ok(Some(coordinates_to_range_ref(&[x1, row2, x2, row2], abs)?))
574    } else {
575        Ok(None)
576    }
577}
578
579fn shift_rows_down(ws: &mut crate::xml::worksheet::XlsxWorksheet, start_row: i64, n: i64) {
580    for row in &mut ws.sheet_data.row {
581        if let Some(row_num) = row.r {
582            if row_num >= start_row {
583                row.r = Some(row_num + n);
584                for c in &mut row.c {
585                    if let Some(name) = &c.r {
586                        if let Ok((col, old_row)) = crate::lib_util::cell_name_to_coordinates(name)
587                        {
588                            if let Ok(new_name) = crate::lib_util::coordinates_to_cell_name(
589                                col,
590                                old_row + n as i32,
591                                false,
592                            ) {
593                                c.r = Some(new_name);
594                            }
595                        }
596                    }
597                }
598            }
599        }
600    }
601}
602
603/// Ensure worksheet rows are sorted by row number and contiguous from 1 to the
604/// maximum row number, matching Go's `prepareSheetXML` behavior for iteration.
605pub(crate) fn prepare_rows(ws: &mut XlsxWorksheet) {
606    if ws.sheet_data.row.is_empty() {
607        return;
608    }
609    let mut map: BTreeMap<i64, crate::xml::worksheet::XlsxRow> = BTreeMap::new();
610    let mut max_row = 0_i64;
611    let mut next = 1_i64;
612    for row in ws.sheet_data.row.drain(..) {
613        let r = row.r.unwrap_or(next);
614        max_row = max_row.max(r);
615        if !map.contains_key(&r) {
616            map.insert(r, row);
617        }
618        next = r + 1;
619    }
620    let mut rows = Vec::with_capacity(max_row as usize);
621    for r in 1..=max_row {
622        let mut row = map.remove(&r).unwrap_or_default();
623        row.r = Some(r);
624        rows.push(row);
625    }
626    ws.sheet_data.row = rows;
627}
628
629/// Convert a worksheet cell to its string representation.
630pub(crate) fn cell_value_string(
631    file: &File,
632    c: &crate::xml::worksheet::XlsxC,
633    raw: bool,
634) -> String {
635    let raw_value = match c.t.as_deref() {
636        Some("s") => {
637            if let Some(v) = &c.v {
638                if let Ok(idx) = v.parse::<i32>() {
639                    return read_shared_string(file, idx);
640                }
641            }
642            return String::new();
643        }
644        Some("inlineStr") => {
645            return c.is.as_ref().map(inline_string_text).unwrap_or_default();
646        }
647        Some("b") => {
648            return c.v.as_deref().map_or(String::new(), |v| {
649                if v == "1" {
650                    "TRUE".to_string()
651                } else {
652                    "FALSE".to_string()
653                }
654            });
655        }
656        Some("str") => return c.v.clone().unwrap_or_default(),
657        _ => c.v.clone().unwrap_or_default(),
658    };
659
660    // For formula cells without a cached value, return the formula text.
661    if c.f.is_some() && c.v.is_none() {
662        return format!("={}", c.f.as_ref().unwrap().content);
663    }
664
665    if raw {
666        return raw_value;
667    }
668
669    if let Some(num_fmt_id) = get_cell_num_fmt_id(file, c) {
670        if let Ok(value) = raw_value.parse::<f64>() {
671            let format_code = get_num_fmt_code(file, num_fmt_id);
672            let date1904 = file
673                .workbook_reader()
674                .ok()
675                .and_then(|wb| wb.workbook_pr.as_ref().and_then(|p| p.date1904))
676                .unwrap_or(false);
677            return crate::numfmt::apply_number_format(
678                value,
679                num_fmt_id,
680                format_code.as_deref(),
681                date1904,
682            );
683        }
684    }
685
686    raw_value
687}
688
689fn read_shared_string(file: &File, idx: i32) -> String {
690    let sst = file.shared_strings_reader().unwrap_or_default();
691    sst.si.get(idx as usize).map_or(String::new(), |si| {
692        if let Some(t) = &si.t {
693            t.val.clone()
694        } else {
695            si.r.iter()
696                .filter_map(|r| r.t.as_ref().map(|t| t.val.clone()))
697                .collect()
698        }
699    })
700}
701
702fn inline_string_text(si: &crate::xml::shared_strings::XlsxSi) -> String {
703    if let Some(t) = &si.t {
704        return t.val.clone();
705    }
706    si.r.iter()
707        .filter_map(|r| r.t.as_ref().map(|t| t.val.clone()))
708        .collect()
709}
710
711fn get_cell_num_fmt_id(file: &File, c: &crate::xml::worksheet::XlsxC) -> Option<i32> {
712    let style_id = c.s.unwrap_or(0) as usize;
713    let styles = file.styles_reader().ok()?;
714    let cell_xfs = styles.cell_xfs?;
715    let xf = cell_xfs.xf.get(style_id)?;
716    xf.num_fmt_id.map(|id| id as i32)
717}
718
719fn get_num_fmt_code(file: &File, num_fmt_id: i32) -> Option<String> {
720    let styles = file.styles_reader().ok()?;
721    styles
722        .num_fmts?
723        .num_fmt
724        .into_iter()
725        .find(|n| n.num_fmt_id == num_fmt_id as i64)
726        .map(|n| n.format_code)
727}
728
729#[cfg(test)]
730mod tests {
731    use super::*;
732
733    #[test]
734    fn row_height_round_trip() {
735        let f = File::new_with_options(crate::options::Options::default());
736        f.set_row_height("Sheet1", 2, 30.0).unwrap();
737        assert_eq!(f.get_row_height("Sheet1", 2).unwrap(), 30.0);
738        assert_eq!(f.get_row_height("Sheet1", 3).unwrap(), DEFAULT_ROW_HEIGHT);
739    }
740
741    #[test]
742    fn row_visible_round_trip() {
743        let f = File::new_with_options(crate::options::Options::default());
744        f.set_row_visible("Sheet1", 2, false).unwrap();
745        assert!(!f.get_row_visible("Sheet1", 2).unwrap());
746        assert!(f.get_row_visible("Sheet1", 1).unwrap());
747    }
748
749    #[test]
750    fn set_row_style_range() {
751        let f = File::new_with_options(crate::options::Options::default());
752        let style = f
753            .new_style(&crate::styles::Style {
754                fill: crate::styles::Fill {
755                    r#type: "pattern".to_string(),
756                    pattern: 1,
757                    color: vec!["63BE7B".to_string()],
758                    ..Default::default()
759                },
760                ..Default::default()
761            })
762            .unwrap();
763        f.set_cell_str("Sheet1", "B2", "x").unwrap();
764        f.set_cell_str("Sheet1", "C1", "y").unwrap();
765
766        // Range is applied and out-of-range cells are untouched.
767        f.set_row_style("Sheet1", 2, 3, style).unwrap();
768        assert_eq!(f.get_cell_style("Sheet1", "B2").unwrap(), style);
769        assert_eq!(f.get_cell_style("Sheet1", "A3").unwrap(), style);
770        assert_eq!(f.get_cell_style("Sheet1", "C1").unwrap(), 0);
771
772        // End/start are swapped when necessary.
773        f.set_cell_str("Sheet1", "A5", "z").unwrap();
774        f.set_row_style("Sheet1", 5, 4, style).unwrap();
775        assert_eq!(f.get_cell_style("Sheet1", "A5").unwrap(), style);
776
777        // Error cases match Go behavior.
778        assert!(f.set_row_style("Sheet1", 0, 1, style).is_err());
779        assert!(f.set_row_style("Sheet1", 1, TOTAL_ROWS + 1, style).is_err());
780        assert!(f.set_row_style("Sheet1", 1, 1, -1).is_err());
781        assert!(f.set_row_style("Sheet1", 1, 1, 999).is_err());
782    }
783
784    #[test]
785    fn rows_iterator_and_get_rows() {
786        let f = File::new_with_options(crate::options::Options::default());
787        f.set_cell_str("Sheet1", "A1", "a").unwrap();
788        f.set_cell_str("Sheet1", "C1", "c").unwrap();
789        f.set_cell_str("Sheet1", "A3", "x").unwrap();
790
791        let mut rows = f.rows("Sheet1").unwrap();
792        assert!(rows.next());
793        assert_eq!(
794            rows.columns(Options::default()).unwrap(),
795            vec!["a", "", "c"]
796        );
797        assert!(rows.next());
798        assert_eq!(
799            rows.columns(Options::default()).unwrap(),
800            Vec::<String>::new()
801        );
802        assert!(rows.next());
803        assert_eq!(rows.columns(Options::default()).unwrap(), vec!["x"]);
804        assert!(!rows.next());
805        rows.close().unwrap();
806
807        let all = f.get_rows("Sheet1", Options::default()).unwrap();
808        assert_eq!(all, vec![vec!["a", "", "c"], vec![], vec!["x"]]);
809    }
810
811    #[test]
812    fn rows_iterator_opts() {
813        let mut opts = Options::default();
814        opts.raw_cell_value = true;
815        let f = File::new_with_options(opts.clone());
816        f.set_cell_value(
817            "Sheet1",
818            "A1",
819            crate::cell::CellValue::Date(chrono::NaiveDate::from_ymd_opt(2024, 7, 13).unwrap()),
820        )
821        .unwrap();
822        let mut rows = f.rows("Sheet1").unwrap();
823        assert!(rows.next());
824        assert_eq!(rows.columns(opts).unwrap(), vec!["45486"]);
825    }
826
827    #[test]
828    fn duplicate_row_to_basic() {
829        let f = File::new_with_options(crate::options::Options::default());
830        f.set_cell_str("Sheet1", "A2", "x").unwrap();
831        f.set_cell_str("Sheet1", "B2", "y").unwrap();
832        f.duplicate_row_to("Sheet1", 2, 5).unwrap();
833        assert_eq!(f.get_cell_value("Sheet1", "A2").unwrap(), "x");
834        assert_eq!(f.get_cell_value("Sheet1", "A5").unwrap(), "x");
835        assert_eq!(f.get_cell_value("Sheet1", "B5").unwrap(), "y");
836    }
837
838    #[test]
839    fn remove_row_shifts_formulas_and_values() {
840        let f = File::new_with_options(crate::options::Options::default());
841        f.set_cell_str("Sheet1", "A1", "a").unwrap();
842        f.set_cell_formula("Sheet1", "A3", "A1+1").unwrap();
843        f.set_cell_formula("Sheet1", "A4", "A3+1").unwrap();
844        f.remove_row("Sheet1", 2).unwrap();
845
846        assert_eq!(f.get_cell_formula("Sheet1", "A2").unwrap(), "A1+1");
847        assert_eq!(f.get_cell_formula("Sheet1", "A3").unwrap(), "A2+1");
848    }
849
850    #[test]
851    fn remove_row_clears_shared_formula_siblings() {
852        let f = File::new_with_options(crate::options::Options::default());
853        let path = f.get_sheet_xml_path("Sheet1").unwrap();
854        let mut ws = f.work_sheet_reader("Sheet1").unwrap();
855
856        ws.sheet_data.row.push(crate::xml::worksheet::XlsxRow {
857            r: Some(1),
858            c: vec![crate::xml::worksheet::XlsxC {
859                r: Some("A1".to_string()),
860                f: Some(crate::xml::worksheet::XlsxF {
861                    content: "A2*2".to_string(),
862                    t: Some("shared".to_string()),
863                    r#ref: Some("A1:A3".to_string()),
864                    si: Some(0),
865                    ..Default::default()
866                }),
867                ..Default::default()
868            }],
869            ..Default::default()
870        });
871        ws.sheet_data.row.push(crate::xml::worksheet::XlsxRow {
872            r: Some(2),
873            c: vec![crate::xml::worksheet::XlsxC {
874                r: Some("A2".to_string()),
875                f: Some(crate::xml::worksheet::XlsxF {
876                    t: Some("shared".to_string()),
877                    si: Some(0),
878                    ..Default::default()
879                }),
880                ..Default::default()
881            }],
882            ..Default::default()
883        });
884        ws.sheet_data.row.push(crate::xml::worksheet::XlsxRow {
885            r: Some(3),
886            c: vec![crate::xml::worksheet::XlsxC {
887                r: Some("A3".to_string()),
888                f: Some(crate::xml::worksheet::XlsxF {
889                    t: Some("shared".to_string()),
890                    si: Some(0),
891                    ..Default::default()
892                }),
893                ..Default::default()
894            }],
895            ..Default::default()
896        });
897        f.sheet.insert(path, ws);
898
899        f.remove_row("Sheet1", 1).unwrap();
900        let ws = f.work_sheet_reader("Sheet1").unwrap();
901        assert!(
902            ws.sheet_data
903                .row
904                .iter()
905                .flat_map(|r| &r.c)
906                .all(|c| c.f.is_none())
907        );
908    }
909
910    #[test]
911    fn duplicate_row_to_copies_single_row_merge() {
912        let f = File::new_with_options(crate::options::Options::default());
913        f.set_cell_str("Sheet1", "A1", "merged").unwrap();
914        f.merge_cell("Sheet1", "A1", "B1").unwrap();
915        f.duplicate_row_to("Sheet1", 1, 3).unwrap();
916
917        let merges = f.get_merge_cells("Sheet1").unwrap();
918        assert!(merges.contains(&"A1:B1".to_string()));
919        assert!(merges.contains(&"A3:B3".to_string()));
920        assert_eq!(f.get_cell_value("Sheet1", "A3").unwrap(), "merged");
921    }
922}