Skip to main content

excelize_rs/
cell.rs

1//! Cell-level read/write API.
2//!
3//! This module corresponds to `cell.go` in the Go implementation.
4
5use std::time::Duration;
6
7use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
8
9use crate::constants::{
10    SOURCE_RELATIONSHIP, SOURCE_RELATIONSHIP_HYPER_LINK, TOTAL_SHEET_HYPERLINKS,
11};
12use crate::date;
13use crate::errors::Result;
14use crate::errors::{
15    ErrCellCharsLength, ErrCoordinates, ErrInvalidFormula, ErrParameterInvalid,
16    ErrTotalSheetHyperlinks,
17};
18use crate::file::File;
19use crate::lib_util::{
20    cell_name_to_coordinates, coordinates_to_cell_name, range_ref_to_coordinates, sort_coordinates,
21    split_cell_name,
22};
23use crate::numfmt;
24use crate::xml::common::{RichTextRun, XlsxR, XlsxT};
25use crate::xml::shared_strings::XlsxSi;
26use crate::xml::styles::{XlsxNumFmt, XlsxXf};
27use crate::xml::worksheet::{XlsxC, XlsxF, XlsxHyperlink, XlsxHyperlinks, XlsxWorksheet};
28
29// ------------------------------------------------------------------
30// Public cell value types
31// ------------------------------------------------------------------
32
33/// Value that can be written to a worksheet cell.
34#[derive(Debug, Clone, PartialEq)]
35pub enum CellValue {
36    /// Plain text or string value.
37    String(String),
38    /// Integer value.
39    Int(i64),
40    /// Floating point value.
41    Float(f64),
42    /// Boolean value.
43    Bool(bool),
44    /// Formula string (without leading `=`).
45    Formula(String),
46    /// Date/time value.
47    DateTime(NaiveDateTime),
48    /// Date value.
49    Date(NaiveDate),
50    /// Time value.
51    Time(NaiveTime),
52    /// Time duration stored as a fraction of a day.
53    Duration(Duration),
54    /// Rich text runs.
55    RichText(Vec<RichTextRun>),
56}
57
58impl From<&str> for CellValue {
59    fn from(v: &str) -> Self {
60        CellValue::String(v.to_string())
61    }
62}
63
64impl From<String> for CellValue {
65    fn from(v: String) -> Self {
66        CellValue::String(v)
67    }
68}
69
70impl From<i64> for CellValue {
71    fn from(v: i64) -> Self {
72        CellValue::Int(v)
73    }
74}
75
76impl From<i32> for CellValue {
77    fn from(v: i32) -> Self {
78        CellValue::Int(v as i64)
79    }
80}
81
82impl From<u64> for CellValue {
83    fn from(v: u64) -> Self {
84        CellValue::Int(v as i64)
85    }
86}
87
88impl From<f64> for CellValue {
89    fn from(v: f64) -> Self {
90        CellValue::Float(v)
91    }
92}
93
94impl From<bool> for CellValue {
95    fn from(v: bool) -> Self {
96        CellValue::Bool(v)
97    }
98}
99
100impl From<NaiveDateTime> for CellValue {
101    fn from(v: NaiveDateTime) -> Self {
102        CellValue::DateTime(v)
103    }
104}
105
106impl From<NaiveDate> for CellValue {
107    fn from(v: NaiveDate) -> Self {
108        CellValue::Date(v)
109    }
110}
111
112impl From<NaiveTime> for CellValue {
113    fn from(v: NaiveTime) -> Self {
114        CellValue::Time(v)
115    }
116}
117
118impl From<Duration> for CellValue {
119    fn from(v: Duration) -> Self {
120        CellValue::Duration(v)
121    }
122}
123
124impl From<Vec<RichTextRun>> for CellValue {
125    fn from(v: Vec<RichTextRun>) -> Self {
126        CellValue::RichText(v)
127    }
128}
129
130// ------------------------------------------------------------------
131// Cell data type
132// ------------------------------------------------------------------
133
134/// Cell data type enumeration.
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub enum CellType {
137    /// Unset / blank.
138    Unset,
139    /// Boolean.
140    Bool,
141    /// ISO 8601 date.
142    Date,
143    /// Error.
144    Error,
145    /// Formula string.
146    Formula,
147    /// Inline string.
148    InlineString,
149    /// Number.
150    Number,
151    /// Shared string.
152    SharedString,
153}
154
155impl Default for CellType {
156    fn default() -> Self {
157        CellType::Unset
158    }
159}
160
161impl CellType {
162    fn from_type_attr(t: Option<&str>) -> Self {
163        match t {
164            Some("b") => CellType::Bool,
165            Some("d") => CellType::Date,
166            Some("e") => CellType::Error,
167            Some("str") => CellType::Formula,
168            Some("inlineStr") => CellType::InlineString,
169            Some("s") => CellType::SharedString,
170            Some(_) => CellType::Number,
171            None => CellType::Unset,
172        }
173    }
174}
175
176// ------------------------------------------------------------------
177// Formula / hyperlink options
178// ------------------------------------------------------------------
179
180/// Optional settings for [`File::set_cell_formula_with_opts`].
181#[derive(Debug, Default, Clone, PartialEq, Eq)]
182pub struct FormulaOpts {
183    /// Formula type (`array`, `shared`, `dataTable`, ...).
184    pub r#type: Option<String>,
185    /// Reference range for array/shared formulas.
186    pub r#ref: Option<String>,
187}
188
189/// Optional settings for [`File::set_cell_hyperlink`].
190#[derive(Debug, Default, Clone, PartialEq, Eq)]
191pub struct HyperlinkOpts {
192    /// Display text for the hyperlink.
193    pub display: Option<String>,
194    /// Tooltip text for the hyperlink.
195    pub tooltip: Option<String>,
196}
197
198// ------------------------------------------------------------------
199// Public API
200// ------------------------------------------------------------------
201
202impl File {
203    /// Set the value of a cell.
204    pub fn set_cell_value(
205        &self,
206        sheet: &str,
207        cell: &str,
208        value: impl Into<CellValue>,
209    ) -> Result<()> {
210        let value = value.into();
211        self.set_cell_value_internal(sheet, cell, value)
212    }
213
214    /// Get the raw string value of a cell.
215    pub fn get_cell_value(&self, sheet: &str, cell: &str) -> Result<String> {
216        let raw = self.options.lock().unwrap().raw_cell_value;
217        self.get_cell_value_with_options(sheet, cell, raw)
218    }
219
220    /// Get the formatted value of a cell, optionally bypassing number formatting.
221    pub fn get_cell_value_with_options(
222        &self,
223        sheet: &str,
224        cell: &str,
225        raw: bool,
226    ) -> Result<String> {
227        let ws = self.work_sheet_reader(sheet)?;
228        let cell = merge_cells_parser(&ws, cell);
229        let Some(c) = find_cell(&ws, &cell) else {
230            return Ok(String::new());
231        };
232        Ok(read_cell_value(self, c, raw))
233    }
234
235    /// Get the data type of a cell.
236    pub fn get_cell_type(&self, sheet: &str, cell: &str) -> Result<CellType> {
237        let ws = self.work_sheet_reader(sheet)?;
238        let cell = merge_cells_parser(&ws, cell);
239        let Some(c) = find_cell(&ws, &cell) else {
240            return Ok(CellType::Unset);
241        };
242        if c.f.is_some() {
243            return Ok(CellType::Formula);
244        }
245        Ok(CellType::from_type_attr(c.t.as_deref()))
246    }
247
248    /// Set a cell to a string value.
249    pub fn set_cell_str(&self, sheet: &str, cell: &str, value: &str) -> Result<()> {
250        self.set_cell_value(sheet, cell, CellValue::String(value.to_string()))
251    }
252
253    /// Set a cell to an integer value.
254    pub fn set_cell_int(&self, sheet: &str, cell: &str, value: i64) -> Result<()> {
255        self.set_cell_value(sheet, cell, CellValue::Int(value))
256    }
257
258    /// Set a cell to an unsigned integer value.
259    pub fn set_cell_uint(&self, sheet: &str, cell: &str, value: u64) -> Result<()> {
260        self.set_cell_value(sheet, cell, CellValue::Int(value as i64))
261    }
262
263    /// Set a cell to a floating point value.
264    pub fn set_cell_float(&self, sheet: &str, cell: &str, value: f64) -> Result<()> {
265        self.set_cell_float_with_precision(sheet, cell, value, -1, 64)
266    }
267
268    /// Set a cell to a floating point value with explicit precision and bit size.
269    ///
270    /// `precision` of `-1` uses as many decimal places as necessary. `bit_size`
271    /// should be `32` for `f32` values and `64` for `f64` values.
272    pub fn set_cell_float_with_precision(
273        &self,
274        sheet: &str,
275        cell: &str,
276        value: f64,
277        precision: i32,
278        bit_size: i32,
279    ) -> Result<()> {
280        if value.is_nan() || value.is_infinite() {
281            return self.set_cell_str(sheet, cell, &value.to_string());
282        }
283        let path =
284            self.get_sheet_xml_path(sheet)
285                .ok_or_else(|| crate::errors::ErrSheetNotExist {
286                    sheet_name: sheet.to_string(),
287                })?;
288        let mut ws = self.work_sheet_reader(sheet)?;
289        let c = get_or_make_cell(&mut ws, cell);
290        let v = if precision < 0 {
291            if bit_size <= 32 {
292                format!("{}", value as f32)
293            } else {
294                format!("{}", value)
295            }
296        } else if bit_size <= 32 {
297            format!("{:.*}", precision as usize, value as f32)
298        } else {
299            format!("{:.*}", precision as usize, value)
300        };
301        c.t = None;
302        c.v = Some(v);
303        c.f = None;
304        c.is = None;
305        update_dimension(&mut ws)?;
306        self.sheet.insert(path, ws);
307        Ok(())
308    }
309
310    /// Set a cell to a boolean value.
311    pub fn set_cell_bool(&self, sheet: &str, cell: &str, value: bool) -> Result<()> {
312        self.set_cell_value(sheet, cell, CellValue::Bool(value))
313    }
314
315    /// Set a cell to a string value without escaping it as a shared string.
316    pub fn set_cell_default(&self, sheet: &str, cell: &str, value: &str) -> Result<()> {
317        let path =
318            self.get_sheet_xml_path(sheet)
319                .ok_or_else(|| crate::errors::ErrSheetNotExist {
320                    sheet_name: sheet.to_string(),
321                })?;
322        let mut ws = self.work_sheet_reader(sheet)?;
323        let c = get_or_make_cell(&mut ws, cell);
324        set_cell_default_value(c, value);
325        update_dimension(&mut ws)?;
326        self.sheet.insert(path, ws);
327        Ok(())
328    }
329
330    /// Set a cell formula.
331    pub fn set_cell_formula(&self, sheet: &str, cell: &str, formula: &str) -> Result<()> {
332        self.set_cell_formula_with_opts(sheet, cell, formula, &[])
333    }
334
335    /// Set a cell formula with optional formula type and reference range.
336    pub fn set_cell_formula_with_opts(
337        &self,
338        sheet: &str,
339        cell: &str,
340        formula: &str,
341        opts: &[FormulaOpts],
342    ) -> Result<()> {
343        let path =
344            self.get_sheet_xml_path(sheet)
345                .ok_or_else(|| crate::errors::ErrSheetNotExist {
346                    sheet_name: sheet.to_string(),
347                })?;
348        let mut ws = self.work_sheet_reader(sheet)?;
349        self.clear_calc_cache();
350
351        if formula.is_empty() {
352            if let Some(c) = find_cell_mut(&mut ws, cell) {
353                c.f = None;
354            }
355            self.delete_calc_chain(self.get_sheet_id(sheet), cell)?;
356            update_dimension(&mut ws)?;
357            self.sheet.insert(path, ws);
358            return Ok(());
359        }
360
361        if formula.starts_with('=') {
362            return Err(Box::new(ErrInvalidFormula));
363        }
364
365        let c = get_or_make_cell(&mut ws, cell);
366        let mut f = XlsxF {
367            content: formula.to_string(),
368            ..Default::default()
369        };
370        for opt in opts {
371            if let Some(t) = &opt.r#type {
372                f.t = Some(t.clone());
373            }
374            if let Some(r#ref) = &opt.r#ref {
375                f.r#ref = Some(r#ref.clone());
376            }
377        }
378        c.f = Some(f);
379        c.is = None;
380        update_dimension(&mut ws)?;
381        self.sheet.insert(path, ws);
382        Ok(())
383    }
384
385    /// Get the formula from a cell.
386    pub fn get_cell_formula(&self, sheet: &str, cell: &str) -> Result<String> {
387        let ws = self.work_sheet_reader(sheet)?;
388        let cell = merge_cells_parser(&ws, cell);
389        let Some(c) = find_cell(&ws, &cell) else {
390            return Ok(String::new());
391        };
392        let Some(f) = &c.f else {
393            return Ok(String::new());
394        };
395        if f.t.as_deref() == Some("shared") {
396            if let Some(si) = f.si {
397                if let Some(master) = find_shared_formula_master(&ws, si) {
398                    return Ok(master.content.clone());
399                }
400            }
401        }
402        Ok(f.content.clone())
403    }
404
405    /// Get the style index applied to a cell.
406    ///
407    /// If the cell has no explicit style, the row style and then the column
408    /// style are returned, matching Excel's inheritance behavior.
409    pub fn get_cell_style(&self, sheet: &str, cell: &str) -> Result<i32> {
410        let ws = self.work_sheet_reader(sheet)?;
411        let (col, row) =
412            cell_name_to_coordinates(cell).map_err(|_| Box::new(ErrParameterInvalid))?;
413        let mut style = 0_i64;
414        if let Some(c) = find_cell(&ws, cell) {
415            style = c.s.unwrap_or(0);
416        }
417        if style == 0 {
418            if let Some(r) = ws.sheet_data.row.iter().find(|r| r.r == Some(row as i64)) {
419                style = r.s.unwrap_or(0);
420            }
421        }
422        if style == 0 {
423            if let Some(cols) = &ws.cols {
424                for c in &cols.col {
425                    if c.min <= col as i64 && col as i64 <= c.max {
426                        style = c.style.unwrap_or(0);
427                        if style != 0 {
428                            break;
429                        }
430                    }
431                }
432            }
433        }
434        Ok(style as i32)
435    }
436
437    /// Apply a style index to a range of cells.
438    ///
439    /// The range is defined by `top_left_cell` and `bottom_right_cell`. The
440    /// corners are normalized automatically, so `B3:A1` is equivalent to
441    /// `A1:B3`. Passing the same cell for both corners styles a single cell.
442    pub fn set_cell_style(
443        &self,
444        sheet: &str,
445        top_left_cell: &str,
446        bottom_right_cell: &str,
447        style_id: i32,
448    ) -> Result<()> {
449        if style_id < 0 {
450            return Err(crate::errors::new_invalid_style_id_error(style_id).into());
451        }
452        let style_count = self
453            .styles_reader()?
454            .cell_xfs
455            .as_ref()
456            .map(|x| x.xf.len())
457            .unwrap_or(1) as i32;
458        if style_id >= style_count {
459            return Err(crate::errors::new_invalid_style_id_error(style_id).into());
460        }
461        let (mut h_col, mut h_row) =
462            cell_name_to_coordinates(top_left_cell).map_err(|_| Box::new(ErrParameterInvalid))?;
463        let (mut v_col, mut v_row) = cell_name_to_coordinates(bottom_right_cell)
464            .map_err(|_| Box::new(ErrParameterInvalid))?;
465        if v_col < h_col {
466            std::mem::swap(&mut v_col, &mut h_col);
467        }
468        if v_row < h_row {
469            std::mem::swap(&mut v_row, &mut h_row);
470        }
471        let path =
472            self.get_sheet_xml_path(sheet)
473                .ok_or_else(|| crate::errors::ErrSheetNotExist {
474                    sheet_name: sheet.to_string(),
475                })?;
476        let mut ws = self.work_sheet_reader(sheet)?;
477        for row in h_row..=v_row {
478            for col in h_col..=v_col {
479                let cell = coordinates_to_cell_name(col, row, false)
480                    .map_err(|_| Box::new(ErrParameterInvalid))?;
481                let c = get_or_make_cell(&mut ws, &cell);
482                c.s = Some(style_id as i64);
483            }
484        }
485        self.sheet.insert(path, ws);
486        Ok(())
487    }
488
489    /// Set rich text runs to a cell.
490    pub fn set_cell_rich_text(
491        &self,
492        sheet: &str,
493        cell: &str,
494        runs: Vec<RichTextRun>,
495    ) -> Result<()> {
496        if runs.is_empty() {
497            return self.set_cell_str(sheet, cell, "");
498        }
499        let path =
500            self.get_sheet_xml_path(sheet)
501                .ok_or_else(|| crate::errors::ErrSheetNotExist {
502                    sheet_name: sheet.to_string(),
503                })?;
504        let mut ws = self.work_sheet_reader(sheet)?;
505        let (col, row) = cell_name_to_coordinates(cell).unwrap_or((1, 1));
506        let current_s = {
507            let c = get_or_make_cell(&mut ws, cell);
508            c.s.unwrap_or(0)
509        };
510        let style_id = prepare_cell_style(&ws, col as i64, row as i64, current_s);
511        let c = get_or_make_cell(&mut ws, cell);
512        c.s = Some(style_id);
513
514        let si = runs_to_xlsx_si(&runs);
515        let mut sst = self.shared_strings_reader().unwrap_or_default();
516        let idx = sst
517            .si
518            .iter()
519            .position(|existing| existing == &si)
520            .unwrap_or_else(|| {
521                sst.si.push(si);
522                sst.unique_count += 1;
523                sst.si.len() - 1
524            });
525        sst.count += 1;
526        *self.shared_strings.lock().unwrap() = Some(sst);
527
528        c.t = Some("s".to_string());
529        c.v = Some(idx.to_string());
530        c.f = None;
531        c.is = None;
532        update_dimension(&mut ws)?;
533        self.sheet.insert(path, ws);
534        Ok(())
535    }
536
537    /// Get rich text runs from a cell.
538    pub fn get_cell_rich_text(&self, sheet: &str, cell: &str) -> Result<Vec<RichTextRun>> {
539        let ws = self.work_sheet_reader(sheet)?;
540        let cell = merge_cells_parser(&ws, cell);
541        let Some(c) = find_cell(&ws, &cell) else {
542            return Ok(Vec::new());
543        };
544        if c.t.as_deref() == Some("inlineStr") {
545            if let Some(is) = &c.is {
546                return Ok(runs_from_xlsx_si(is));
547            }
548        }
549        if c.t.as_deref() == Some("s") {
550            if let Some(v) = &c.v {
551                if let Ok(idx) = v.parse::<usize>() {
552                    let sst = self.shared_strings_reader()?;
553                    if let Some(si) = sst.si.get(idx) {
554                        return Ok(runs_from_xlsx_si(si));
555                    }
556                }
557            }
558        }
559        Ok(Vec::new())
560    }
561
562    /// Get a cell hyperlink.
563    ///
564    /// Returns `(true, link)` if the cell has a hyperlink, otherwise
565    /// `(false, "")`.
566    pub fn get_cell_hyperlink(&self, sheet: &str, cell: &str) -> Result<(bool, String)> {
567        split_cell_name(cell).map_err(|_| Box::new(ErrParameterInvalid))?;
568        let ws = self.work_sheet_reader(sheet)?;
569        let cell = merge_cells_parser(&ws, cell);
570        if let Some(links) = &ws.hyperlinks {
571            for link in &links.hyperlink {
572                if link.r#ref == cell {
573                    if let Some(rid) = &link.rid {
574                        return Ok((true, self.get_sheet_relationships_target_by_id(sheet, rid)));
575                    }
576                    return Ok((true, link.location.clone().unwrap_or_default()));
577                }
578                let ok = check_cell_in_range_ref(&cell, &link.r#ref).unwrap_or(false);
579                if ok {
580                    if let Some(rid) = &link.rid {
581                        return Ok((true, self.get_sheet_relationships_target_by_id(sheet, rid)));
582                    }
583                    return Ok((true, link.location.clone().unwrap_or_default()));
584                }
585            }
586        }
587        Ok((false, String::new()))
588    }
589
590    /// Get all cell references which contain hyperlinks in a worksheet.
591    ///
592    /// `link_type` may be `"External"`, `"Location"`, `"None"` or empty.
593    pub fn get_hyperlink_cells(&self, sheet: &str, link_type: &str) -> Result<Vec<String>> {
594        let ws = self.work_sheet_reader(sheet)?;
595        let mut out = Vec::new();
596        if let Some(links) = &ws.hyperlinks {
597            for link in &links.hyperlink {
598                match link_type {
599                    "External" => {
600                        if link.rid.is_some() {
601                            out.push(link.r#ref.clone());
602                        }
603                    }
604                    "Location" => {
605                        if link.location.as_deref().unwrap_or("").is_empty() {
606                            continue;
607                        }
608                        out.push(link.r#ref.clone());
609                    }
610                    "None" => return Ok(out),
611                    "" => out.push(link.r#ref.clone()),
612                    _ => return Err(crate::errors::new_invalid_link_type_error(link_type).into()),
613                }
614            }
615        }
616        Ok(out)
617    }
618
619    /// Set a cell hyperlink.
620    pub fn set_cell_hyperlink(
621        &self,
622        sheet: &str,
623        cell: &str,
624        link: &str,
625        link_type: &str,
626        opts: &[HyperlinkOpts],
627    ) -> Result<()> {
628        split_cell_name(cell).map_err(|_| Box::new(ErrParameterInvalid))?;
629        let path =
630            self.get_sheet_xml_path(sheet)
631                .ok_or_else(|| crate::errors::ErrSheetNotExist {
632                    sheet_name: sheet.to_string(),
633                })?;
634        let mut ws = self.work_sheet_reader(sheet)?;
635        let cell = merge_cells_parser(&ws, cell);
636
637        if link_type == "None" {
638            remove_hyperlink(self, &mut ws, sheet, &cell)?;
639            update_dimension(&mut ws)?;
640            self.sheet.insert(path, ws);
641            return Ok(());
642        }
643
644        if ws.hyperlinks.is_none() {
645            ws.hyperlinks = Some(XlsxHyperlinks::default());
646        }
647        let links = ws.hyperlinks.as_mut().unwrap();
648
649        let existing_idx = links.hyperlink.iter().position(|h| h.r#ref == cell);
650        let mut link_data = existing_idx
651            .and_then(|i| links.hyperlink.get(i).cloned())
652            .unwrap_or_default();
653        link_data.r#ref = cell.clone();
654
655        if links.hyperlink.len() as i32 > TOTAL_SHEET_HYPERLINKS {
656            return Err(Box::new(ErrTotalSheetHyperlinks));
657        }
658
659        match link_type {
660            "External" => {
661                let sheet_path = self.get_sheet_xml_path(sheet).unwrap_or_default();
662                let sheet_rels = format!(
663                    "xl/worksheets/_rels/{}.rels",
664                    sheet_path.trim_start_matches("xl/worksheets/")
665                );
666                let rid_num = self.set_rels(
667                    link_data.rid.as_deref().unwrap_or(""),
668                    &sheet_rels,
669                    SOURCE_RELATIONSHIP_HYPER_LINK,
670                    link,
671                    "External",
672                );
673                link_data = XlsxHyperlink {
674                    r#ref: cell,
675                    rid: Some(format!("rId{rid_num}")),
676                    ..Default::default()
677                };
678                self.add_sheet_name_space(sheet, SOURCE_RELATIONSHIP);
679            }
680            "Location" => {
681                link_data.location = Some(link.to_string());
682                link_data.rid = None;
683            }
684            _ => return Err(crate::errors::new_invalid_link_type_error(link_type).into()),
685        }
686
687        for opt in opts {
688            if let Some(display) = &opt.display {
689                link_data.display = Some(display.clone());
690            }
691            if let Some(tooltip) = &opt.tooltip {
692                link_data.tooltip = Some(tooltip.clone());
693            }
694        }
695
696        if let Some(idx) = existing_idx {
697            links.hyperlink[idx] = link_data;
698        } else {
699            links.hyperlink.push(link_data);
700        }
701        update_dimension(&mut ws)?;
702        self.sheet.insert(path, ws);
703        Ok(())
704    }
705
706    /// Write a row of values starting at the given cell.
707    pub fn set_sheet_row(&self, sheet: &str, cell: &str, values: &[CellValue]) -> Result<()> {
708        let (col, row) =
709            cell_name_to_coordinates(cell).map_err(|_| Box::new(ErrParameterInvalid))?;
710        for (i, value) in values.iter().enumerate() {
711            let cell_name = coordinates_to_cell_name(col + i as i32, row, false)
712                .map_err(|_| Box::new(ErrParameterInvalid))?;
713            self.set_cell_value(sheet, &cell_name, value.clone())?;
714        }
715        Ok(())
716    }
717
718    /// Write a column of values starting at the given cell.
719    pub fn set_sheet_col(&self, sheet: &str, cell: &str, values: &[CellValue]) -> Result<()> {
720        let (col, row) =
721            cell_name_to_coordinates(cell).map_err(|_| Box::new(ErrParameterInvalid))?;
722        for (i, value) in values.iter().enumerate() {
723            let cell_name = coordinates_to_cell_name(col, row + i as i32, false)
724                .map_err(|_| Box::new(ErrParameterInvalid))?;
725            self.set_cell_value(sheet, &cell_name, value.clone())?;
726        }
727        Ok(())
728    }
729}
730
731// ------------------------------------------------------------------
732// Internal helpers
733// ------------------------------------------------------------------
734
735impl File {
736    fn set_cell_value_internal(&self, sheet: &str, cell: &str, value: CellValue) -> Result<()> {
737        let path =
738            self.get_sheet_xml_path(sheet)
739                .ok_or_else(|| crate::errors::ErrSheetNotExist {
740                    sheet_name: sheet.to_string(),
741                })?;
742        let mut ws = self.work_sheet_reader(sheet)?;
743
744        match value {
745            CellValue::String(s) => self.set_string_cell_value(&mut ws, cell, &s),
746            CellValue::Int(n) => {
747                let c = get_or_make_cell(&mut ws, cell);
748                c.t = None;
749                c.v = Some(n.to_string());
750                c.f = None;
751                c.is = None;
752            }
753            CellValue::Float(n) => {
754                let c = get_or_make_cell(&mut ws, cell);
755                c.t = None;
756                c.v = Some(n.to_string());
757                c.f = None;
758                c.is = None;
759            }
760            CellValue::Bool(b) => {
761                let c = get_or_make_cell(&mut ws, cell);
762                c.t = Some("b".to_string());
763                c.v = Some(if b { "1".to_string() } else { "0".to_string() });
764                c.f = None;
765                c.is = None;
766            }
767            CellValue::Formula(f) => {
768                if f.starts_with('=') {
769                    return Err(Box::new(ErrInvalidFormula));
770                }
771                let c = get_or_make_cell(&mut ws, cell);
772                c.t = None;
773                c.v = None;
774                c.f = Some(XlsxF {
775                    content: f,
776                    ..Default::default()
777                });
778                c.is = None;
779            }
780            CellValue::DateTime(dt) => {
781                self.set_default_date_time_style(&mut ws, cell, dt.date(), true)?;
782                let c = get_or_make_cell(&mut ws, cell);
783                c.v = Some(date::datetime_to_excel_serial(dt, self.date_1904()?).to_string());
784                c.t = None;
785                c.f = None;
786                c.is = None;
787            }
788            CellValue::Date(d) => {
789                self.set_default_date_time_style(&mut ws, cell, d, false)?;
790                let c = get_or_make_cell(&mut ws, cell);
791                c.v = Some(date::date_to_excel_serial(d, self.date_1904()?).to_string());
792                c.t = None;
793                c.f = None;
794                c.is = None;
795            }
796            CellValue::Time(t) => {
797                self.set_default_time_style(&mut ws, cell)?;
798                let c = get_or_make_cell(&mut ws, cell);
799                c.v = Some(date::time_to_excel_serial(t).to_string());
800                c.t = None;
801                c.f = None;
802                c.is = None;
803            }
804            CellValue::Duration(d) => {
805                self.set_default_time_style(&mut ws, cell)?;
806                let c = get_or_make_cell(&mut ws, cell);
807                let days = d.as_secs_f64() / 86400.0;
808                c.v = Some(days.to_string());
809                c.t = None;
810                c.f = None;
811                c.is = None;
812            }
813            CellValue::RichText(runs) => {
814                let c = get_or_make_cell(&mut ws, cell);
815                c.t = Some("inlineStr".to_string());
816                c.v = None;
817                c.f = None;
818                c.is = Some(runs_to_xlsx_si(&runs));
819            }
820        }
821
822        update_dimension(&mut ws)?;
823        self.sheet.insert(path, ws);
824        Ok(())
825    }
826
827    /// Remove the formula from a cell while preserving its cached value, if any.
828    ///
829    /// Equivalent to Go `removeFormula`. Shared-formula masters are expanded so
830    /// that every sibling formula cell is also cleared.
831    pub(crate) fn remove_formula(
832        &self,
833        c: &mut XlsxC,
834        ws: &mut XlsxWorksheet,
835        sheet: &str,
836    ) -> Result<()> {
837        self.clear_calc_cache();
838        if c.f.is_some() && c.vm.is_none() {
839            let sheet_id = self.get_sheet_id(sheet);
840            if let Some(ref cell_ref) = c.r {
841                self.delete_calc_chain(sheet_id, cell_ref)?;
842            }
843            let f = c.f.as_ref().unwrap();
844            if f.t.as_deref() == Some("shared")
845                && f.r#ref.as_deref().map_or(false, |s| !s.is_empty())
846            {
847                if let Some(si_val) = f.si {
848                    for row in &mut ws.sheet_data.row {
849                        for cell in &mut row.c {
850                            if cell.f.as_ref().and_then(|ff| ff.si) == Some(si_val) {
851                                if let Some(ref r) = cell.r {
852                                    let _ = self.delete_calc_chain(sheet_id, r);
853                                }
854                                cell.f = None;
855                            }
856                        }
857                    }
858                }
859            }
860            c.f = None;
861        }
862        Ok(())
863    }
864
865    fn set_string_cell_value(&self, ws: &mut XlsxWorksheet, cell: &str, value: &str) {
866        if value.is_empty() {
867            return;
868        }
869        if value.len() > crate::constants::TOTAL_CELL_CHARS {
870            let _ = Err::<(), _>(Box::new(ErrCellCharsLength));
871            return;
872        }
873        let idx = self.shared_string_index(value);
874        let c = get_or_make_cell(ws, cell);
875        c.t = Some("s".to_string());
876        c.v = Some(idx.to_string());
877        c.f = None;
878        c.is = None;
879    }
880
881    fn set_default_time_style(&self, ws: &mut XlsxWorksheet, cell: &str) -> Result<()> {
882        // Time-only values use the built-in time format 21 (hh:mm:ss).
883        self.set_default_date_time_style(ws, cell, NaiveDate::default(), true)
884    }
885
886    fn set_default_date_time_style(
887        &self,
888        ws: &mut XlsxWorksheet,
889        cell: &str,
890        date: NaiveDate,
891        has_time: bool,
892    ) -> Result<()> {
893        let c = get_or_make_cell(ws, cell);
894        if c.s.is_some() {
895            return Ok(());
896        }
897        let num_fmt_id = if has_time && date != NaiveDate::default() {
898            22 // m/d/yy hh:mm
899        } else if has_time {
900            21 // hh:mm:ss
901        } else {
902            14 // mm-dd-yy
903        };
904        let style_id = self.find_or_create_style(num_fmt_id, None)?;
905        c.s = Some(style_id);
906        Ok(())
907    }
908
909    /// Find an existing cell style with `num_fmt_id`, or create one.
910    fn find_or_create_style(&self, num_fmt_id: i32, format_code: Option<&str>) -> Result<i64> {
911        let mut styles = self.styles_reader()?;
912        let cell_xfs = styles.cell_xfs.get_or_insert_with(Default::default);
913
914        for (idx, xf) in cell_xfs.xf.iter().enumerate() {
915            if xf.num_fmt_id == Some(num_fmt_id as i64) {
916                return Ok(idx as i64);
917            }
918        }
919
920        let new_idx = cell_xfs.xf.len() as i64;
921        cell_xfs.xf.push(XlsxXf {
922            num_fmt_id: Some(num_fmt_id as i64),
923            apply_number_format: Some(true),
924            ..Default::default()
925        });
926        cell_xfs.count = cell_xfs.xf.len() as i64;
927
928        // For custom formats (ID >= 164) register the format code in numFmts.
929        if num_fmt_id >= 164 {
930            if let Some(code) = format_code {
931                let num_fmts = styles.num_fmts.get_or_insert_with(Default::default);
932                let custom_id = num_fmt_id;
933                if !num_fmts
934                    .num_fmt
935                    .iter()
936                    .any(|n| n.num_fmt_id == custom_id as i64)
937                {
938                    num_fmts.num_fmt.push(XlsxNumFmt {
939                        num_fmt_id: custom_id as i64,
940                        format_code: code.to_string(),
941                        format_code_16: None,
942                    });
943                    num_fmts.count = num_fmts.num_fmt.len() as i64;
944                }
945            }
946        }
947
948        *self.styles.lock().unwrap() = Some(styles);
949        Ok(new_idx)
950    }
951
952    fn date_1904(&self) -> Result<bool> {
953        let wb = self.workbook_reader()?;
954        Ok(wb
955            .workbook_pr
956            .as_ref()
957            .and_then(|p| p.date1904)
958            .unwrap_or(false))
959    }
960
961    /// Return the number format ID applied to a cell style, if any.
962    fn get_cell_num_fmt_id(&self, c: &XlsxC) -> Option<i32> {
963        let style_id = c.s.unwrap_or(0) as usize;
964        let styles = self.styles_reader().ok()?;
965        let cell_xfs = styles.cell_xfs?;
966        let xf = cell_xfs.xf.get(style_id)?;
967        xf.num_fmt_id.map(|id| id as i32)
968    }
969
970    /// Return the explicit format code for a number format ID, if registered.
971    fn get_num_fmt_code(&self, num_fmt_id: i32) -> Option<String> {
972        let styles = self.styles_reader().ok()?;
973        styles
974            .num_fmts?
975            .num_fmt
976            .into_iter()
977            .find(|n| n.num_fmt_id == num_fmt_id as i64)
978            .map(|n| n.format_code)
979    }
980
981    /// Return the shared-string index for `text`, creating it if necessary.
982    fn shared_string_index(&self, text: &str) -> i32 {
983        {
984            let map = self.shared_strings_map.lock().unwrap();
985            if let Some(&idx) = map.get(text) {
986                return idx;
987            }
988        }
989        let mut sst = self.shared_strings_reader().unwrap_or_default();
990        let mut map = self.shared_strings_map.lock().unwrap();
991        if let Some(&idx) = map.get(text) {
992            return idx;
993        }
994        let idx = map.len() as i32;
995        map.insert(text.to_string(), idx);
996        sst.si.push(XlsxSi {
997            t: Some(XlsxT {
998                space: None,
999                val: text.to_string(),
1000            }),
1001            ..Default::default()
1002        });
1003        sst.unique_count += 1;
1004        sst.count += 1;
1005        *self.shared_strings.lock().unwrap() = Some(sst);
1006        idx
1007    }
1008}
1009
1010// ------------------------------------------------------------------
1011// Worksheet cell helpers
1012// ------------------------------------------------------------------
1013
1014pub(crate) fn find_cell<'a>(ws: &'a XlsxWorksheet, cell: &str) -> Option<&'a XlsxC> {
1015    for row in &ws.sheet_data.row {
1016        for c in &row.c {
1017            if c.r.as_deref() == Some(cell) {
1018                return Some(c);
1019            }
1020        }
1021    }
1022    None
1023}
1024
1025pub(crate) fn find_cell_mut<'a>(ws: &'a mut XlsxWorksheet, cell: &str) -> Option<&'a mut XlsxC> {
1026    for row in &mut ws.sheet_data.row {
1027        for c in &mut row.c {
1028            if c.r.as_deref() == Some(cell) {
1029                return Some(c);
1030            }
1031        }
1032    }
1033    None
1034}
1035
1036fn get_or_make_cell<'a>(ws: &'a mut XlsxWorksheet, cell: &str) -> &'a mut XlsxC {
1037    let (_col, row_num) = cell_name_to_coordinates(cell).unwrap_or((1, 1));
1038    let row_idx = ws
1039        .sheet_data
1040        .row
1041        .iter()
1042        .position(|r| r.r == Some(row_num as i64));
1043    if let Some(idx) = row_idx {
1044        if let Some(pos) = ws.sheet_data.row[idx]
1045            .c
1046            .iter()
1047            .position(|c| c.r.as_deref() == Some(cell))
1048        {
1049            return &mut ws.sheet_data.row[idx].c[pos];
1050        }
1051        let c = XlsxC {
1052            r: Some(cell.to_string()),
1053            ..Default::default()
1054        };
1055        ws.sheet_data.row[idx].c.push(c);
1056        let last = ws.sheet_data.row[idx].c.len() - 1;
1057        return &mut ws.sheet_data.row[idx].c[last];
1058    }
1059    let mut row = crate::xml::worksheet::XlsxRow {
1060        r: Some(row_num as i64),
1061        ..Default::default()
1062    };
1063    row.c.push(XlsxC {
1064        r: Some(cell.to_string()),
1065        ..Default::default()
1066    });
1067    ws.sheet_data.row.push(row);
1068    let last_row = ws.sheet_data.row.len() - 1;
1069    let last_cell = ws.sheet_data.row[last_row].c.len() - 1;
1070    &mut ws.sheet_data.row[last_row].c[last_cell]
1071}
1072
1073fn update_dimension(ws: &mut XlsxWorksheet) -> Result<()> {
1074    if ws.sheet_data.row.is_empty() {
1075        ws.dimension = Some(crate::xml::worksheet::XlsxDimension {
1076            r#ref: "A1".to_string(),
1077        });
1078        return Ok(());
1079    }
1080    let mut min_col = i32::MAX;
1081    let mut min_row = i32::MAX;
1082    let mut max_col = 1;
1083    let mut max_row = 1;
1084    for row in &ws.sheet_data.row {
1085        let row_num = row.r.unwrap_or(1) as i32;
1086        min_row = min_row.min(row_num);
1087        max_row = max_row.max(row_num);
1088        for c in &row.c {
1089            if let Some(name) = &c.r {
1090                if let Ok((col, _)) = cell_name_to_coordinates(name) {
1091                    min_col = min_col.min(col);
1092                    max_col = max_col.max(col);
1093                }
1094            }
1095        }
1096    }
1097    if min_col == i32::MAX {
1098        min_col = 1;
1099    }
1100    let first = coordinates_to_cell_name(min_col, min_row, false)?;
1101    let last = coordinates_to_cell_name(max_col, max_row, false)?;
1102    ws.dimension = Some(crate::xml::worksheet::XlsxDimension {
1103        r#ref: format!("{first}:{last}"),
1104    });
1105    Ok(())
1106}
1107
1108fn set_cell_default_value(c: &mut XlsxC, value: &str) {
1109    c.f = None;
1110    c.is = None;
1111    if value.parse::<f64>().is_ok() {
1112        c.t = None;
1113        c.v = Some(value.to_string());
1114        return;
1115    }
1116    if !value.is_empty() {
1117        c.t = Some("inlineStr".to_string());
1118        c.v = None;
1119        c.is = Some(XlsxSi {
1120            t: Some(XlsxT {
1121                space: None,
1122                val: value.to_string(),
1123            }),
1124            ..Default::default()
1125        });
1126        return;
1127    }
1128    c.t = Some(value.to_string());
1129    c.v = Some(value.to_string());
1130}
1131
1132fn prepare_cell_style(ws: &XlsxWorksheet, col: i64, row: i64, style: i64) -> i64 {
1133    if style != 0 {
1134        return style;
1135    }
1136    if row > 0 && row as usize <= ws.sheet_data.row.len() {
1137        if let Some(style_id) = ws.sheet_data.row[row as usize - 1].s {
1138            if style_id != 0 {
1139                return style_id;
1140            }
1141        }
1142    }
1143    if let Some(cols) = &ws.cols {
1144        for c in &cols.col {
1145            if c.min <= col && col <= c.max {
1146                if let Some(style_id) = c.style {
1147                    if style_id != 0 {
1148                        return style_id;
1149                    }
1150                }
1151            }
1152        }
1153    }
1154    style
1155}
1156
1157// ------------------------------------------------------------------
1158// Merge cell helpers
1159// ------------------------------------------------------------------
1160
1161fn merge_cells_parser(ws: &XlsxWorksheet, cell: &str) -> String {
1162    let upper = cell.to_uppercase();
1163    let Ok((col, row)) = cell_name_to_coordinates(&upper) else {
1164        return upper;
1165    };
1166    if let Some(merges) = &ws.merge_cells {
1167        for mc in &merges.cells {
1168            let mut r#ref = mc.r#ref.clone().unwrap_or_default();
1169            if r#ref.is_empty() {
1170                continue;
1171            }
1172            if !r#ref.contains(':') {
1173                r#ref = format!("{ref}:{ref}", ref = r#ref);
1174            }
1175            let Ok(mut coords) = range_ref_to_coordinates(&r#ref) else {
1176                continue;
1177            };
1178            let _ = sort_coordinates(&mut coords);
1179            if cell_in_range(&[col, row], &coords) {
1180                return r#ref.split(':').next().unwrap_or(&upper).to_string();
1181            }
1182        }
1183    }
1184    upper
1185}
1186
1187fn check_cell_in_range_ref(cell: &str, range_ref: &str) -> Result<bool> {
1188    let (col, row) = cell_name_to_coordinates(cell).map_err(|_| Box::new(ErrParameterInvalid))?;
1189    if !range_ref.contains(':') {
1190        return Ok(false);
1191    }
1192    let mut coords =
1193        range_ref_to_coordinates(range_ref).map_err(|_| Box::new(ErrParameterInvalid))?;
1194    sort_coordinates(&mut coords).map_err(|_| Box::new(ErrCoordinates))?;
1195    Ok(cell_in_range(&[col, row], &coords))
1196}
1197
1198fn cell_in_range(cell: &[i32], rect: &[i32]) -> bool {
1199    cell.len() >= 2
1200        && rect.len() >= 4
1201        && cell[0] >= rect[0]
1202        && cell[0] <= rect[2]
1203        && cell[1] >= rect[1]
1204        && cell[1] <= rect[3]
1205}
1206
1207// ------------------------------------------------------------------
1208// Shared formula helpers
1209// ------------------------------------------------------------------
1210
1211fn find_shared_formula_master<'a>(ws: &'a XlsxWorksheet, si: i64) -> Option<&'a XlsxF> {
1212    for row in &ws.sheet_data.row {
1213        for c in &row.c {
1214            if let Some(f) = &c.f {
1215                if f.t.as_deref() == Some("shared")
1216                    && f.si == Some(si)
1217                    && !f.r#ref.as_deref().unwrap_or("").is_empty()
1218                {
1219                    return Some(f);
1220                }
1221            }
1222        }
1223    }
1224    None
1225}
1226
1227// ------------------------------------------------------------------
1228// Hyperlink helpers
1229// ------------------------------------------------------------------
1230
1231fn remove_hyperlink(file: &File, ws: &mut XlsxWorksheet, sheet: &str, cell: &str) -> Result<()> {
1232    if ws.hyperlinks.is_none() {
1233        return Ok(());
1234    }
1235    let links = ws.hyperlinks.as_mut().unwrap();
1236    let mut i = 0;
1237    while i < links.hyperlink.len() {
1238        let link = &links.hyperlink[i];
1239        let remove =
1240            link.r#ref == cell || check_cell_in_range_ref(cell, &link.r#ref).unwrap_or(false);
1241        if remove {
1242            if let Some(rid) = &link.rid {
1243                file.delete_sheet_relationships(sheet, rid);
1244            }
1245            links.hyperlink.remove(i);
1246        } else {
1247            i += 1;
1248        }
1249    }
1250    if links.hyperlink.is_empty() {
1251        ws.hyperlinks = None;
1252    }
1253    Ok(())
1254}
1255
1256// ------------------------------------------------------------------
1257// Value reading helpers
1258// ------------------------------------------------------------------
1259
1260pub(crate) fn read_cell_value(file: &File, c: &XlsxC, raw: bool) -> String {
1261    let raw_value = match c.t.as_deref() {
1262        Some("s") => {
1263            if let Some(v) = &c.v {
1264                if let Ok(idx) = v.parse::<i32>() {
1265                    return read_shared_string(file, idx);
1266                }
1267            }
1268            return String::new();
1269        }
1270        Some("inlineStr") => {
1271            return c
1272                .is
1273                .as_ref()
1274                .map(|is| inline_string_text(is))
1275                .unwrap_or_default();
1276        }
1277        Some("b") => {
1278            return c
1279                .v
1280                .as_deref()
1281                .map(|v| {
1282                    if v == "1" {
1283                        "TRUE".to_string()
1284                    } else {
1285                        "FALSE".to_string()
1286                    }
1287                })
1288                .unwrap_or_default();
1289        }
1290        Some("str") => return c.v.clone().unwrap_or_default(),
1291        _ => c.v.clone().unwrap_or_default(),
1292    };
1293
1294    // For formula cells without cached value, return the formula.
1295    if c.f.is_some() && c.v.is_none() {
1296        return format!("={}", c.f.as_ref().unwrap().content);
1297    }
1298
1299    // Apply number formatting unless the caller requested raw values.
1300    if raw {
1301        return raw_value;
1302    }
1303
1304    if let Some(num_fmt_id) = file.get_cell_num_fmt_id(c) {
1305        if let Ok(value) = raw_value.parse::<f64>() {
1306            let format_code = file.get_num_fmt_code(num_fmt_id);
1307            let date1904 = file.date_1904().unwrap_or(false);
1308            return numfmt::apply_number_format(
1309                value,
1310                num_fmt_id,
1311                format_code.as_deref(),
1312                date1904,
1313            );
1314        }
1315    }
1316
1317    raw_value
1318}
1319
1320fn read_shared_string(file: &File, idx: i32) -> String {
1321    let sst = file.shared_strings_reader().unwrap_or_default();
1322    sst.si
1323        .get(idx as usize)
1324        .map(|si| {
1325            if let Some(t) = &si.t {
1326                t.val.clone()
1327            } else {
1328                si.r.iter()
1329                    .filter_map(|r| r.t.as_ref().map(|t| t.val.clone()))
1330                    .collect()
1331            }
1332        })
1333        .unwrap_or_default()
1334}
1335
1336fn inline_string_text(si: &XlsxSi) -> String {
1337    if let Some(t) = &si.t {
1338        return t.val.clone();
1339    }
1340    si.r.iter()
1341        .filter_map(|r| r.t.as_ref().map(|t| t.val.clone()))
1342        .collect()
1343}
1344
1345// ------------------------------------------------------------------
1346// Rich text conversion
1347// ------------------------------------------------------------------
1348
1349pub(crate) fn runs_to_xlsx_si(runs: &[RichTextRun]) -> XlsxSi {
1350    let mut r = Vec::new();
1351    for run in runs {
1352        let mut xrun = XlsxR::default();
1353        if !run.text.is_empty() {
1354            let (text, space) = trim_cell_value(&run.text);
1355            xrun.t = Some(XlsxT { space, val: text });
1356        }
1357        if let Some(font) = &run.font {
1358            let mut rpr = crate::xml::common::XlsxRPr::default();
1359            if let Some(name) = &font.name {
1360                rpr.r_font = Some(crate::xml::common::AttrValString {
1361                    val: Some(name.clone()),
1362                });
1363            }
1364            if let Some(size) = font.size {
1365                rpr.sz = Some(crate::xml::common::AttrValFloat { val: Some(size) });
1366            }
1367            if let Some(family) = font.family {
1368                rpr.family = Some(crate::xml::common::AttrValInt { val: Some(family) });
1369            }
1370            if let Some(charset) = font.charset {
1371                rpr.charset = Some(crate::xml::common::AttrValInt { val: Some(charset) });
1372            }
1373            if let Some(bold) = font.bold {
1374                rpr.b = Some(crate::xml::common::AttrValBool { val: Some(bold) });
1375            }
1376            if let Some(italic) = font.italic {
1377                rpr.i = Some(crate::xml::common::AttrValBool { val: Some(italic) });
1378            }
1379            if let Some(strike) = font.strike {
1380                rpr.strike = Some(crate::xml::common::AttrValBool { val: Some(strike) });
1381            }
1382            if let Some(underline) = &font.underline {
1383                rpr.u = Some(crate::xml::common::AttrValString {
1384                    val: Some(underline.clone()),
1385                });
1386            }
1387            if let Some(color) = &font.color {
1388                rpr.color = Some(crate::xml::common::XlsxColor {
1389                    rgb: Some(color.clone()),
1390                    ..Default::default()
1391                });
1392            }
1393            if let Some(vert_align) = &font.vert_align {
1394                rpr.vert_align = Some(crate::xml::common::AttrValString {
1395                    val: Some(vert_align.clone()),
1396                });
1397            }
1398            xrun.r_pr = Some(rpr);
1399        }
1400        r.push(xrun);
1401    }
1402    XlsxSi {
1403        t: None,
1404        r,
1405        ..Default::default()
1406    }
1407}
1408
1409fn runs_from_xlsx_si(si: &XlsxSi) -> Vec<RichTextRun> {
1410    let mut runs = Vec::new();
1411    if let Some(t) = &si.t {
1412        runs.push(RichTextRun {
1413            text: t.val.clone(),
1414            ..Default::default()
1415        });
1416    }
1417    for xrun in &si.r {
1418        let mut run = RichTextRun::default();
1419        if let Some(t) = &xrun.t {
1420            run.text = t.val.clone();
1421        }
1422        if let Some(rpr) = &xrun.r_pr {
1423            let mut font = crate::styles::Font::default();
1424            font.name = rpr.r_font.as_ref().and_then(|f| f.val.clone());
1425            font.size = rpr.sz.as_ref().and_then(|s| s.val);
1426            font.family = rpr.family.as_ref().and_then(|f| f.val);
1427            font.charset = rpr.charset.as_ref().and_then(|f| f.val);
1428            font.bold = rpr.b.as_ref().and_then(|b| b.val);
1429            font.italic = rpr.i.as_ref().and_then(|i| i.val);
1430            font.strike = rpr.strike.as_ref().and_then(|s| s.val);
1431            font.underline = rpr.u.as_ref().and_then(|u| u.val.clone());
1432            font.color = rpr.color.as_ref().and_then(|c| c.rgb.clone());
1433            font.vert_align = rpr.vert_align.as_ref().and_then(|v| v.val.clone());
1434            run.font = Some(font);
1435        }
1436        runs.push(run);
1437    }
1438    runs
1439}
1440
1441fn trim_cell_value(value: &str) -> (String, Option<String>) {
1442    let mut space = None;
1443    if !value.is_empty() {
1444        let prefix = value.as_bytes()[0];
1445        let suffix = value.as_bytes()[value.len() - 1];
1446        for &ascii in &[9u8, 10, 13, 32] {
1447            if prefix == ascii || suffix == ascii {
1448                space = Some("preserve".to_string());
1449                break;
1450            }
1451        }
1452    }
1453    (value.to_string(), space)
1454}
1455
1456#[cfg(test)]
1457mod tests {
1458    use super::*;
1459    use chrono::{NaiveDate, NaiveTime};
1460
1461    #[test]
1462    fn set_and_get_string() {
1463        let f = File::new_with_options(crate::options::Options::default());
1464        f.set_cell_str("Sheet1", "A1", "hello").unwrap();
1465        assert_eq!(f.get_cell_value("Sheet1", "A1").unwrap(), "hello");
1466    }
1467
1468    #[test]
1469    fn set_cell_style_range() {
1470        let f = File::new_with_options(crate::options::Options::default());
1471        let style = f
1472            .new_style(&crate::styles::Style {
1473                fill: crate::styles::Fill {
1474                    r#type: "pattern".to_string(),
1475                    pattern: 1,
1476                    color: vec!["63BE7B".to_string()],
1477                    ..Default::default()
1478                },
1479                ..Default::default()
1480            })
1481            .unwrap();
1482
1483        f.set_cell_style("Sheet1", "B2", "C3", style).unwrap();
1484        assert_eq!(f.get_cell_style("Sheet1", "B2").unwrap(), style);
1485        assert_eq!(f.get_cell_style("Sheet1", "C3").unwrap(), style);
1486        assert_eq!(f.get_cell_style("Sheet1", "A1").unwrap(), 0);
1487        assert_eq!(f.get_cell_style("Sheet1", "D4").unwrap(), 0);
1488
1489        // Reversed corners are normalized.
1490        f.set_cell_style("Sheet1", "E5", "D4", style).unwrap();
1491        assert_eq!(f.get_cell_style("Sheet1", "D4").unwrap(), style);
1492        assert_eq!(f.get_cell_style("Sheet1", "E5").unwrap(), style);
1493
1494        // Single cell still works.
1495        f.set_cell_style("Sheet1", "A1", "A1", style).unwrap();
1496        assert_eq!(f.get_cell_style("Sheet1", "A1").unwrap(), style);
1497    }
1498
1499    #[test]
1500    fn set_and_get_int() {
1501        let f = File::new_with_options(crate::options::Options::default());
1502        f.set_cell_int("Sheet1", "B2", 42).unwrap();
1503        assert_eq!(f.get_cell_value("Sheet1", "B2").unwrap(), "42");
1504    }
1505
1506    #[test]
1507    fn set_and_get_bool() {
1508        let f = File::new_with_options(crate::options::Options::default());
1509        f.set_cell_bool("Sheet1", "C3", true).unwrap();
1510        assert_eq!(f.get_cell_value("Sheet1", "C3").unwrap(), "TRUE");
1511    }
1512
1513    #[test]
1514    fn set_and_get_formula() {
1515        let f = File::new_with_options(crate::options::Options::default());
1516        f.set_cell_formula("Sheet1", "D4", "SUM(A1:A3)").unwrap();
1517        assert_eq!(f.get_cell_value("Sheet1", "D4").unwrap(), "=SUM(A1:A3)");
1518        assert_eq!(f.get_cell_formula("Sheet1", "D4").unwrap(), "SUM(A1:A3)");
1519    }
1520
1521    #[test]
1522    fn set_and_get_date() {
1523        let f = File::new_with_options(crate::options::Options::default());
1524        let d = NaiveDate::from_ymd_opt(2024, 7, 13).unwrap();
1525        f.set_cell_value("Sheet1", "E5", CellValue::Date(d))
1526            .unwrap();
1527        assert_eq!(f.get_cell_value("Sheet1", "E5").unwrap(), "07-13-24");
1528    }
1529
1530    #[test]
1531    fn set_and_get_datetime() {
1532        let f = File::new_with_options(crate::options::Options::default());
1533        let dt = NaiveDate::from_ymd_opt(2024, 7, 13)
1534            .unwrap()
1535            .and_hms_opt(12, 30, 0)
1536            .unwrap();
1537        f.set_cell_value("Sheet1", "F6", CellValue::DateTime(dt))
1538            .unwrap();
1539        assert_eq!(f.get_cell_value("Sheet1", "F6").unwrap(), "7/13/24 12:30");
1540    }
1541
1542    #[test]
1543    fn set_and_get_time() {
1544        let f = File::new_with_options(crate::options::Options::default());
1545        let t = NaiveTime::from_hms_opt(14, 30, 0).unwrap();
1546        f.set_cell_value("Sheet1", "G7", CellValue::Time(t))
1547            .unwrap();
1548        assert_eq!(f.get_cell_value("Sheet1", "G7").unwrap(), "14:30:00");
1549    }
1550
1551    #[test]
1552    fn raw_cell_value_respects_option() {
1553        let mut opts = crate::options::Options::default();
1554        opts.raw_cell_value = true;
1555        let f = File::new_with_options(opts);
1556        let d = NaiveDate::from_ymd_opt(2024, 7, 13).unwrap();
1557        f.set_cell_value("Sheet1", "H8", CellValue::Date(d))
1558            .unwrap();
1559        assert_eq!(f.get_cell_value("Sheet1", "H8").unwrap(), "45486");
1560    }
1561
1562    #[test]
1563    fn set_and_get_default() {
1564        let f = File::new_with_options(crate::options::Options::default());
1565        f.set_cell_default("Sheet1", "A1", "123").unwrap();
1566        assert_eq!(f.get_cell_value("Sheet1", "A1").unwrap(), "123");
1567        f.set_cell_default("Sheet1", "A2", "text").unwrap();
1568        assert_eq!(f.get_cell_value("Sheet1", "A2").unwrap(), "text");
1569    }
1570
1571    #[test]
1572    fn set_and_get_uint() {
1573        let f = File::new_with_options(crate::options::Options::default());
1574        f.set_cell_uint("Sheet1", "A1", 42).unwrap();
1575        assert_eq!(f.get_cell_value("Sheet1", "A1").unwrap(), "42");
1576    }
1577
1578    #[test]
1579    fn set_and_get_float_precision() {
1580        let f = File::new_with_options(crate::options::Options::default());
1581        f.set_cell_float_with_precision("Sheet1", "A1", 1.325, 2, 32)
1582            .unwrap();
1583        assert_eq!(f.get_cell_value("Sheet1", "A1").unwrap(), "1.33");
1584    }
1585
1586    #[test]
1587    fn set_and_get_sheet_row_and_col() {
1588        let f = File::new_with_options(crate::options::Options::default());
1589        f.set_sheet_row("Sheet1", "A1", &["a".into(), "b".into(), 1.into()])
1590            .unwrap();
1591        f.set_sheet_col("Sheet1", "B5", &["x".into(), "y".into()])
1592            .unwrap();
1593        assert_eq!(f.get_cell_value("Sheet1", "A1").unwrap(), "a");
1594        assert_eq!(f.get_cell_value("Sheet1", "C1").unwrap(), "1");
1595        assert_eq!(f.get_cell_value("Sheet1", "B6").unwrap(), "y");
1596    }
1597
1598    #[test]
1599    fn hyperlink_round_trip() {
1600        let f = File::new_with_options(crate::options::Options::default());
1601        f.set_cell_hyperlink("Sheet1", "A1", "https://example.com", "External", &[])
1602            .unwrap();
1603        let (has, link) = f.get_cell_hyperlink("Sheet1", "A1").unwrap();
1604        assert!(has);
1605        assert_eq!(link, "https://example.com");
1606
1607        let cells = f.get_hyperlink_cells("Sheet1", "").unwrap();
1608        assert_eq!(cells, vec!["A1"]);
1609
1610        f.set_cell_hyperlink("Sheet1", "A1", "", "None", &[])
1611            .unwrap();
1612        let (has, _) = f.get_cell_hyperlink("Sheet1", "A1").unwrap();
1613        assert!(!has);
1614    }
1615
1616    #[test]
1617    fn rich_text_round_trip_shared_string() {
1618        let f = File::new_with_options(crate::options::Options::default());
1619        let runs = vec![
1620            RichTextRun {
1621                text: "bold".to_string(),
1622                font: Some(crate::styles::Font {
1623                    bold: Some(true),
1624                    ..Default::default()
1625                }),
1626            },
1627            RichTextRun {
1628                text: " plain".to_string(),
1629                ..Default::default()
1630            },
1631        ];
1632        f.set_cell_rich_text("Sheet1", "A1", runs.clone()).unwrap();
1633        let got = f.get_cell_rich_text("Sheet1", "A1").unwrap();
1634        assert_eq!(got, runs);
1635    }
1636}