Skip to main content

excelize_rs/
styles.rs

1//! Public style API and XML conversion.
2//!
3//! Ported from Go `styles.go` and `xmlStyles.go`.
4
5use std::collections::HashMap;
6use std::sync::LazyLock;
7
8use quick_xml::de::from_reader as xml_from_reader;
9use quick_xml::se::to_string as xml_to_string;
10use serde::{Deserialize, Serialize};
11
12use crate::constants::{
13    EXT_URI_CONDITIONAL_FORMATTING_RULE_ID, EXT_URI_CONDITIONAL_FORMATTINGS, MAX_CELL_STYLES,
14    MAX_COLUMNS, MAX_FONT_FAMILY_LENGTH, MAX_FONT_SIZE, MIN_FONT_SIZE,
15    NAMESPACE_SPREADSHEET_EXCEL_2006_MAIN, NAMESPACE_SPREADSHEET_X14, TOTAL_ROWS,
16    WORKSHEET_EXT_URI_PRIORITY,
17};
18use crate::data_validation::delete_cells_from_sqref;
19use crate::errors::{
20    ErrCellStyles, ErrCustomNumFmt, ErrFillGradientColor, ErrFillGradientShading, ErrFillPattern,
21    ErrFillPatternColor, ErrFillType, ErrFontLength, ErrFontSize, ErrParameterInvalid,
22    ErrParameterRequired, Result,
23};
24use crate::file::File;
25use crate::hsl::theme_color;
26use crate::lib_util::{
27    cell_name_to_coordinates, column_name_to_number, coordinates_to_cell_name, count_utf16_string,
28    flat_sqref, in_str_slice,
29};
30use crate::templates::{
31    INDEXED_COLOR_MAPPING, SUPPORTED_UNDERLINE_TYPES, SUPPORTED_VERT_ALIGN_TYPES,
32};
33use crate::xml::common::{
34    AttrValBool, AttrValFloat, AttrValInt, AttrValString, XlsxColor, XlsxExt, XlsxExtLst,
35};
36use crate::xml::styles::{
37    XlsxAlignment, XlsxBorder, XlsxBorders, XlsxCellStyles, XlsxCellXfs, XlsxDxf, XlsxDxfs,
38    XlsxFill, XlsxFills, XlsxFont, XlsxFonts, XlsxGradientFill, XlsxGradientFillStop, XlsxLine,
39    XlsxNumFmt, XlsxNumFmts, XlsxPatternFill, XlsxProtection, XlsxStyleSheet, XlsxXf,
40};
41use crate::xml::worksheet::{
42    ConditionalFormatOptions, DecodeX14CfRule, DecodeX14Cfvo, DecodeX14ConditionalFormattingRules,
43    DecodeX14DataBar, DecodeX14IconSet, Xlsx14Cfvo, Xlsx14DataBar, Xlsx14IconSet, XlsxCfRule,
44    XlsxCfvo, XlsxColorScale, XlsxConditionalFormatting, XlsxDataBar, XlsxIconSet, XlsxWorksheet,
45    XlsxX14CfRule, XlsxX14ConditionalFormatting,
46};
47
48// ------------------------------------------------------------------
49// Public types
50// ------------------------------------------------------------------
51
52/// Directly maps the alignment settings of the cells.
53#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
54pub struct Alignment {
55    #[serde(default)]
56    pub horizontal: String,
57    #[serde(default)]
58    pub indent: i64,
59    #[serde(default)]
60    pub justify_last_line: bool,
61    #[serde(default)]
62    pub reading_order: u64,
63    #[serde(default)]
64    pub relative_indent: i64,
65    #[serde(default)]
66    pub shrink_to_fit: bool,
67    #[serde(default)]
68    pub text_rotation: i64,
69    #[serde(default)]
70    pub vertical: String,
71    #[serde(default)]
72    pub wrap_text: bool,
73}
74
75/// Directly maps the font settings.
76#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
77pub struct Font {
78    #[serde(rename = "sz", default, skip_serializing_if = "Option::is_none")]
79    pub size: Option<f64>,
80    /// Font family name (e.g. `"Calibri"`). Equivalent to Go `Font.Family`.
81    #[serde(rename = "name", default, skip_serializing_if = "Option::is_none")]
82    pub name: Option<String>,
83    /// Font family number (e.g. `2` for Swiss).
84    #[serde(rename = "family", default, skip_serializing_if = "Option::is_none")]
85    pub family: Option<i64>,
86    #[serde(rename = "b", default, skip_serializing_if = "Option::is_none")]
87    pub bold: Option<bool>,
88    #[serde(rename = "i", default, skip_serializing_if = "Option::is_none")]
89    pub italic: Option<bool>,
90    #[serde(rename = "strike", default, skip_serializing_if = "Option::is_none")]
91    pub strike: Option<bool>,
92    #[serde(rename = "u", default, skip_serializing_if = "Option::is_none")]
93    pub underline: Option<String>,
94    /// RGB color in `RRGGBB` notation.
95    #[serde(rename = "color", default, skip_serializing_if = "Option::is_none")]
96    pub color: Option<String>,
97    #[serde(default, skip_serializing_if = "Option::is_none")]
98    pub color_indexed: Option<i64>,
99    #[serde(default, skip_serializing_if = "Option::is_none")]
100    pub color_theme: Option<i64>,
101    #[serde(default)]
102    pub color_tint: f64,
103    #[serde(default, skip_serializing_if = "Option::is_none")]
104    pub vert_align: Option<String>,
105    #[serde(default, skip_serializing_if = "Option::is_none")]
106    pub charset: Option<i64>,
107}
108
109/// Directly maps the border settings of cells.
110#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
111pub struct Border {
112    #[serde(default)]
113    pub r#type: String,
114    #[serde(default)]
115    pub color: String,
116    #[serde(default)]
117    pub style: i64,
118}
119
120/// Directly maps the fill settings of cells. Supports both `gradient` and
121/// `pattern` fills via the `r#type` field.
122#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
123pub struct Fill {
124    #[serde(default)]
125    pub r#type: String,
126    #[serde(default)]
127    pub pattern: i64,
128    #[serde(default)]
129    pub color: Vec<String>,
130    #[serde(default)]
131    pub shading: i64,
132    #[serde(default)]
133    pub transparency: i64,
134}
135
136/// Directly maps the protection settings of cells.
137#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
138pub struct Protection {
139    #[serde(default)]
140    pub hidden: bool,
141    #[serde(default)]
142    pub locked: bool,
143}
144
145/// Directly maps the style settings of cells.
146#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
147pub struct Style {
148    #[serde(default)]
149    pub border: Vec<Border>,
150    #[serde(default)]
151    pub fill: Fill,
152    #[serde(default)]
153    pub font: Option<Font>,
154    #[serde(default)]
155    pub alignment: Option<Alignment>,
156    #[serde(default)]
157    pub protection: Option<Protection>,
158    #[serde(default)]
159    pub num_fmt: i64,
160    #[serde(default)]
161    pub decimal_places: Option<i64>,
162    #[serde(default)]
163    pub custom_num_fmt: Option<String>,
164    #[serde(default)]
165    pub neg_red: bool,
166}
167
168// ------------------------------------------------------------------
169// Public API
170// ------------------------------------------------------------------
171
172impl File {
173    /// Create a new cell style and return its style ID.
174    ///
175    /// Equivalent to Go `NewStyle`.
176    pub fn new_style(&self, style: &Style) -> Result<i32> {
177        let mut ss = self.styles.lock().unwrap();
178        let mut style_sheet = ss.take().unwrap_or_default();
179
180        let mut fs = parse_format_style_set(style)?;
181
182        if let Some(dp) = fs.decimal_places {
183            if dp < 0 || dp > 30 {
184                // Match Go behavior: reset out-of-range decimal places to 2.
185                fs.decimal_places = Some(2);
186            }
187        }
188
189        let existing = get_style_id(&style_sheet, &fs);
190        if existing != -1 {
191            *ss = Some(style_sheet);
192            return Ok(existing);
193        }
194
195        let num_fmt_id = new_num_fmt(&mut style_sheet, &fs);
196
197        let font_id = if let Some(ref font) = fs.font {
198            let fid = get_font_id(&style_sheet, font);
199            if fid == -1 {
200                ensure_fonts(&mut style_sheet);
201                style_sheet
202                    .fonts
203                    .as_mut()
204                    .unwrap()
205                    .font
206                    .push(new_font(font));
207                let id = style_sheet.fonts.as_ref().unwrap().count;
208                style_sheet.fonts.as_mut().unwrap().count = id + 1;
209                id as i32
210            } else {
211                fid
212            }
213        } else {
214            0
215        };
216
217        let border_id = {
218            let bid = get_border_id(&style_sheet, &fs);
219            if bid == -1 {
220                if fs.border.is_empty() {
221                    0
222                } else {
223                    ensure_borders(&mut style_sheet);
224                    style_sheet
225                        .borders
226                        .as_mut()
227                        .unwrap()
228                        .border
229                        .push(new_borders(&fs));
230                    let id = style_sheet.borders.as_ref().unwrap().count;
231                    style_sheet.borders.as_mut().unwrap().count = id + 1;
232                    id as i32
233                }
234            } else {
235                bid
236            }
237        };
238
239        let fill_id = {
240            let fid = get_fill_id(&style_sheet, &fs);
241            if fid == -1 {
242                if let Some(xfill) = new_fills(&fs, true) {
243                    ensure_fills(&mut style_sheet);
244                    style_sheet.fills.as_mut().unwrap().fill.push(xfill);
245                    let id = style_sheet.fills.as_ref().unwrap().count;
246                    style_sheet.fills.as_mut().unwrap().count = id + 1;
247                    id as i32
248                } else {
249                    0
250                }
251            } else {
252                fid
253            }
254        };
255
256        let apply_alignment = fs.alignment.is_some();
257        let alignment = new_alignment(&fs);
258        let apply_protection = fs.protection.is_some();
259        let protection = new_protection(&fs);
260
261        let id = set_cell_xfs(
262            &mut style_sheet,
263            font_id,
264            num_fmt_id,
265            fill_id,
266            border_id,
267            apply_alignment,
268            apply_protection,
269            alignment,
270            protection,
271        )?;
272
273        *ss = Some(style_sheet);
274        Ok(id)
275    }
276
277    /// Get the style definition for a given style ID.
278    ///
279    /// Equivalent to Go `GetStyle`.
280    pub fn get_style(&self, style_id: i32) -> Result<Style> {
281        // Clone the style sheet while holding the lock and release it before
282        // calling the extract helpers. Those helpers may call back into the
283        // File (e.g. `theme_reader`/`styles_reader` through `get_theme_color`),
284        // and re-entering this lock would deadlock.
285        let style_sheet = {
286            let ss = self.styles.lock().unwrap();
287            let style_sheet = ss
288                .as_ref()
289                .ok_or_else(|| crate::errors::new_invalid_style_id_error(style_id))?;
290            if style_id < 0
291                || style_sheet.cell_xfs.is_none()
292                || style_sheet.cell_xfs.as_ref().unwrap().xf.len() as i32 <= style_id
293            {
294                return Err(crate::errors::new_invalid_style_id_error(style_id).into());
295            }
296            style_sheet.clone()
297        };
298
299        let xf = &style_sheet.cell_xfs.as_ref().unwrap().xf[style_id as usize];
300        let mut style = Style::default();
301
302        if should_extract_fill(xf, &style_sheet) {
303            if let Some(fill) = style_sheet
304                .fills
305                .as_ref()
306                .and_then(|f| f.fill.get(*xf.fill_id.as_ref().unwrap() as usize))
307            {
308                extract_fill(self, fill, &mut style);
309            }
310        }
311        if should_extract_border(xf, &style_sheet) {
312            if let Some(border) = style_sheet
313                .borders
314                .as_ref()
315                .and_then(|b| b.border.get(*xf.border_id.as_ref().unwrap() as usize))
316            {
317                extract_border(self, border, &mut style);
318            }
319        }
320        if should_extract_font(xf, &style_sheet) {
321            if let Some(font) = style_sheet
322                .fonts
323                .as_ref()
324                .and_then(|f| f.font.get(*xf.font_id.as_ref().unwrap() as usize))
325            {
326                style.font = Some(extract_font(font));
327            }
328        }
329        if should_extract_alignment(xf, &style_sheet) {
330            if let Some(ref alignment) = xf.alignment {
331                style.alignment = Some(extract_alignment(alignment));
332            }
333        }
334        if should_extract_protection(xf, &style_sheet) {
335            if let Some(ref protection) = xf.protection {
336                style.protection = Some(extract_protection(protection));
337            }
338        }
339        if let Some(num_fmt_id) = xf.num_fmt_id {
340            extract_num_fmt(num_fmt_id as i32, &style_sheet, &mut style);
341        }
342        Ok(style)
343    }
344
345    /// Return the default font name in the workbook.
346    ///
347    /// Equivalent to Go `GetDefaultFont`.
348    pub fn get_default_font(&self) -> Result<String> {
349        let ss = self.styles.lock().unwrap();
350        let style_sheet = ss
351            .as_ref()
352            .ok_or_else(|| crate::errors::new_invalid_style_id_error(0))?;
353        if style_sheet
354            .fonts
355            .as_ref()
356            .map(|f| f.font.is_empty())
357            .unwrap_or(true)
358        {
359            return Ok("Calibri".to_string());
360        }
361        Ok(style_sheet.fonts.as_ref().unwrap().font[0]
362            .name
363            .as_ref()
364            .and_then(|n| n.val.clone())
365            .unwrap_or_default())
366    }
367
368    /// Change the default font in the workbook.
369    ///
370    /// Equivalent to Go `SetDefaultFont`.
371    pub fn set_default_font(&self, font_name: &str) -> Result<()> {
372        let mut ss = self.styles.lock().unwrap();
373        let mut style_sheet = ss.take().unwrap_or_default();
374        if style_sheet
375            .fonts
376            .as_ref()
377            .map(|f| f.font.is_empty())
378            .unwrap_or(true)
379        {
380            *ss = Some(style_sheet);
381            return Ok(());
382        }
383        style_sheet.fonts.as_mut().unwrap().font[0].name = Some(AttrValString {
384            val: Some(font_name.to_string()),
385        });
386        if let Some(ref mut cs) = style_sheet.cell_styles {
387            if !cs.cell_style.is_empty() {
388                cs.cell_style[0].custom_built_in = Some(true);
389            }
390        }
391        *ss = Some(style_sheet);
392        Ok(())
393    }
394
395    /// Create a new style for conditional formats and return its dxf ID.
396    ///
397    /// Equivalent to Go `NewConditionalStyle`.
398    pub fn new_conditional_style(&self, style: &Style) -> Result<i32> {
399        let mut ss = self.styles.lock().unwrap();
400        let mut style_sheet = ss.take().unwrap_or_default();
401        let mut fs = parse_format_style_set(style)?;
402        if let Some(dp) = fs.decimal_places {
403            if dp < 0 || dp > 30 {
404                // Match Go behavior: reset out-of-range decimal places to 2.
405                fs.decimal_places = Some(2);
406            }
407        }
408        let mut dxf = XlsxDxf::default();
409        dxf.fill = new_fills(&fs, false);
410        dxf.alignment = new_alignment(&fs);
411        if !fs.border.is_empty() {
412            dxf.border = Some(new_borders(&fs));
413        }
414        if let Some(ref font) = fs.font {
415            dxf.font = Some(new_font(font));
416        }
417        dxf.protection = new_protection(&fs);
418        dxf.num_fmt = new_dxf_num_fmt(&style_sheet, style);
419        if style_sheet.dxfs.is_none() {
420            style_sheet.dxfs = Some(XlsxDxfs::default());
421        }
422        let dxfs = style_sheet.dxfs.as_mut().unwrap();
423        dxfs.count += 1;
424        dxfs.dxfs.push(dxf);
425        let id = dxfs.count - 1;
426        *ss = Some(style_sheet);
427        Ok(id as i32)
428    }
429
430    /// Get the conditional format style definition for a given dxf ID.
431    ///
432    /// Equivalent to Go `GetConditionalStyle`.
433    pub fn get_conditional_style(&self, style_id: i32) -> Result<Style> {
434        // Clone the needed data while holding the lock and release it before
435        // calling extract helpers that may re-enter the styles lock.
436        let (dxf, num_fmt_id) = {
437            let ss = self.styles.lock().unwrap();
438            let style_sheet = ss
439                .as_ref()
440                .ok_or_else(|| crate::errors::new_invalid_style_id_error(style_id))?;
441            if style_id < 0
442                || style_sheet.dxfs.is_none()
443                || style_sheet.dxfs.as_ref().unwrap().dxfs.len() as i32 <= style_id
444            {
445                return Err(crate::errors::new_invalid_style_id_error(style_id).into());
446            }
447            let dxf = style_sheet.dxfs.as_ref().unwrap().dxfs[style_id as usize].clone();
448            let num_fmt_id = dxf.num_fmt.as_ref().map(|n| n.num_fmt_id as i32);
449            (dxf, num_fmt_id)
450        };
451
452        let mut style = Style::default();
453        if let Some(mut fill) = dxf.fill {
454            if fill.pattern_fill.is_some()
455                && fill
456                    .pattern_fill
457                    .as_ref()
458                    .unwrap()
459                    .pattern_type
460                    .as_deref()
461                    .unwrap_or("")
462                    == ""
463            {
464                fill.pattern_fill.as_mut().unwrap().pattern_type = Some("solid".to_string());
465            }
466            extract_fill(self, &fill, &mut style);
467        }
468        if let Some(ref border) = dxf.border {
469            extract_border(self, border, &mut style);
470        }
471        if let Some(ref font) = dxf.font {
472            style.font = Some(extract_font(font));
473        }
474        if let Some(ref alignment) = dxf.alignment {
475            style.alignment = Some(extract_alignment(alignment));
476        }
477        if let Some(ref protection) = dxf.protection {
478            style.protection = Some(extract_protection(protection));
479        }
480        if let Some(num_fmt_id) = num_fmt_id {
481            let style_sheet = self.styles_reader()?;
482            extract_num_fmt(num_fmt_id, &style_sheet, &mut style);
483        }
484        Ok(style)
485    }
486
487    /// Create conditional formatting rules for a range of cells.
488    ///
489    /// Equivalent to Go `SetConditionalFormat`.
490    pub fn set_conditional_format(
491        &self,
492        sheet: &str,
493        range_ref: &str,
494        opts: &[ConditionalFormatOptions],
495    ) -> Result<()> {
496        let path = self.get_sheet_xml_path(sheet).ok_or_else(|| {
497            Box::new(crate::errors::ErrSheetNotExist {
498                sheet_name: sheet.to_string(),
499            }) as Box<dyn std::error::Error + Send + Sync>
500        })?;
501        let mut ws = self.work_sheet_reader(sheet)?;
502        let (sqref, mast_cell) = prepare_conditional_format_range(range_ref)?;
503        let mut rules: i64 = 0;
504        for cf in &ws.conditional_formatting {
505            rules += cf.cf_rule.len() as i64;
506        }
507        let no_criteria_types = [
508            "containsBlanks",
509            "notContainsBlanks",
510            "containsErrors",
511            "notContainsErrors",
512            "expression",
513            "iconSet",
514        ];
515        let sheet_id = self.get_sheet_id(sheet) as u32;
516        let mut cf_rules: Vec<XlsxCfRule> = Vec::new();
517        for (i, opt) in opts.iter().enumerate() {
518            let vt = VALID_TYPE.get(opt.r#type.as_str()).copied();
519            let Some(vt) = vt else {
520                return Err(Box::new(ErrParameterInvalid));
521            };
522            let ct = CRITERIA_TYPE
523                .get(opt.criteria.as_str())
524                .copied()
525                .unwrap_or("");
526            let ct_ok = !ct.is_empty() || in_str_slice(&no_criteria_types, vt, true) != -1;
527            if !ct_ok {
528                return Err(Box::new(ErrParameterInvalid));
529            }
530            let Some(draw_func) = DRAW_COND_FMT_FUNC.get(vt) else {
531                return Err(Box::new(ErrParameterInvalid));
532            };
533            let priority = rules + i as i64;
534            let guid = format!(
535                "{{00000000-0000-0000-{priority:04X}-{sheet_id:012X}}}",
536                priority = priority,
537                sheet_id = sheet_id
538            );
539            let (rule, x14_rule) = draw_func(priority as i32, ct, &mast_cell, &guid, opt);
540            if rule.is_none() && x14_rule.is_none() {
541                return Err(Box::new(ErrParameterInvalid));
542            }
543            if let Some(rule) = rule {
544                cf_rules.push(rule);
545            }
546            if let Some(x14_rule) = x14_rule {
547                append_cf_rule(&mut ws, &x14_rule, &sqref)?;
548                self.add_sheet_name_space(sheet, NAMESPACE_SPREADSHEET_X14);
549            }
550        }
551        if !cf_rules.is_empty() {
552            ws.conditional_formatting.push(XlsxConditionalFormatting {
553                sqref: Some(sqref.clone()),
554                cf_rule: cf_rules,
555                ..Default::default()
556            });
557        }
558        self.sheet.insert(path, ws);
559        Ok(())
560    }
561
562    /// Return conditional format settings by worksheet name.
563    ///
564    /// Equivalent to Go `GetConditionalFormats`.
565    pub fn get_conditional_formats(
566        &self,
567        sheet: &str,
568    ) -> Result<HashMap<String, Vec<ConditionalFormatOptions>>> {
569        let mut result: HashMap<String, Vec<ConditionalFormatOptions>> = HashMap::new();
570        let ws = self.work_sheet_reader(sheet)?;
571        for cf in &ws.conditional_formatting {
572            let (_, mast_cell) =
573                prepare_conditional_format_range(cf.sqref.as_deref().unwrap_or(""))?;
574            let mut opts = Vec::new();
575            for cr in &cf.cf_rule {
576                if let Some(t) = cr.r#type.as_deref() {
577                    if let Some(extract_func) = EXTRACT_COND_FMT_FUNC.get(t) {
578                        opts.push(extract_func(self, &mast_cell, cr, ws.ext_lst.as_ref()));
579                    }
580                }
581            }
582            if let Some(sqref) = cf.sqref.clone() {
583                result.insert(sqref, opts);
584            }
585        }
586        if let Some(ext_lst) = ws.ext_lst.as_ref() {
587            for ext in &ext_lst.ext {
588                if ext.uri.as_deref() == Some(EXT_URI_CONDITIONAL_FORMATTINGS) {
589                    let decoded = parse_x14_conditional_formattings(&ext.content)?;
590                    for cond_fmt in &decoded.cond_fmt {
591                        let mut opts = Vec::new();
592                        for rule in &cond_fmt.cf_rule {
593                            if rule.r#type.as_deref() == Some("iconSet") {
594                                opts.push(extract_x14_cond_fmt_icon_set(rule));
595                            }
596                        }
597                        if let Some(sqref) = cond_fmt.sqref.as_deref() {
598                            result.entry(sqref.to_string()).or_default().extend(opts);
599                        }
600                    }
601                }
602            }
603        }
604        Ok(result)
605    }
606
607    /// Unset conditional formats for a range of cells.
608    ///
609    /// Equivalent to Go `UnsetConditionalFormat`.
610    pub fn unset_conditional_format(&self, sheet: &str, range_ref: &str) -> Result<()> {
611        let path = self.get_sheet_xml_path(sheet).ok_or_else(|| {
612            Box::new(crate::errors::ErrSheetNotExist {
613                sheet_name: sheet.to_string(),
614            }) as Box<dyn std::error::Error + Send + Sync>
615        })?;
616        let mut ws = self.work_sheet_reader(sheet)?;
617        let del_cells = flat_sqref(range_ref)?;
618        let mut i = 0;
619        while i < ws.conditional_formatting.len() {
620            let sqref = ws.conditional_formatting[i]
621                .sqref
622                .as_deref()
623                .unwrap_or("")
624                .to_string();
625            let new_sqref = delete_cells_from_sqref(&sqref, &del_cells)?;
626            if new_sqref.is_empty() {
627                ws.conditional_formatting.remove(i);
628            } else {
629                ws.conditional_formatting[i].sqref = Some(new_sqref);
630                i += 1;
631            }
632        }
633        if let Some(ext_lst) = ws.ext_lst.as_mut() {
634            let mut idx = 0;
635            while idx < ext_lst.ext.len() {
636                if ext_lst.ext[idx].uri.as_deref() == Some(EXT_URI_CONDITIONAL_FORMATTINGS) {
637                    let content = ext_lst.ext[idx].content.clone();
638                    let decoded = parse_x14_conditional_formattings(&content)?;
639                    let new_content = delete_x14_cf_rule(&decoded, &del_cells)?;
640                    ext_lst.ext[idx].content = new_content;
641                    if ext_lst.ext[idx].content.len() == 57 {
642                        ext_lst.ext.remove(idx);
643                        continue;
644                    }
645                }
646                idx += 1;
647            }
648            sort_ext_lst(ext_lst);
649            if ext_lst.ext.is_empty() {
650                ws.ext_lst = None;
651            }
652        }
653        self.sheet.insert(path, ws);
654        Ok(())
655    }
656
657    /// Return the preferred hex color code from a hex color, indexed color, or
658    /// theme color.
659    ///
660    /// Equivalent to Go `GetBaseColor`.
661    pub fn get_base_color(
662        &self,
663        hex_color: &str,
664        indexed_color: i64,
665        theme_color: Option<i64>,
666    ) -> String {
667        if let Ok(Some(theme)) = self.theme_reader() {
668            if let Some(tc) = theme_color {
669                let clr_scheme = &theme.theme_elements.clr_scheme;
670                let choices: [(i64, &crate::xml::theme::DecodeCtColor); 10] = [
671                    (0, &clr_scheme.lt1),
672                    (1, &clr_scheme.dk1),
673                    (2, &clr_scheme.lt2),
674                    (3, &clr_scheme.dk2),
675                    (4, &clr_scheme.accent1),
676                    (5, &clr_scheme.accent2),
677                    (6, &clr_scheme.accent3),
678                    (7, &clr_scheme.accent4),
679                    (8, &clr_scheme.accent5),
680                    (9, &clr_scheme.accent6),
681                ];
682                for (idx, clr) in choices {
683                    if idx == tc {
684                        if let Some(val) = decode_ct_color_choice(clr) {
685                            return val;
686                        }
687                        break;
688                    }
689                }
690            }
691        }
692        if hex_color.len() == 6 {
693            return hex_color.to_string();
694        }
695        if hex_color.len() == 8 {
696            return hex_color
697                .strip_prefix("FF")
698                .unwrap_or(hex_color)
699                .to_string();
700        }
701        if let Ok(styles) = self.styles_reader() {
702            if let Some(colors) = styles.colors {
703                if let Some(indexed) = colors.indexed_colors {
704                    if let Some(rgb) = indexed.rgb_color.get(indexed_color as usize) {
705                        if let Some(rgb_val) = rgb.rgb.as_ref() {
706                            return rgb_val.strip_prefix("FF").unwrap_or(rgb_val).to_string();
707                        }
708                    }
709                }
710            }
711        }
712        if let Some(color) = INDEXED_COLOR_MAPPING.get(indexed_color as usize) {
713            return color.to_string();
714        }
715        hex_color.to_string()
716    }
717}
718
719// ------------------------------------------------------------------
720// Validation
721// ------------------------------------------------------------------
722
723fn parse_format_style_set(style: &Style) -> Result<Style> {
724    let fs = style.clone();
725    if let Some(ref font) = fs.font {
726        if count_utf16_string(font.name.as_deref().unwrap_or("")) > MAX_FONT_FAMILY_LENGTH {
727            return Err(Box::new(ErrFontLength));
728        }
729        if let Some(size) = font.size {
730            if size > MAX_FONT_SIZE as f64 {
731                return Err(Box::new(ErrFontSize));
732            }
733        }
734    }
735    match fs.fill.r#type.as_str() {
736        "gradient" => {
737            if fs.fill.color.len() != 2 {
738                return Err(Box::new(ErrFillGradientColor));
739            }
740            if fs.fill.shading < 0 || fs.fill.shading > 16 {
741                return Err(Box::new(ErrFillGradientShading));
742            }
743        }
744        "pattern" => {
745            if fs.fill.color.len() > 1 {
746                return Err(Box::new(ErrFillPatternColor));
747            }
748            if fs.fill.pattern < 0 || fs.fill.pattern > 18 {
749                return Err(Box::new(ErrFillPattern));
750            }
751        }
752        "" => {}
753        _ => return Err(Box::new(ErrFillType)),
754    }
755    if let Some(ref cnf) = fs.custom_num_fmt {
756        if cnf.is_empty() {
757            return Err(Box::new(ErrCustomNumFmt));
758        }
759    }
760    Ok(fs)
761}
762
763// ------------------------------------------------------------------
764// XML builders
765// ------------------------------------------------------------------
766
767fn new_font(font: &Font) -> XlsxFont {
768    let mut xfont = XlsxFont {
769        family: Some(AttrValInt { val: Some(2) }),
770        ..Default::default()
771    };
772    if let Some(size) = font.size {
773        if size >= MIN_FONT_SIZE as f64 {
774            xfont.sz = Some(AttrValFloat { val: Some(size) });
775        }
776    }
777    if let Some(name) = &font.name {
778        xfont.name = Some(AttrValString {
779            val: Some(name.clone()),
780        });
781    }
782    if let Some(charset) = font.charset {
783        xfont.charset = Some(AttrValInt { val: Some(charset) });
784    }
785    xfont.color = new_font_color(font);
786    if let Some(bold) = font.bold {
787        xfont.b = Some(AttrValBool { val: Some(bold) });
788    }
789    if let Some(italic) = font.italic {
790        xfont.i = Some(AttrValBool { val: Some(italic) });
791    }
792    if let Some(strike) = font.strike {
793        xfont.strike = Some(AttrValBool { val: Some(strike) });
794    }
795    if let Some(underline) = &font.underline {
796        if SUPPORTED_UNDERLINE_TYPES.contains(&underline.as_str()) {
797            xfont.u = Some(AttrValString {
798                val: Some(underline.clone()),
799            });
800        }
801    }
802    if let Some(vert_align) = &font.vert_align {
803        if SUPPORTED_VERT_ALIGN_TYPES.contains(&vert_align.as_str()) {
804            xfont.vert_align = Some(AttrValString {
805                val: Some(vert_align.clone()),
806            });
807        }
808    }
809    xfont
810}
811
812fn new_font_color(font: &Font) -> Option<XlsxColor> {
813    let mut color: Option<XlsxColor> = None;
814    if let Some(c) = &font.color {
815        color.get_or_insert_default().rgb = Some(get_palette_color(c));
816    }
817    if let Some(indexed) = font.color_indexed {
818        if indexed >= 0 && indexed <= (INDEXED_COLOR_MAPPING.len() as i64 + 1) {
819            color.get_or_insert_default().indexed = Some(indexed);
820        }
821    }
822    if let Some(theme) = font.color_theme {
823        color.get_or_insert_default().theme = Some(theme);
824    }
825    if font.color_tint != 0.0 {
826        color.get_or_insert_default().tint = Some(font.color_tint);
827    }
828    color
829}
830
831fn decode_ct_color_choice(clr: &crate::xml::theme::DecodeCtColor) -> Option<String> {
832    if let Some(srgb) = &clr.srgb_clr {
833        if let Some(val) = &srgb.val {
834            return Some(val.clone());
835        }
836    }
837    if let Some(sys) = &clr.sys_clr {
838        return Some(sys.last_clr.clone());
839    }
840    None
841}
842
843pub(crate) fn get_palette_color(color: &str) -> String {
844    format!("FF{}", color.to_uppercase().replace('#', ""))
845}
846
847fn get_font_id(style_sheet: &XlsxStyleSheet, font: &Font) -> i32 {
848    if style_sheet.fonts.is_none() {
849        return -1;
850    }
851    let target = new_font(font);
852    for (idx, fnt) in style_sheet.fonts.as_ref().unwrap().font.iter().enumerate() {
853        if *fnt == target {
854            return idx as i32;
855        }
856    }
857    -1
858}
859
860fn new_fills(style: &Style, fg: bool) -> Option<XlsxFill> {
861    let mut fill = XlsxFill::default();
862    match style.fill.r#type.as_str() {
863        "gradient" => {
864            let mut variants = style_fill_variants();
865            let shading = style.fill.shading as usize;
866            if shading >= variants.len() {
867                return None;
868            }
869            let mut gradient = variants.remove(shading);
870            if let Some(c) = style.fill.color.get(0) {
871                if gradient.stop.is_empty() {
872                    gradient.stop.push(XlsxGradientFillStop::default());
873                }
874                gradient.stop[0].color = Some(XlsxColor {
875                    rgb: Some(get_palette_color(c)),
876                    ..Default::default()
877                });
878            }
879            if let Some(c) = style.fill.color.get(1) {
880                if gradient.stop.len() < 2 {
881                    gradient.stop.push(XlsxGradientFillStop {
882                        position: 1.0,
883                        color: None,
884                    });
885                }
886                gradient.stop[1].color = Some(XlsxColor {
887                    rgb: Some(get_palette_color(c)),
888                    ..Default::default()
889                });
890            }
891            if gradient.stop.len() == 3 {
892                if let Some(c) = style.fill.color.get(0) {
893                    gradient.stop[2].color = Some(XlsxColor {
894                        rgb: Some(get_palette_color(c)),
895                        ..Default::default()
896                    });
897                }
898            }
899            fill.gradient_fill = Some(gradient);
900        }
901        "pattern" => {
902            let mut pattern = XlsxPatternFill::default();
903            let pattern_idx = style.fill.pattern as usize;
904            if pattern_idx < STYLE_FILL_PATTERNS.len() {
905                pattern.pattern_type = Some(STYLE_FILL_PATTERNS[pattern_idx].to_string());
906            }
907            if style.fill.color.is_empty() {
908                if style.fill.pattern == 1 {
909                    pattern.fg_color = Some(XlsxColor {
910                        auto: Some(true),
911                        ..Default::default()
912                    });
913                    pattern.bg_color = Some(XlsxColor {
914                        auto: Some(true),
915                        ..Default::default()
916                    });
917                }
918            } else if fg {
919                pattern.fg_color = Some(XlsxColor {
920                    rgb: Some(get_palette_color(&style.fill.color[0])),
921                    ..Default::default()
922                });
923            } else {
924                pattern.bg_color = Some(XlsxColor {
925                    rgb: Some(get_palette_color(&style.fill.color[0])),
926                    ..Default::default()
927                });
928            }
929            fill.pattern_fill = Some(pattern);
930        }
931        _ => return None,
932    }
933    Some(fill)
934}
935
936fn get_fill_id(style_sheet: &XlsxStyleSheet, style: &Style) -> i32 {
937    if style_sheet.fills.is_none() || style.fill.r#type.is_empty() {
938        return -1;
939    }
940    let target = match new_fills(style, true) {
941        Some(f) => f,
942        None => return -1,
943    };
944    for (idx, fill) in style_sheet.fills.as_ref().unwrap().fill.iter().enumerate() {
945        if *fill == target {
946            return idx as i32;
947        }
948    }
949    -1
950}
951
952fn new_borders(style: &Style) -> XlsxBorder {
953    let mut border = XlsxBorder::default();
954    for v in &style.border {
955        if v.style >= 0 && v.style < STYLE_BORDERS.len() as i64 {
956            let line = XlsxLine {
957                style: Some(STYLE_BORDERS[v.style as usize].to_string()),
958                color: Some(XlsxColor {
959                    rgb: Some(get_palette_color(&v.color)),
960                    ..Default::default()
961                }),
962            };
963            match v.r#type.as_str() {
964                "left" => border.left = Some(line),
965                "right" => border.right = Some(line),
966                "top" => border.top = Some(line),
967                "bottom" => border.bottom = Some(line),
968                "diagonalUp" => {
969                    border.diagonal = Some(line);
970                    border.diagonal_up = Some(true);
971                }
972                "diagonalDown" => {
973                    border.diagonal = Some(line);
974                    border.diagonal_down = Some(true);
975                }
976                _ => {}
977            }
978        }
979    }
980    border
981}
982
983fn get_border_id(style_sheet: &XlsxStyleSheet, style: &Style) -> i32 {
984    if style_sheet.borders.is_none() || style.border.is_empty() {
985        return -1;
986    }
987    let target = new_borders(style);
988    for (idx, border) in style_sheet
989        .borders
990        .as_ref()
991        .unwrap()
992        .border
993        .iter()
994        .enumerate()
995    {
996        if *border == target {
997            return idx as i32;
998        }
999    }
1000    -1
1001}
1002
1003fn new_alignment(style: &Style) -> Option<XlsxAlignment> {
1004    style.alignment.as_ref().map(|a| XlsxAlignment {
1005        horizontal: Some(a.horizontal.clone()).filter(|s| !s.is_empty()),
1006        indent: Some(a.indent).filter(|&v| v != 0),
1007        justify_last_line: Some(a.justify_last_line).filter(|&v| v),
1008        reading_order: Some(a.reading_order).filter(|&v| v != 0),
1009        relative_indent: Some(a.relative_indent).filter(|&v| v != 0),
1010        shrink_to_fit: Some(a.shrink_to_fit).filter(|&v| v),
1011        text_rotation: Some(a.text_rotation).filter(|&v| v != 0),
1012        vertical: Some(a.vertical.clone()).filter(|s| !s.is_empty()),
1013        wrap_text: Some(a.wrap_text).filter(|&v| v),
1014    })
1015}
1016
1017fn new_protection(style: &Style) -> Option<XlsxProtection> {
1018    style.protection.as_ref().map(|p| XlsxProtection {
1019        hidden: Some(p.hidden).filter(|&v| v),
1020        locked: Some(p.locked).filter(|&v| v),
1021    })
1022}
1023
1024fn set_cell_xfs(
1025    style_sheet: &mut XlsxStyleSheet,
1026    font_id: i32,
1027    num_fmt_id: i32,
1028    fill_id: i32,
1029    border_id: i32,
1030    apply_alignment: bool,
1031    apply_protection: bool,
1032    alignment: Option<XlsxAlignment>,
1033    protection: Option<XlsxProtection>,
1034) -> Result<i32> {
1035    ensure_cell_xfs(style_sheet);
1036    let cell_xfs = style_sheet.cell_xfs.as_mut().unwrap();
1037    if cell_xfs.xf.len() as i32 >= MAX_CELL_STYLES {
1038        return Err(Box::new(ErrCellStyles));
1039    }
1040    let mut xf = XlsxXf::default();
1041    xf.font_id = Some(font_id as i64);
1042    if font_id != 0 {
1043        xf.apply_font = Some(true);
1044    }
1045    xf.num_fmt_id = Some(num_fmt_id as i64);
1046    if num_fmt_id != 0 {
1047        xf.apply_number_format = Some(true);
1048    }
1049    xf.fill_id = Some(fill_id as i64);
1050    if fill_id != 0 {
1051        xf.apply_fill = Some(true);
1052    }
1053    xf.border_id = Some(border_id as i64);
1054    if border_id != 0 {
1055        xf.apply_border = Some(true);
1056    }
1057    xf.xf_id = Some(0);
1058    xf.alignment = alignment;
1059    if apply_alignment {
1060        xf.apply_alignment = Some(true);
1061    }
1062    if apply_protection {
1063        xf.apply_protection = Some(true);
1064        xf.protection = protection;
1065    }
1066    cell_xfs.xf.push(xf);
1067    cell_xfs.count = cell_xfs.xf.len() as i64;
1068    Ok((cell_xfs.count - 1) as i32)
1069}
1070
1071// ------------------------------------------------------------------
1072// Deduplication
1073// ------------------------------------------------------------------
1074
1075fn get_style_id(style_sheet: &XlsxStyleSheet, style: &Style) -> i32 {
1076    if style_sheet.cell_xfs.is_none() {
1077        return -1;
1078    }
1079    if style.font.is_none() {
1080        return -1;
1081    }
1082    let num_fmt_id = get_num_fmt_id(style_sheet, style);
1083    let border_id = get_border_id(style_sheet, style);
1084    let fill_id = get_fill_id(style_sheet, style);
1085    let font_id = get_font_id(style_sheet, style.font.as_ref().unwrap());
1086    let num_fmt_id_for_custom = if style.custom_num_fmt.is_some() {
1087        get_custom_num_fmt_id(style_sheet, style)
1088    } else {
1089        num_fmt_id
1090    };
1091
1092    for (idx, xf) in style_sheet.cell_xfs.as_ref().unwrap().xf.iter().enumerate() {
1093        if !match_num_fmt(num_fmt_id_for_custom, xf, style) {
1094            continue;
1095        }
1096        if !match_font(font_id, xf, style) {
1097            continue;
1098        }
1099        if !match_fill(fill_id, xf, style) {
1100            continue;
1101        }
1102        if !match_border(border_id, xf, style) {
1103            continue;
1104        }
1105        if !match_alignment(xf, style) {
1106            continue;
1107        }
1108        if !match_protection(xf, style) {
1109            continue;
1110        }
1111        return idx as i32;
1112    }
1113    -1
1114}
1115
1116fn match_num_fmt(num_fmt_id: i32, xf: &XlsxXf, style: &Style) -> bool {
1117    if style.custom_num_fmt.is_none() && num_fmt_id == -1 {
1118        return xf.num_fmt_id.map(|v| v == 0).unwrap_or(false);
1119    }
1120    if style.neg_red || style.decimal_places.map(|v| v != 2).unwrap_or(false) {
1121        return false;
1122    }
1123    xf.num_fmt_id == Some(num_fmt_id as i64)
1124}
1125
1126fn match_font(font_id: i32, xf: &XlsxXf, style: &Style) -> bool {
1127    if style.font.is_none() || font_id == 0 {
1128        return (xf.font_id.is_none() || xf.font_id == Some(0))
1129            && (xf.apply_font.is_none() || !xf.apply_font.unwrap());
1130    }
1131    xf.font_id == Some(font_id as i64) && xf.apply_font == Some(true)
1132}
1133
1134fn match_fill(fill_id: i32, xf: &XlsxXf, style: &Style) -> bool {
1135    if style.fill.r#type.is_empty() || fill_id == 0 {
1136        return (xf.fill_id.is_none() || xf.fill_id == Some(0))
1137            && (xf.apply_fill.is_none() || !xf.apply_fill.unwrap());
1138    }
1139    xf.fill_id == Some(fill_id as i64) && xf.apply_fill == Some(true)
1140}
1141
1142fn match_border(border_id: i32, xf: &XlsxXf, style: &Style) -> bool {
1143    if style.border.is_empty() {
1144        return (xf.border_id.is_none() || xf.border_id == Some(0))
1145            && (xf.apply_border.is_none() || !xf.apply_border.unwrap());
1146    }
1147    xf.border_id == Some(border_id as i64) && xf.apply_border == Some(true)
1148}
1149
1150fn match_alignment(xf: &XlsxXf, style: &Style) -> bool {
1151    if style.alignment.is_none() {
1152        return xf.apply_alignment.is_none() || !xf.apply_alignment.unwrap();
1153    }
1154    xf.alignment == new_alignment(style)
1155}
1156
1157fn match_protection(xf: &XlsxXf, style: &Style) -> bool {
1158    if style.protection.is_none() {
1159        return xf.apply_protection.is_none() || !xf.apply_protection.unwrap();
1160    }
1161    xf.protection == new_protection(style) && xf.apply_protection == Some(true)
1162}
1163
1164// ------------------------------------------------------------------
1165// Number formats
1166// ------------------------------------------------------------------
1167
1168static BUILT_IN_NUM_FMT: LazyLock<HashMap<i32, &'static str>> = LazyLock::new(|| {
1169    let mut m = HashMap::new();
1170    m.insert(0, "general");
1171    m.insert(1, "0");
1172    m.insert(2, "0.00");
1173    m.insert(3, "#,##0");
1174    m.insert(4, "#,##0.00");
1175    m.insert(9, "0%");
1176    m.insert(10, "0.00%");
1177    m.insert(11, "0.00E+00");
1178    m.insert(12, "# ?/?");
1179    m.insert(13, "# ??/??");
1180    m.insert(14, "mm-dd-yy");
1181    m.insert(15, "d-mmm-yy");
1182    m.insert(16, "d-mmm");
1183    m.insert(17, "mmm-yy");
1184    m.insert(18, "h:mm AM/PM");
1185    m.insert(19, "h:mm:ss AM/PM");
1186    m.insert(20, "hh:mm");
1187    m.insert(21, "hh:mm:ss");
1188    m.insert(22, "m/d/yy hh:mm");
1189    m.insert(37, "#,##0 ;(#,##0)");
1190    m.insert(38, "#,##0 ;[red](#,##0)");
1191    m.insert(39, "#,##0.00 ;(#,##0.00)");
1192    m.insert(40, "#,##0.00 ;[red](#,##0.00)");
1193    m.insert(41, "_(* #,##0_);_(* \\(#,##0\\);_(* \"-\"_);_(@_)");
1194    m.insert(
1195        42,
1196        "_(\"$\"* #,##0_);_(\"$\"* \\(#,##0\\);_(\"$\"* \"-\"_);_(@_)",
1197    );
1198    m.insert(43, "_(* #,##0.00_);_(* \\(#,##0.00\\);_(* \"-\"??_);_(@_)");
1199    m.insert(
1200        44,
1201        "_(\"$\"* #,##0.00_);_(\"$\"* \\(#,##0.00\\);_(\"$\"* \"-\"??_);_(@_)",
1202    );
1203    m.insert(45, "mm:ss");
1204    m.insert(46, "[h]:mm:ss");
1205    m.insert(47, "mm:ss.0");
1206    m.insert(48, "##0.0E+0");
1207    m.insert(49, "@");
1208    m
1209});
1210
1211static CURRENCY_NUM_FMT: LazyLock<HashMap<i32, &'static str>> = LazyLock::new(|| {
1212    let mut m = HashMap::new();
1213    m.insert(164, "\"\u{00A5}\"#,##0.00");
1214    m.insert(165, "[$$-409]#,##0.00");
1215    m.insert(166, "[$$-45C]#,##0.00");
1216    m.insert(167, "[$$-1004]#,##0.00");
1217    m.insert(168, "[$$-404]#,##0.00");
1218    m.insert(169, "[$$-C09]#,##0.00");
1219    m.insert(170, "[$$-2809]#,##0.00");
1220    m.insert(171, "[$$-1009]#,##0.00");
1221    m.insert(172, "[$$-2009]#,##0.00");
1222    m.insert(173, "[$$-1409]#,##0.00");
1223    m.insert(174, "[$$-4809]#,##0.00");
1224    m.insert(175, "[$$-2C09]#,##0.00");
1225    m.insert(176, "[$$-2409]#,##0.00");
1226    m.insert(177, "[$$-1000]#,##0.00");
1227    m.insert(178, "#,##0.00\\ [$$-C0C]");
1228    m.insert(179, "[$$-475]#,##0.00");
1229    m.insert(180, "[$$-83E]#,##0.00");
1230    m.insert(181, "[$$-86B]\\ #,##0.00");
1231    m.insert(182, "[$$-340A]\\ #,##0.00");
1232    m.insert(183, "[$$-240A]#,##0.00");
1233    m.insert(184, "[$$-300A]\\ #,##0.00");
1234    m.insert(185, "[$$-440A]#,##0.00");
1235    m.insert(186, "[$$-80A]#,##0.00");
1236    m.insert(187, "[$$-500A]#,##0.00");
1237    m.insert(188, "[$$-540A]#,##0.00");
1238    m.insert(189, "[$$-380A]\\ #,##0.00");
1239    m.insert(190, "[$\u{00A3}-809]#,##0.00");
1240    m.insert(191, "[$\u{00A3}-491]#,##0.00");
1241    m.insert(192, "[$\u{00A3}-452]#,##0.00");
1242    m.insert(193, "[$\u{00A5}-804]#,##0.00");
1243    m.insert(194, "[$\u{00A5}-411]#,##0.00");
1244    m.insert(195, "[$\u{00A5}-478]#,##0.00");
1245    m.insert(196, "[$\u{00A5}-451]#,##0.00");
1246    m.insert(197, "[$\u{00A5}-480]#,##0.00");
1247    m.insert(198, "#,##0.00\\ [$\u{058F}-42B]");
1248    m.insert(199, "[$\u{060B}-463]#,##0.00");
1249    m.insert(200, "[$\u{060B}-48C]#,##0.00");
1250    m.insert(201, "[$\u{09F3}-845]\\ #,##0.00");
1251    m.insert(202, "#,##0.00[$\u{17DB}-453]");
1252    m.insert(203, "[$\u{20A1}-140A]#,##0.00");
1253    m.insert(204, "[$\u{20A6}-468]\\ #,##0.00");
1254    m.insert(205, "[$\u{20A6}-470]\\ #,##0.00");
1255    m.insert(206, "[$\u{20A9}-412]#,##0.00");
1256    m.insert(207, "[$\u{20AA}-40D]\\ #,##0.00");
1257    m.insert(208, "#,##0.00\\ [$\u{20AB}-42A]");
1258    m.insert(209, "#,##0.00\\ [$\u{20AC}-42D]");
1259    m.insert(210, "#,##0.00\\ [$\u{20AC}-47E]");
1260    m.insert(211, "#,##0.00\\ [$\u{20AC}-403]");
1261    m.insert(212, "#,##0.00\\ [$\u{20AC}-483]");
1262    m.insert(213, "[$\u{20AC}-813]\\ #,##0.00");
1263    m.insert(214, "[$\u{20AC}-413]\\ #,##0.00");
1264    m.insert(215, "[$\u{20AC}-1809]#,##0.00");
1265    m.insert(216, "#,##0.00\\ [$\u{20AC}-425]");
1266    m.insert(217, "[$\u{20AC}-2]\\ #,##0.00");
1267    m.insert(218, "#,##0.00\\ [$\u{20AC}-1]");
1268    m.insert(219, "#,##0.00\\ [$\u{20AC}-40B]");
1269    m.insert(220, "#,##0.00\\ [$\u{20AC}-80C]");
1270    m.insert(221, "#,##0.00\\ [$\u{20AC}-40C]");
1271    m.insert(222, "#,##0.00\\ [$\u{20AC}-140C]");
1272    m.insert(223, "#,##0.00\\ [$\u{20AC}-180C]");
1273    m.insert(224, "[$\u{20AC}-200C]#,##0.00");
1274    m.insert(225, "#,##0.00\\ [$\u{20AC}-456]");
1275    m.insert(226, "#,##0.00\\ [$\u{20AC}-C07]");
1276    m.insert(227, "#,##0.00\\ [$\u{20AC}-407]");
1277    m.insert(228, "#,##0.00\\ [$\u{20AC}-1007]");
1278    m.insert(229, "#,##0.00\\ [$\u{20AC}-408]");
1279    m.insert(230, "#,##0.00\\ [$\u{20AC}-243B]");
1280    m.insert(231, "[$\u{20AC}-83C]#,##0.00");
1281    m.insert(232, "[$\u{20AC}-410]\\ #,##0.00");
1282    m.insert(233, "[$\u{20AC}-476]#,##0.00");
1283    m.insert(234, "#,##0.00\\ [$\u{20AC}-2C1A]");
1284    m.insert(235, "[$\u{20AC}-426]\\ #,##0.00");
1285    m.insert(236, "#,##0.00\\ [$\u{20AC}-427]");
1286    m.insert(237, "#,##0.00\\ [$\u{20AC}-82E]");
1287    m.insert(238, "#,##0.00\\ [$\u{20AC}-46E]");
1288    m.insert(239, "[$\u{20AC}-43A]#,##0.00");
1289    m.insert(240, "#,##0.00\\ [$\u{20AC}-C3B]");
1290    m.insert(241, "#,##0.00\\ [$\u{20AC}-482]");
1291    m.insert(242, "#,##0.00\\ [$\u{20AC}-816]");
1292    m.insert(243, "#,##0.00\\ [$\u{20AC}-301A]");
1293    m.insert(244, "#,##0.00\\ [$\u{20AC}-203B]");
1294    m.insert(245, "[$\u{20AC}-41B]#,##0.00");
1295    m.insert(246, "#,##0.00\\ [$\u{20AC}-424]");
1296    m.insert(247, "#,##0.00\\ [$\u{20AC}-C0A]");
1297    m.insert(248, "#,##0.00\\ [$\u{20AC}-81D]");
1298    m.insert(249, "#,##0.00\\ [$\u{20AC}-484]");
1299    m.insert(250, "#,##0.00\\ [$\u{20AC}-42E]");
1300    m.insert(251, "[$\u{20AC}-462]\\ #,##0.00");
1301    m.insert(252, "#,##0.00\\ [$\u{20AD}-454]");
1302    m.insert(253, "#,##0.00\\ [$\u{20AE}-450]");
1303    m.insert(254, "[$\u{20AE}-C50]#,##0.00");
1304    m.insert(255, "[$\u{20B1}-3409]#,##0.00");
1305    m.insert(256, "[$\u{20B1}-464]#,##0.00");
1306    m.insert(257, "#,##0.00[$\u{20B4}-422]");
1307    m.insert(258, "[$\u{20B8}-43F]#,##0.00");
1308    m.insert(259, "[$\u{20B9}-460]#,##0.00");
1309    m.insert(260, "[$\u{20B9}-4009]\\ #,##0.00");
1310    m.insert(261, "[$\u{20B9}-447]\\ #,##0.00");
1311    m.insert(262, "[$\u{20B9}-439]\\ #,##0.00");
1312    m.insert(263, "[$\u{20B9}-44B]\\ #,##0.00");
1313    m.insert(264, "[$\u{20B9}-860]#,##0.00");
1314    m.insert(265, "[$\u{20B9}-457]\\ #,##0.00");
1315    m.insert(266, "[$\u{20B9}-458]#,##0.00");
1316    m.insert(267, "[$\u{20B9}-44E]\\ #,##0.00");
1317    m.insert(268, "[$\u{20B9}-861]#,##0.00");
1318    m.insert(269, "[$\u{20B9}-448]\\ #,##0.00");
1319    m.insert(270, "[$\u{20B9}-446]\\ #,##0.00");
1320    m.insert(271, "[$\u{20B9}-44F]\\ #,##0.00");
1321    m.insert(272, "[$\u{20B9}-459]#,##0.00");
1322    m.insert(273, "[$\u{20B9}-449]\\ #,##0.00");
1323    m.insert(274, "[$\u{20B9}-820]#,##0.00");
1324    m.insert(275, "#,##0.00\\ [$\u{20BA}-41F]");
1325    m.insert(276, "#,##0.00\\ [$\u{20BC}-42C]");
1326    m.insert(277, "#,##0.00\\ [$\u{20BC}-82C]");
1327    m.insert(278, "#,##0.00\\ [$\u{20BD}-419]");
1328    m.insert(279, "#,##0.00[$\u{20BD}-485]");
1329    m.insert(280, "#,##0.00\\ [$\u{20BE}-437]");
1330    m.insert(281, "[$B/.-180A]\\ #,##0.00");
1331    m.insert(282, "[$Br-472]#,##0.00");
1332    m.insert(283, "[$Br-477]#,##0.00");
1333    m.insert(284, "#,##0.00[$Br-473]");
1334    m.insert(285, "[$Bs-46B]\\ #,##0.00");
1335    m.insert(286, "[$Bs-400A]\\ #,##0.00");
1336    m.insert(287, "[$Bs.-200A]\\ #,##0.00");
1337    m.insert(288, "[$BWP-832]\\ #,##0.00");
1338    m.insert(289, "[$C$-4C0A]#,##0.00");
1339    m.insert(290, "[$CA$-85D]#,##0.00");
1340    m.insert(291, "[$CA$-47C]#,##0.00");
1341    m.insert(292, "[$CA$-45D]#,##0.00");
1342    m.insert(293, "[$CFA-340C]#,##0.00");
1343    m.insert(294, "[$CFA-280C]#,##0.00");
1344    m.insert(295, "#,##0.00\\ [$CFA-867]");
1345    m.insert(296, "#,##0.00\\ [$CFA-488]");
1346    m.insert(297, "#,##0.00\\ [$CHF-100C]");
1347    m.insert(298, "[$CHF-1407]\\ #,##0.00");
1348    m.insert(299, "[$CHF-807]\\ #,##0.00");
1349    m.insert(300, "[$CHF-810]\\ #,##0.00");
1350    m.insert(301, "[$CHF-417]\\ #,##0.00");
1351    m.insert(302, "[$CLP-47A]\\ #,##0.00");
1352    m.insert(303, "[$CN\u{00A5}-850]#,##0.00");
1353    m.insert(304, "#,##0.00\\ [$DZD-85F]");
1354    m.insert(305, "[$FCFA-2C0C]#,##0.00");
1355    m.insert(306, "#,##0.00\\ [$Ft-40E]");
1356    m.insert(307, "[$G-3C0C]#,##0.00");
1357    m.insert(308, "[$Gs.-3C0A]\\ #,##0.00");
1358    m.insert(309, "[$GTQ-486]#,##0.00");
1359    m.insert(310, "[$HK$-C04]#,##0.00");
1360    m.insert(311, "[$HK$-3C09]#,##0.00");
1361    m.insert(312, "#,##0.00\\ [$HRK-41A]");
1362    m.insert(313, "[$IDR-3809]#,##0.00");
1363    m.insert(314, "[$IQD-492]#,##0.00");
1364    m.insert(315, "#,##0.00\\ [$ISK-40F]");
1365    m.insert(316, "[$K-455]#,##0.00");
1366    m.insert(317, "#,##0.00\\ [$K\u{010D}-405]");
1367    m.insert(318, "#,##0.00\\ [$KM-141A]");
1368    m.insert(319, "#,##0.00\\ [$KM-101A]");
1369    m.insert(320, "#,##0.00\\ [$KM-181A]");
1370    m.insert(321, "[$kr-438]\\ #,##0.00");
1371    m.insert(322, "[$kr-43B]\\ #,##0.00");
1372    m.insert(323, "#,##0.00\\ [$kr-83B]");
1373    m.insert(324, "[$kr-414]\\ #,##0.00");
1374    m.insert(325, "[$kr-814]\\ #,##0.00");
1375    m.insert(326, "#,##0.00\\ [$kr-41D]");
1376    m.insert(327, "[$kr.-406]\\ #,##0.00");
1377    m.insert(328, "[$kr.-46F]\\ #,##0.00");
1378    m.insert(329, "[$Ksh-441]#,##0.00");
1379    m.insert(330, "[$L-818]#,##0.00");
1380    m.insert(331, "[$L-819]#,##0.00");
1381    m.insert(332, "[$L-480A]\\ #,##0.00");
1382    m.insert(333, "#,##0.00\\ [$Lek\u{00EB}-41C]");
1383    m.insert(334, "[$MAD-45F]#,##0.00");
1384    m.insert(335, "[$MAD-380C]#,##0.00");
1385    m.insert(336, "#,##0.00\\ [$MAD-105F]");
1386    m.insert(337, "[$MOP$-1404]#,##0.00");
1387    m.insert(338, "#,##0.00\\ [$MVR-465]_-");
1388    m.insert(339, "#,##0.00[$Nfk-873]");
1389    m.insert(340, "[$NGN-466]#,##0.00");
1390    m.insert(341, "[$NGN-467]#,##0.00");
1391    m.insert(342, "[$NGN-469]#,##0.00");
1392    m.insert(343, "[$NGN-471]#,##0.00");
1393    m.insert(344, "[$NOK-103B]\\ #,##0.00");
1394    m.insert(345, "[$NOK-183B]\\ #,##0.00");
1395    m.insert(346, "[$NZ$-481]#,##0.00");
1396    m.insert(347, "[$PKR-859]\\ #,##0.00");
1397    m.insert(348, "[$PYG-474]#,##0.00");
1398    m.insert(349, "[$Q-100A]#,##0.00");
1399    m.insert(350, "[$R-436]\\ #,##0.00");
1400    m.insert(351, "[$R-1C09]\\ #,##0.00");
1401    m.insert(352, "[$R-435]\\ #,##0.00");
1402    m.insert(353, "[$R$-416]\\ #,##0.00");
1403    m.insert(354, "[$RD$-1C0A]#,##0.00");
1404    m.insert(355, "#,##0.00\\ [$RF-487]");
1405    m.insert(356, "[$RM-4409]#,##0.00");
1406    m.insert(357, "[$RM-43E]#,##0.00");
1407    m.insert(358, "#,##0.00\\ [$RON-418]");
1408    m.insert(359, "[$Rp-421]#,##0.00");
1409    m.insert(360, "[$Rs-420]#,##0.00_-");
1410    m.insert(361, "[$Rs.-849]\\ #,##0.00");
1411    m.insert(362, "#,##0.00\\ [$RSD-81A]");
1412    m.insert(363, "#,##0.00\\ [$RSD-C1A]");
1413    m.insert(364, "#,##0.00\\ [$RUB-46D]");
1414    m.insert(365, "#,##0.00\\ [$RUB-444]");
1415    m.insert(366, "[$S/.-C6B]\\ #,##0.00");
1416    m.insert(367, "[$S/.-280A]\\ #,##0.00");
1417    m.insert(368, "#,##0.00\\ [$SEK-143B]");
1418    m.insert(369, "#,##0.00\\ [$SEK-1C3B]");
1419    m.insert(370, "#,##0.00\\ [$so\u{02BB}m-443]");
1420    m.insert(371, "#,##0.00\\ [$so\u{02BB}m-843]");
1421    m.insert(372, "#,##0.00\\ [$SYP-45A]");
1422    m.insert(373, "[$THB-41E]#,##0.00");
1423    m.insert(374, "#,##0.00[$TMT-442]");
1424    m.insert(375, "[$US$-3009]#,##0.00");
1425    m.insert(376, "[$ZAR-46C]\\ #,##0.00");
1426    m.insert(377, "[$ZAR-430]#,##0.00");
1427    m.insert(378, "[$ZAR-431]#,##0.00");
1428    m.insert(379, "[$ZAR-432]\\ #,##0.00");
1429    m.insert(380, "[$ZAR-433]#,##0.00");
1430    m.insert(381, "[$ZAR-434]\\ #,##0.00");
1431    m.insert(382, "#,##0.00\\ [$z\u{0142}-415]");
1432    m.insert(383, "#,##0.00\\ [$\u{0434}\u{0435}\u{043D}-42F]");
1433    m.insert(384, "#,##0.00\\ [$\u{041A}\u{041C}-201A]");
1434    m.insert(385, "#,##0.00\\ [$\u{041A}\u{041C}-1C1A]");
1435    m.insert(386, "#,##0.00\\ [$\u{043B}\u{0432}.-402]");
1436    m.insert(387, "#,##0.00\\ [$\u{0440}.-423]");
1437    m.insert(388, "#,##0.00\\ [$\u{0441}\u{043E}\u{043C}-440]");
1438    m.insert(389, "#,##0.00\\ [$\u{0441}\u{043E}\u{043C}-428]");
1439    m.insert(390, "[$\u{062C}.\u{0645}.-C01]\\ #,##0.00_-");
1440    m.insert(391, "[$\u{062F}.\u{0623}.-2C01]\\ #,##0.00_-");
1441    m.insert(392, "[$\u{062F}.\u{0625}.-3801]\\ #,##0.00_-");
1442    m.insert(393, "[$\u{062F}.\u{0628}.-3C01]\\ #,##0.00_-");
1443    m.insert(394, "[$\u{062F}.\u{062A}.-1C01]\\ #,##0.00_-");
1444    m.insert(395, "[$\u{062F}.\u{062C}.-1401]\\ #,##0.00_-");
1445    m.insert(396, "[$\u{062F}.\u{0639}.-801]\\ #,##0.00_-");
1446    m.insert(397, "[$\u{062F}.\u{0643}.-3401]\\ #,##0.00_-");
1447    m.insert(398, "[$\u{062F}.\u{0644}.-1001]#,##0.00_-");
1448    m.insert(399, "[$\u{062F}.\u{0645}.-1801]\\ #,##0.00_-");
1449    m.insert(400, "[$\u{0631}-846]\\ #,##0.00");
1450    m.insert(401, "[$\u{0631}.\u{0633}.-401]\\ #,##0.00_-");
1451    m.insert(402, "[$\u{0631}.\u{0639}.-2001]\\ #,##0.00_-");
1452    m.insert(403, "[$\u{0631}.\u{0642}.-4001]\\ #,##0.00_-");
1453    m.insert(404, "[$\u{0631}.\u{064A}.-2401]\\ #,##0.00_-");
1454    m.insert(405, "[$\u{0631}\u{06CC}\u{0627}\u{0644}-429]#,##0.00_-");
1455    m.insert(406, "[$\u{0644}.\u{0633}.-2801]\\ #,##0.00_-");
1456    m.insert(407, "[$\u{0644}.\u{0644}.-3001]\\ #,##0.00_-");
1457    m.insert(408, "[$\u{1265}\u{122D}-45E]#,##0.00");
1458    m.insert(409, "[$\u{0930}\u{0942}-461]#,##0.00");
1459    m.insert(410, "[$\u{0DBB}\u{0DD4}.-45B]\\ #,##0.00");
1460    m.insert(411, "[$ADP]\\ #,##0.00");
1461    m.insert(412, "[$AED]\\ #,##0.00");
1462    m.insert(413, "[$AFA]\\ #,##0.00");
1463    m.insert(414, "[$AFN]\\ #,##0.00");
1464    m.insert(415, "[$ALL]\\ #,##0.00");
1465    m.insert(416, "[$AMD]\\ #,##0.00");
1466    m.insert(417, "[$ANG]\\ #,##0.00");
1467    m.insert(418, "[$AOA]\\ #,##0.00");
1468    m.insert(419, "[$ARS]\\ #,##0.00");
1469    m.insert(420, "[$ATS]\\ #,##0.00");
1470    m.insert(421, "[$AUD]\\ #,##0.00");
1471    m.insert(422, "[$AWG]\\ #,##0.00");
1472    m.insert(423, "[$AZM]\\ #,##0.00");
1473    m.insert(424, "[$AZN]\\ #,##0.00");
1474    m.insert(425, "[$BAM]\\ #,##0.00");
1475    m.insert(426, "[$BBD]\\ #,##0.00");
1476    m.insert(427, "[$BDT]\\ #,##0.00");
1477    m.insert(428, "[$BEF]\\ #,##0.00");
1478    m.insert(429, "[$BGL]\\ #,##0.00");
1479    m.insert(430, "[$BGN]\\ #,##0.00");
1480    m.insert(431, "[$BHD]\\ #,##0.00");
1481    m.insert(432, "[$BIF]\\ #,##0.00");
1482    m.insert(433, "[$BMD]\\ #,##0.00");
1483    m.insert(434, "[$BND]\\ #,##0.00");
1484    m.insert(435, "[$BOB]\\ #,##0.00");
1485    m.insert(436, "[$BOV]\\ #,##0.00");
1486    m.insert(437, "[$BRL]\\ #,##0.00");
1487    m.insert(438, "[$BSD]\\ #,##0.00");
1488    m.insert(439, "[$BTN]\\ #,##0.00");
1489    m.insert(440, "[$BWP]\\ #,##0.00");
1490    m.insert(441, "[$BYR]\\ #,##0.00");
1491    m.insert(442, "[$BZD]\\ #,##0.00");
1492    m.insert(443, "[$CAD]\\ #,##0.00");
1493    m.insert(444, "[$CDF]\\ #,##0.00");
1494    m.insert(445, "[$CHE]\\ #,##0.00");
1495    m.insert(446, "[$CHF]\\ #,##0.00");
1496    m.insert(447, "[$CHW]\\ #,##0.00");
1497    m.insert(448, "[$CLF]\\ #,##0.00");
1498    m.insert(449, "[$CLP]\\ #,##0.00");
1499    m.insert(450, "[$CNY]\\ #,##0.00");
1500    m.insert(451, "[$COP]\\ #,##0.00");
1501    m.insert(452, "[$COU]\\ #,##0.00");
1502    m.insert(453, "[$CRC]\\ #,##0.00");
1503    m.insert(454, "[$CSD]\\ #,##0.00");
1504    m.insert(455, "[$CUC]\\ #,##0.00");
1505    m.insert(456, "[$CVE]\\ #,##0.00");
1506    m.insert(457, "[$CYP]\\ #,##0.00");
1507    m.insert(458, "[$CZK]\\ #,##0.00");
1508    m.insert(459, "[$DEM]\\ #,##0.00");
1509    m.insert(460, "[$DJF]\\ #,##0.00");
1510    m.insert(461, "[$DKK]\\ #,##0.00");
1511    m.insert(462, "[$DOP]\\ #,##0.00");
1512    m.insert(463, "[$DZD]\\ #,##0.00");
1513    m.insert(464, "[$ECS]\\ #,##0.00");
1514    m.insert(465, "[$ECV]\\ #,##0.00");
1515    m.insert(466, "[$EEK]\\ #,##0.00");
1516    m.insert(467, "[$EGP]\\ #,##0.00");
1517    m.insert(468, "[$ERN]\\ #,##0.00");
1518    m.insert(469, "[$ESP]\\ #,##0.00");
1519    m.insert(470, "[$ETB]\\ #,##0.00");
1520    m.insert(471, "[$EUR]\\ #,##0.00");
1521    m.insert(472, "[$FIM]\\ #,##0.00");
1522    m.insert(473, "[$FJD]\\ #,##0.00");
1523    m.insert(474, "[$FKP]\\ #,##0.00");
1524    m.insert(475, "[$FRF]\\ #,##0.00");
1525    m.insert(476, "[$GBP]\\ #,##0.00");
1526    m.insert(477, "[$GEL]\\ #,##0.00");
1527    m.insert(478, "[$GHC]\\ #,##0.00");
1528    m.insert(479, "[$GHS]\\ #,##0.00");
1529    m.insert(480, "[$GIP]\\ #,##0.00");
1530    m.insert(481, "[$GMD]\\ #,##0.00");
1531    m.insert(482, "[$GNF]\\ #,##0.00");
1532    m.insert(483, "[$GRD]\\ #,##0.00");
1533    m.insert(484, "[$GTQ]\\ #,##0.00");
1534    m.insert(485, "[$GYD]\\ #,##0.00");
1535    m.insert(486, "[$HKD]\\ #,##0.00");
1536    m.insert(487, "[$HNL]\\ #,##0.00");
1537    m.insert(488, "[$HRK]\\ #,##0.00");
1538    m.insert(489, "[$HTG]\\ #,##0.00");
1539    m.insert(490, "[$HUF]\\ #,##0.00");
1540    m.insert(491, "[$IDR]\\ #,##0.00");
1541    m.insert(492, "[$IEP]\\ #,##0.00");
1542    m.insert(493, "[$ILS]\\ #,##0.00");
1543    m.insert(494, "[$INR]\\ #,##0.00");
1544    m.insert(495, "[$IQD]\\ #,##0.00");
1545    m.insert(496, "[$IRR]\\ #,##0.00");
1546    m.insert(497, "[$ISK]\\ #,##0.00");
1547    m.insert(498, "[$ITL]\\ #,##0.00");
1548    m.insert(499, "[$JMD]\\ #,##0.00");
1549    m.insert(500, "[$JOD]\\ #,##0.00");
1550    m.insert(501, "[$JPY]\\ #,##0.00");
1551    m.insert(502, "[$KAF]\\ #,##0.00");
1552    m.insert(503, "[$KES]\\ #,##0.00");
1553    m.insert(504, "[$KGS]\\ #,##0.00");
1554    m.insert(505, "[$KHR]\\ #,##0.00");
1555    m.insert(506, "[$KMF]\\ #,##0.00");
1556    m.insert(507, "[$KPW]\\ #,##0.00");
1557    m.insert(508, "[$KRW]\\ #,##0.00");
1558    m.insert(509, "[$KWD]\\ #,##0.00");
1559    m.insert(510, "[$KYD]\\ #,##0.00");
1560    m.insert(511, "[$KZT]\\ #,##0.00");
1561    m.insert(512, "[$LAK]\\ #,##0.00");
1562    m.insert(513, "[$LBP]\\ #,##0.00");
1563    m.insert(514, "[$LKR]\\ #,##0.00");
1564    m.insert(515, "[$LRD]\\ #,##0.00");
1565    m.insert(516, "[$LSL]\\ #,##0.00");
1566    m.insert(517, "[$LTL]\\ #,##0.00");
1567    m.insert(518, "[$LUF]\\ #,##0.00");
1568    m.insert(519, "[$LVL]\\ #,##0.00");
1569    m.insert(520, "[$LYD]\\ #,##0.00");
1570    m.insert(521, "[$MAD]\\ #,##0.00");
1571    m.insert(522, "[$MDL]\\ #,##0.00");
1572    m.insert(523, "[$MGA]\\ #,##0.00");
1573    m.insert(524, "[$MGF]\\ #,##0.00");
1574    m.insert(525, "[$MKD]\\ #,##0.00");
1575    m.insert(526, "[$MMK]\\ #,##0.00");
1576    m.insert(527, "[$MNT]\\ #,##0.00");
1577    m.insert(528, "[$MOP]\\ #,##0.00");
1578    m.insert(529, "[$MRO]\\ #,##0.00");
1579    m.insert(530, "[$MTL]\\ #,##0.00");
1580    m.insert(531, "[$MUR]\\ #,##0.00");
1581    m.insert(532, "[$MVR]\\ #,##0.00");
1582    m.insert(533, "[$MWK]\\ #,##0.00");
1583    m.insert(534, "[$MXN]\\ #,##0.00");
1584    m.insert(535, "[$MXV]\\ #,##0.00");
1585    m.insert(536, "[$MYR]\\ #,##0.00");
1586    m.insert(537, "[$MZM]\\ #,##0.00");
1587    m.insert(538, "[$MZN]\\ #,##0.00");
1588    m.insert(539, "[$NAD]\\ #,##0.00");
1589    m.insert(540, "[$NGN]\\ #,##0.00");
1590    m.insert(541, "[$NIO]\\ #,##0.00");
1591    m.insert(542, "[$NLG]\\ #,##0.00");
1592    m.insert(543, "[$NOK]\\ #,##0.00");
1593    m.insert(544, "[$NPR]\\ #,##0.00");
1594    m.insert(545, "[$NTD]\\ #,##0.00");
1595    m.insert(546, "[$NZD]\\ #,##0.00");
1596    m.insert(547, "[$OMR]\\ #,##0.00");
1597    m.insert(548, "[$PAB]\\ #,##0.00");
1598    m.insert(549, "[$PEN]\\ #,##0.00");
1599    m.insert(550, "[$PGK]\\ #,##0.00");
1600    m.insert(551, "[$PHP]\\ #,##0.00");
1601    m.insert(552, "[$PKR]\\ #,##0.00");
1602    m.insert(553, "[$PLN]\\ #,##0.00");
1603    m.insert(554, "[$PTE]\\ #,##0.00");
1604    m.insert(555, "[$PYG]\\ #,##0.00");
1605    m.insert(556, "[$QAR]\\ #,##0.00");
1606    m.insert(557, "[$ROL]\\ #,##0.00");
1607    m.insert(558, "[$RON]\\ #,##0.00");
1608    m.insert(559, "[$RSD]\\ #,##0.00");
1609    m.insert(560, "[$RUB]\\ #,##0.00");
1610    m.insert(561, "[$RUR]\\ #,##0.00");
1611    m.insert(562, "[$RWF]\\ #,##0.00");
1612    m.insert(563, "[$SAR]\\ #,##0.00");
1613    m.insert(564, "[$SBD]\\ #,##0.00");
1614    m.insert(565, "[$SCR]\\ #,##0.00");
1615    m.insert(566, "[$SDD]\\ #,##0.00");
1616    m.insert(567, "[$SDG]\\ #,##0.00");
1617    m.insert(568, "[$SDP]\\ #,##0.00");
1618    m.insert(569, "[$SEK]\\ #,##0.00");
1619    m.insert(570, "[$SGD]\\ #,##0.00");
1620    m.insert(571, "[$SHP]\\ #,##0.00");
1621    m.insert(572, "[$SIT]\\ #,##0.00");
1622    m.insert(573, "[$SKK]\\ #,##0.00");
1623    m.insert(574, "[$SLL]\\ #,##0.00");
1624    m.insert(575, "[$SOS]\\ #,##0.00");
1625    m.insert(576, "[$SPL]\\ #,##0.00");
1626    m.insert(577, "[$SRD]\\ #,##0.00");
1627    m.insert(578, "[$SRG]\\ #,##0.00");
1628    m.insert(579, "[$STD]\\ #,##0.00");
1629    m.insert(580, "[$SVC]\\ #,##0.00");
1630    m.insert(581, "[$SYP]\\ #,##0.00");
1631    m.insert(582, "[$SZL]\\ #,##0.00");
1632    m.insert(583, "[$THB]\\ #,##0.00");
1633    m.insert(584, "[$TJR]\\ #,##0.00");
1634    m.insert(585, "[$TJS]\\ #,##0.00");
1635    m.insert(586, "[$TMM]\\ #,##0.00");
1636    m.insert(587, "[$TMT]\\ #,##0.00");
1637    m.insert(588, "[$TND]\\ #,##0.00");
1638    m.insert(589, "[$TOP]\\ #,##0.00");
1639    m.insert(590, "[$TRL]\\ #,##0.00");
1640    m.insert(591, "[$TRY]\\ #,##0.00");
1641    m.insert(592, "[$TTD]\\ #,##0.00");
1642    m.insert(593, "[$TWD]\\ #,##0.00");
1643    m.insert(594, "[$TZS]\\ #,##0.00");
1644    m.insert(595, "[$UAH]\\ #,##0.00");
1645    m.insert(596, "[$UGX]\\ #,##0.00");
1646    m.insert(597, "[$USD]\\ #,##0.00");
1647    m.insert(598, "[$USN]\\ #,##0.00");
1648    m.insert(599, "[$USS]\\ #,##0.00");
1649    m.insert(600, "[$UYI]\\ #,##0.00");
1650    m.insert(601, "[$UYU]\\ #,##0.00");
1651    m.insert(602, "[$UZS]\\ #,##0.00");
1652    m.insert(603, "[$VEB]\\ #,##0.00");
1653    m.insert(604, "[$VEF]\\ #,##0.00");
1654    m.insert(605, "[$VND]\\ #,##0.00");
1655    m.insert(606, "[$VUV]\\ #,##0.00");
1656    m.insert(607, "[$WST]\\ #,##0.00");
1657    m.insert(608, "[$XAF]\\ #,##0.00");
1658    m.insert(609, "[$XAG]\\ #,##0.00");
1659    m.insert(610, "[$XAU]\\ #,##0.00");
1660    m.insert(611, "[$XB5]\\ #,##0.00");
1661    m.insert(612, "[$XBA]\\ #,##0.00");
1662    m.insert(613, "[$XBB]\\ #,##0.00");
1663    m.insert(614, "[$XBC]\\ #,##0.00");
1664    m.insert(615, "[$XBD]\\ #,##0.00");
1665    m.insert(616, "[$XCD]\\ #,##0.00");
1666    m.insert(617, "[$XDR]\\ #,##0.00");
1667    m.insert(618, "[$XFO]\\ #,##0.00");
1668    m.insert(619, "[$XFU]\\ #,##0.00");
1669    m.insert(620, "[$XOF]\\ #,##0.00");
1670    m.insert(621, "[$XPD]\\ #,##0.00");
1671    m.insert(622, "[$XPF]\\ #,##0.00");
1672    m.insert(623, "[$XPT]\\ #,##0.00");
1673    m.insert(624, "[$XTS]\\ #,##0.00");
1674    m.insert(625, "[$XXX]\\ #,##0.00");
1675    m.insert(626, "[$YER]\\ #,##0.00");
1676    m.insert(627, "[$YUM]\\ #,##0.00");
1677    m.insert(628, "[$ZAR]\\ #,##0.00");
1678    m.insert(629, "[$ZMK]\\ #,##0.00");
1679    m.insert(630, "[$ZMW]\\ #,##0.00");
1680    m.insert(631, "[$ZWD]\\ #,##0.00");
1681    m.insert(632, "[$ZWL]\\ #,##0.00");
1682    m.insert(633, "[$ZWN]\\ #,##0.00");
1683    m.insert(634, "[$ZWR]\\ #,##0.00");
1684    m
1685});
1686
1687fn get_num_fmt_id(style_sheet: &XlsxStyleSheet, style: &Style) -> i32 {
1688    let num_fmt = style.num_fmt as i32;
1689    if BUILT_IN_NUM_FMT.contains_key(&num_fmt) {
1690        return num_fmt;
1691    }
1692    if (27..=36).contains(&num_fmt) || (50..=81).contains(&num_fmt) {
1693        return num_fmt;
1694    }
1695    if let Some(fmt_code) = CURRENCY_NUM_FMT.get(&num_fmt) {
1696        if let Some(ref num_fmts) = style_sheet.num_fmts {
1697            for num_fmt_entry in &num_fmts.num_fmt {
1698                if &num_fmt_entry.format_code == fmt_code {
1699                    return num_fmt_entry.num_fmt_id as i32;
1700                }
1701            }
1702        }
1703        return num_fmt;
1704    }
1705    -1
1706}
1707
1708fn new_num_fmt(style_sheet: &mut XlsxStyleSheet, style: &Style) -> i32 {
1709    let mut dp = String::from("0");
1710    if let Some(places) = style.decimal_places {
1711        if places > 0 {
1712            dp.push('.');
1713            for _ in 0..places {
1714                dp.push('0');
1715            }
1716        }
1717    }
1718    if style.custom_num_fmt.is_some() {
1719        let id = get_custom_num_fmt_id(style_sheet, style);
1720        if id != -1 {
1721            return id;
1722        }
1723        return set_custom_num_fmt(style_sheet, style);
1724    }
1725    let num_fmt = style.num_fmt as i32;
1726    if BUILT_IN_NUM_FMT.contains_key(&num_fmt) {
1727        return num_fmt;
1728    }
1729    if let Some(fmt_code) = CURRENCY_NUM_FMT.get(&num_fmt).copied() {
1730        let mut fc = fmt_code.to_string();
1731        if let Some(places) = style.decimal_places {
1732            if places > 0 {
1733                fc = fc.replace("0.00", &dp);
1734            }
1735        }
1736        if style.neg_red {
1737            fc = format!("{fc};[Red]{fc}");
1738        }
1739        ensure_num_fmts(style_sheet);
1740        let num_fmts = style_sheet.num_fmts.as_mut().unwrap();
1741        let mut num_fmt_id = 164;
1742        if let Some(last) = num_fmts.num_fmt.last() {
1743            num_fmt_id = last.num_fmt_id as i32 + 1;
1744        }
1745        num_fmts.num_fmt.push(XlsxNumFmt {
1746            num_fmt_id: num_fmt_id as i64,
1747            format_code: fc,
1748            format_code_16: None,
1749        });
1750        num_fmts.count += 1;
1751        return num_fmt_id;
1752    }
1753    if is_lang_num_fmt(num_fmt) {
1754        return num_fmt;
1755    }
1756    0
1757}
1758
1759fn set_custom_num_fmt(style_sheet: &mut XlsxStyleSheet, style: &Style) -> i32 {
1760    let mut nf = XlsxNumFmt {
1761        num_fmt_id: 163,
1762        format_code: style.custom_num_fmt.as_ref().unwrap().clone(),
1763        format_code_16: None,
1764    };
1765    ensure_num_fmts(style_sheet);
1766    let num_fmts = style_sheet.num_fmts.as_mut().unwrap();
1767    for num_fmt in &num_fmts.num_fmt {
1768        if num_fmt.num_fmt_id >= nf.num_fmt_id {
1769            nf.num_fmt_id = num_fmt.num_fmt_id;
1770        }
1771    }
1772    nf.num_fmt_id += 1;
1773    let id = nf.num_fmt_id;
1774    num_fmts.num_fmt.push(nf);
1775    num_fmts.count = num_fmts.num_fmt.len() as i64;
1776    id as i32
1777}
1778
1779fn get_custom_num_fmt_id(style_sheet: &XlsxStyleSheet, style: &Style) -> i32 {
1780    if style_sheet.num_fmts.is_none() {
1781        return -1;
1782    }
1783    if let Some(ref custom) = style.custom_num_fmt {
1784        for num_fmt in &style_sheet.num_fmts.as_ref().unwrap().num_fmt {
1785            if &num_fmt.format_code == custom {
1786                return num_fmt.num_fmt_id as i32;
1787            }
1788        }
1789    }
1790    -1
1791}
1792
1793fn is_lang_num_fmt(id: i32) -> bool {
1794    (27..=36).contains(&id) || (50..=62).contains(&id) || (67..=81).contains(&id)
1795}
1796
1797// ------------------------------------------------------------------
1798// Style extraction (GetStyle)
1799// ------------------------------------------------------------------
1800
1801fn should_extract_fill(xf: &XlsxXf, ss: &XlsxStyleSheet) -> bool {
1802    (xf.apply_fill.is_none() || xf.apply_fill.unwrap())
1803        && xf.fill_id.is_some()
1804        && ss.fills.is_some()
1805        && (*xf.fill_id.as_ref().unwrap() as usize) < ss.fills.as_ref().unwrap().fill.len()
1806}
1807
1808fn should_extract_border(xf: &XlsxXf, ss: &XlsxStyleSheet) -> bool {
1809    (xf.apply_border.is_none() || xf.apply_border.unwrap())
1810        && xf.border_id.is_some()
1811        && ss.borders.is_some()
1812        && (*xf.border_id.as_ref().unwrap() as usize) < ss.borders.as_ref().unwrap().border.len()
1813}
1814
1815fn should_extract_font(xf: &XlsxXf, ss: &XlsxStyleSheet) -> bool {
1816    (xf.apply_font.is_none() || xf.apply_font.unwrap())
1817        && xf.font_id.is_some()
1818        && ss.fonts.is_some()
1819        && (*xf.font_id.as_ref().unwrap() as usize) < ss.fonts.as_ref().unwrap().font.len()
1820}
1821
1822fn should_extract_alignment(xf: &XlsxXf, _ss: &XlsxStyleSheet) -> bool {
1823    xf.apply_alignment.is_none() || xf.apply_alignment.unwrap()
1824}
1825
1826fn should_extract_protection(xf: &XlsxXf, _ss: &XlsxStyleSheet) -> bool {
1827    xf.apply_protection.is_none() || xf.apply_protection.unwrap()
1828}
1829
1830fn extract_border(f: &File, border: &XlsxBorder, style: &mut Style) {
1831    let mut extract = |line_type: &str, line: &XlsxLine| {
1832        if line
1833            .style
1834            .as_deref()
1835            .map(|s| !s.is_empty())
1836            .unwrap_or(false)
1837        {
1838            let style_idx =
1839                index_in_static_slice(STYLE_BORDERS, line.style.as_deref().unwrap(), false);
1840            if style_idx != -1 {
1841                style.border.push(Border {
1842                    r#type: line_type.to_string(),
1843                    color: get_theme_color(f, line.color.as_ref()),
1844                    style: style_idx as i64,
1845                });
1846            }
1847        }
1848    };
1849    let types = [
1850        "left",
1851        "right",
1852        "top",
1853        "bottom",
1854        "diagonalUp",
1855        "diagonalDown",
1856    ];
1857    let lines = [
1858        &border.left,
1859        &border.right,
1860        &border.top,
1861        &border.bottom,
1862        &border.diagonal,
1863        &border.diagonal,
1864    ];
1865    for (i, line) in lines.iter().enumerate() {
1866        if let Some(line) = line {
1867            if i < 4 {
1868                extract(types[i], line);
1869            }
1870            if i == 4 && border.diagonal_up == Some(true) {
1871                extract(types[i], line);
1872            }
1873            if i == 5 && border.diagonal_down == Some(true) {
1874                extract(types[i], line);
1875            }
1876        }
1877    }
1878}
1879
1880fn get_theme_color(f: &File, clr: Option<&XlsxColor>) -> String {
1881    let Some(c) = clr else {
1882        return String::new();
1883    };
1884    let rgb = f.get_base_color(
1885        c.rgb.as_deref().unwrap_or(""),
1886        c.indexed.unwrap_or(0),
1887        c.theme,
1888    );
1889    if rgb.is_empty() {
1890        return String::new();
1891    }
1892    let tint = c.tint.unwrap_or(0.0);
1893    if tint != 0.0 {
1894        if let Some(s) = theme_color(&rgb, tint).strip_prefix("FF") {
1895            s.to_string()
1896        } else {
1897            rgb
1898        }
1899    } else {
1900        rgb
1901    }
1902}
1903
1904fn extract_fill(f: &File, fill: &XlsxFill, style: &mut Style) {
1905    // Prefer pattern fills. Empty sibling elements like `<gradientFill/>` can be
1906    // emitted by quick_xml when `skip_serializing_if` is not used, so checking
1907    // pattern first makes round-trips robust.
1908    if let Some(ref pf) = fill.pattern_fill {
1909        if pf.pattern_type.is_some() || pf.fg_color.is_some() || pf.bg_color.is_some() {
1910            style.fill.r#type = "pattern".to_string();
1911            if let Some(ref pt) = pf.pattern_type {
1912                style.fill.pattern = index_in_static_slice(STYLE_FILL_PATTERNS, pt, false) as i64;
1913            }
1914            if let Some(ref bg) = pf.bg_color {
1915                if bg.auto != Some(true) {
1916                    style.fill.color = vec![get_theme_color(f, Some(bg))];
1917                }
1918            }
1919            if let Some(ref fg) = pf.fg_color {
1920                if fg.auto != Some(true) {
1921                    style.fill.color = vec![get_theme_color(f, Some(fg))];
1922                }
1923            }
1924            return;
1925        }
1926    }
1927    if let Some(ref gf) = fill.gradient_fill {
1928        style.fill.r#type = "gradient".to_string();
1929        for (shading, variant) in style_fill_variants().iter().enumerate() {
1930            if gf.bottom == variant.bottom
1931                && gf.degree == variant.degree
1932                && gf.left == variant.left
1933                && gf.right == variant.right
1934                && gf.top == variant.top
1935                && gf.r#type == variant.r#type
1936            {
1937                style.fill.shading = shading as i64;
1938                break;
1939            }
1940        }
1941        for stop in &gf.stop {
1942            style
1943                .fill
1944                .color
1945                .push(get_theme_color(f, stop.color.as_ref()));
1946        }
1947    }
1948}
1949
1950fn extract_font(fnt: &XlsxFont) -> Font {
1951    let mut font = Font::default();
1952    font.charset = fnt.charset.as_ref().and_then(|c| c.val);
1953    font.bold = fnt.b.as_ref().and_then(|b| b.val);
1954    font.italic = fnt.i.as_ref().and_then(|i| i.val);
1955    font.strike = fnt.strike.as_ref().and_then(|s| s.val);
1956    if let Some(ref u) = fnt.u {
1957        font.underline = u.val.clone();
1958        if font.underline.as_deref() == Some("") {
1959            font.underline = Some("single".to_string());
1960        }
1961    }
1962    font.name = fnt.name.as_ref().and_then(|n| n.val.clone());
1963    font.size = fnt.sz.as_ref().and_then(|s| s.val);
1964    if let Some(ref color) = fnt.color {
1965        font.color = color
1966            .rgb
1967            .as_ref()
1968            .map(|rgb| rgb.strip_prefix("FF").unwrap_or(rgb).to_string());
1969        font.color_indexed = color.indexed;
1970        font.color_theme = color.theme;
1971        font.color_tint = color.tint.unwrap_or(0.0);
1972    }
1973    font.vert_align = fnt.vert_align.as_ref().and_then(|v| v.val.clone());
1974    font
1975}
1976
1977fn extract_alignment(a: &XlsxAlignment) -> Alignment {
1978    Alignment {
1979        horizontal: a.horizontal.clone().unwrap_or_default(),
1980        indent: a.indent.unwrap_or(0),
1981        justify_last_line: a.justify_last_line.unwrap_or(false),
1982        reading_order: a.reading_order.unwrap_or(0),
1983        relative_indent: a.relative_indent.unwrap_or(0),
1984        shrink_to_fit: a.shrink_to_fit.unwrap_or(false),
1985        text_rotation: a.text_rotation.unwrap_or(0),
1986        vertical: a.vertical.clone().unwrap_or_default(),
1987        wrap_text: a.wrap_text.unwrap_or(false),
1988    }
1989}
1990
1991fn extract_protection(p: &XlsxProtection) -> Protection {
1992    Protection {
1993        hidden: p.hidden.unwrap_or(false),
1994        locked: p.locked.unwrap_or(false),
1995    }
1996}
1997
1998fn extract_num_fmt(num_fmt_id: i32, style_sheet: &XlsxStyleSheet, style: &mut Style) {
1999    if let Some(code) = BUILT_IN_NUM_FMT.get(&num_fmt_id) {
2000        style.num_fmt = num_fmt_id as i64;
2001        if let Some(dp) = extract_num_fmt_decimal_places(code) {
2002            style.decimal_places = Some(dp);
2003        }
2004        return;
2005    }
2006    if is_lang_num_fmt(num_fmt_id) {
2007        style.num_fmt = num_fmt_id as i64;
2008        return;
2009    }
2010    if let Some(ref num_fmts) = style_sheet.num_fmts {
2011        for num_fmt in &num_fmts.num_fmt {
2012            if num_fmt.num_fmt_id as i32 == num_fmt_id {
2013                if let Some(dp) = extract_num_fmt_decimal_places(&num_fmt.format_code) {
2014                    style.decimal_places = Some(dp);
2015                }
2016                style.custom_num_fmt = Some(num_fmt.format_code.clone());
2017                if num_fmt.format_code.contains(";[Red]") {
2018                    style.neg_red = true;
2019                }
2020                for (&id, fmt_code) in CURRENCY_NUM_FMT.iter() {
2021                    let mut fc = fmt_code.to_string();
2022                    if style.neg_red {
2023                        fc = format!("{fc};[Red]{fc}");
2024                    }
2025                    if num_fmt.format_code == fc {
2026                        style.num_fmt = id as i64;
2027                    }
2028                }
2029            }
2030        }
2031    }
2032}
2033
2034fn extract_num_fmt_decimal_places(code: &str) -> Option<i64> {
2035    if let Some(pos) = code.find('.') {
2036        let rest = &code[pos + 1..];
2037        let count = rest.chars().take_while(|c| c.is_ascii_digit()).count();
2038        if count > 0 {
2039            return Some(count as i64);
2040        }
2041    }
2042    None
2043}
2044
2045// ------------------------------------------------------------------
2046// Ensure stylesheet containers exist
2047// ------------------------------------------------------------------
2048
2049fn ensure_fonts(ss: &mut XlsxStyleSheet) {
2050    if ss.fonts.is_none() {
2051        ss.fonts = Some(XlsxFonts::default());
2052    }
2053}
2054
2055fn ensure_fills(ss: &mut XlsxStyleSheet) {
2056    if ss.fills.is_none() {
2057        ss.fills = Some(XlsxFills::default());
2058    }
2059}
2060
2061fn ensure_borders(ss: &mut XlsxStyleSheet) {
2062    if ss.borders.is_none() {
2063        ss.borders = Some(XlsxBorders::default());
2064    }
2065}
2066
2067fn ensure_cell_xfs(ss: &mut XlsxStyleSheet) {
2068    if ss.cell_xfs.is_none() {
2069        ss.cell_xfs = Some(XlsxCellXfs::default());
2070    }
2071}
2072
2073fn ensure_num_fmts(ss: &mut XlsxStyleSheet) {
2074    if ss.num_fmts.is_none() {
2075        ss.num_fmts = Some(XlsxNumFmts::default());
2076    }
2077}
2078
2079fn new_dxf_num_fmt(style_sheet: &XlsxStyleSheet, style: &Style) -> Option<XlsxNumFmt> {
2080    let mut dp = String::from("0");
2081    if let Some(places) = style.decimal_places {
2082        if places > 0 {
2083            dp.push('.');
2084            for _ in 0..places {
2085                dp.push('0');
2086            }
2087        }
2088    }
2089    if let Some(ref custom) = style.custom_num_fmt {
2090        let mut num_fmt_id = 164i64;
2091        if let Some(ref dxfs) = style_sheet.dxfs {
2092            for d in &dxfs.dxfs {
2093                if let Some(ref nf) = d.num_fmt {
2094                    if nf.num_fmt_id > num_fmt_id {
2095                        num_fmt_id = nf.num_fmt_id;
2096                    }
2097                }
2098            }
2099        }
2100        return Some(XlsxNumFmt {
2101            num_fmt_id: num_fmt_id + 1,
2102            format_code: custom.clone(),
2103            format_code_16: None,
2104        });
2105    }
2106    let num_fmt = style.num_fmt as i32;
2107    if let Some(code) = BUILT_IN_NUM_FMT.get(&num_fmt) {
2108        return Some(XlsxNumFmt {
2109            num_fmt_id: num_fmt as i64,
2110            format_code: code.to_string(),
2111            format_code_16: None,
2112        });
2113    }
2114    if let Some(fmt_code) = CURRENCY_NUM_FMT.get(&num_fmt).copied() {
2115        let mut fc = fmt_code.to_string();
2116        if let Some(places) = style.decimal_places {
2117            if places > 0 {
2118                fc = fc.replace("0.00", &dp);
2119            }
2120        }
2121        if style.neg_red {
2122            fc = format!("{fc};[Red]{fc}");
2123        }
2124        return Some(XlsxNumFmt {
2125            num_fmt_id: num_fmt as i64,
2126            format_code: fc,
2127            format_code_16: None,
2128        });
2129    }
2130    None
2131}
2132
2133#[allow(dead_code)]
2134fn ensure_cell_styles(ss: &mut XlsxStyleSheet) {
2135    if ss.cell_styles.is_none() {
2136        ss.cell_styles = Some(XlsxCellStyles::default());
2137    }
2138}
2139
2140// ------------------------------------------------------------------
2141// Lookup tables
2142// ------------------------------------------------------------------
2143
2144const STYLE_BORDERS: &[&str] = &[
2145    "none",
2146    "thin",
2147    "medium",
2148    "dashed",
2149    "dotted",
2150    "thick",
2151    "double",
2152    "hair",
2153    "mediumDashed",
2154    "dashDot",
2155    "mediumDashDot",
2156    "dashDotDot",
2157    "mediumDashDotDot",
2158    "slantDashDot",
2159];
2160
2161const STYLE_FILL_PATTERNS: &[&str] = &[
2162    "none",
2163    "solid",
2164    "mediumGray",
2165    "darkGray",
2166    "lightGray",
2167    "darkHorizontal",
2168    "darkVertical",
2169    "darkDown",
2170    "darkUp",
2171    "darkGrid",
2172    "darkTrellis",
2173    "lightHorizontal",
2174    "lightVertical",
2175    "lightDown",
2176    "lightUp",
2177    "lightGrid",
2178    "lightTrellis",
2179    "gray125",
2180    "gray0625",
2181];
2182
2183fn index_in_static_slice(slice: &[&str], value: &str, case_sensitive: bool) -> i64 {
2184    for (idx, item) in slice.iter().enumerate() {
2185        let matches = if case_sensitive {
2186            *item == value
2187        } else {
2188            item.eq_ignore_ascii_case(value)
2189        };
2190        if matches {
2191            return idx as i64;
2192        }
2193    }
2194    -1
2195}
2196
2197fn style_fill_variants() -> Vec<XlsxGradientFill> {
2198    vec![
2199        XlsxGradientFill {
2200            degree: Some(90.0),
2201            stop: vec![
2202                XlsxGradientFillStop::default(),
2203                XlsxGradientFillStop {
2204                    position: 1.0,
2205                    color: None,
2206                },
2207            ],
2208            ..Default::default()
2209        },
2210        XlsxGradientFill {
2211            degree: Some(270.0),
2212            stop: vec![
2213                XlsxGradientFillStop::default(),
2214                XlsxGradientFillStop {
2215                    position: 1.0,
2216                    color: None,
2217                },
2218            ],
2219            ..Default::default()
2220        },
2221        XlsxGradientFill {
2222            degree: Some(90.0),
2223            stop: vec![
2224                XlsxGradientFillStop::default(),
2225                XlsxGradientFillStop {
2226                    position: 0.5,
2227                    color: None,
2228                },
2229                XlsxGradientFillStop {
2230                    position: 1.0,
2231                    color: None,
2232                },
2233            ],
2234            ..Default::default()
2235        },
2236        XlsxGradientFill {
2237            stop: vec![
2238                XlsxGradientFillStop::default(),
2239                XlsxGradientFillStop {
2240                    position: 1.0,
2241                    color: None,
2242                },
2243            ],
2244            ..Default::default()
2245        },
2246        XlsxGradientFill {
2247            degree: Some(180.0),
2248            stop: vec![
2249                XlsxGradientFillStop::default(),
2250                XlsxGradientFillStop {
2251                    position: 1.0,
2252                    color: None,
2253                },
2254            ],
2255            ..Default::default()
2256        },
2257        XlsxGradientFill {
2258            stop: vec![
2259                XlsxGradientFillStop::default(),
2260                XlsxGradientFillStop {
2261                    position: 0.5,
2262                    color: None,
2263                },
2264                XlsxGradientFillStop {
2265                    position: 1.0,
2266                    color: None,
2267                },
2268            ],
2269            ..Default::default()
2270        },
2271        XlsxGradientFill {
2272            degree: Some(45.0),
2273            stop: vec![
2274                XlsxGradientFillStop::default(),
2275                XlsxGradientFillStop {
2276                    position: 1.0,
2277                    color: None,
2278                },
2279            ],
2280            ..Default::default()
2281        },
2282        XlsxGradientFill {
2283            degree: Some(255.0),
2284            stop: vec![
2285                XlsxGradientFillStop::default(),
2286                XlsxGradientFillStop {
2287                    position: 1.0,
2288                    color: None,
2289                },
2290            ],
2291            ..Default::default()
2292        },
2293        XlsxGradientFill {
2294            degree: Some(45.0),
2295            stop: vec![
2296                XlsxGradientFillStop::default(),
2297                XlsxGradientFillStop {
2298                    position: 0.5,
2299                    color: None,
2300                },
2301                XlsxGradientFillStop {
2302                    position: 1.0,
2303                    color: None,
2304                },
2305            ],
2306            ..Default::default()
2307        },
2308        XlsxGradientFill {
2309            degree: Some(135.0),
2310            stop: vec![
2311                XlsxGradientFillStop::default(),
2312                XlsxGradientFillStop {
2313                    position: 1.0,
2314                    color: None,
2315                },
2316            ],
2317            ..Default::default()
2318        },
2319        XlsxGradientFill {
2320            degree: Some(315.0),
2321            stop: vec![
2322                XlsxGradientFillStop::default(),
2323                XlsxGradientFillStop {
2324                    position: 1.0,
2325                    color: None,
2326                },
2327            ],
2328            ..Default::default()
2329        },
2330        XlsxGradientFill {
2331            degree: Some(135.0),
2332            stop: vec![
2333                XlsxGradientFillStop::default(),
2334                XlsxGradientFillStop {
2335                    position: 0.5,
2336                    color: None,
2337                },
2338                XlsxGradientFillStop {
2339                    position: 1.0,
2340                    color: None,
2341                },
2342            ],
2343            ..Default::default()
2344        },
2345        XlsxGradientFill {
2346            r#type: Some("path".to_string()),
2347            stop: vec![
2348                XlsxGradientFillStop::default(),
2349                XlsxGradientFillStop {
2350                    position: 1.0,
2351                    color: None,
2352                },
2353            ],
2354            ..Default::default()
2355        },
2356        XlsxGradientFill {
2357            r#type: Some("path".to_string()),
2358            left: Some(1.0),
2359            right: Some(1.0),
2360            stop: vec![
2361                XlsxGradientFillStop::default(),
2362                XlsxGradientFillStop {
2363                    position: 1.0,
2364                    color: None,
2365                },
2366            ],
2367            ..Default::default()
2368        },
2369        XlsxGradientFill {
2370            r#type: Some("path".to_string()),
2371            bottom: Some(1.0),
2372            top: Some(1.0),
2373            stop: vec![
2374                XlsxGradientFillStop::default(),
2375                XlsxGradientFillStop {
2376                    position: 1.0,
2377                    color: None,
2378                },
2379            ],
2380            ..Default::default()
2381        },
2382        XlsxGradientFill {
2383            r#type: Some("path".to_string()),
2384            bottom: Some(1.0),
2385            left: Some(1.0),
2386            right: Some(1.0),
2387            top: Some(1.0),
2388            stop: vec![
2389                XlsxGradientFillStop::default(),
2390                XlsxGradientFillStop {
2391                    position: 1.0,
2392                    color: None,
2393                },
2394            ],
2395            ..Default::default()
2396        },
2397        XlsxGradientFill {
2398            r#type: Some("path".to_string()),
2399            bottom: Some(0.5),
2400            left: Some(0.5),
2401            right: Some(0.5),
2402            top: Some(0.5),
2403            stop: vec![
2404                XlsxGradientFillStop::default(),
2405                XlsxGradientFillStop {
2406                    position: 1.0,
2407                    color: None,
2408                },
2409            ],
2410            ..Default::default()
2411        },
2412    ]
2413}
2414
2415// ------------------------------------------------------------------
2416// Conditional formatting lookup tables
2417// ------------------------------------------------------------------
2418
2419static VALID_TYPE: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
2420    let mut m = HashMap::new();
2421    m.insert("cell", "cellIs");
2422    m.insert("average", "aboveAverage");
2423    m.insert("duplicate", "duplicateValues");
2424    m.insert("unique", "uniqueValues");
2425    m.insert("top", "top10");
2426    m.insert("bottom", "top10");
2427    m.insert("text", "text");
2428    m.insert("time_period", "timePeriod");
2429    m.insert("blanks", "containsBlanks");
2430    m.insert("no_blanks", "notContainsBlanks");
2431    m.insert("errors", "containsErrors");
2432    m.insert("no_errors", "notContainsErrors");
2433    m.insert("2_color_scale", "2_color_scale");
2434    m.insert("3_color_scale", "3_color_scale");
2435    m.insert("data_bar", "dataBar");
2436    m.insert("formula", "expression");
2437    m.insert("icon_set", "iconSet");
2438    m
2439});
2440
2441static CRITERIA_TYPE: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
2442    let mut m = HashMap::new();
2443    m.insert("!=", "notEqual");
2444    m.insert("<", "lessThan");
2445    m.insert("<=", "lessThanOrEqual");
2446    m.insert("<>", "notEqual");
2447    m.insert("=", "equal");
2448    m.insert("==", "equal");
2449    m.insert(">", "greaterThan");
2450    m.insert(">=", "greaterThanOrEqual");
2451    m.insert("begins with", "beginsWith");
2452    m.insert("between", "between");
2453    m.insert("containing", "containsText");
2454    m.insert("continue month", "nextMonth");
2455    m.insert("continue week", "nextWeek");
2456    m.insert("ends with", "endsWith");
2457    m.insert("equal to", "equal");
2458    m.insert("greater than or equal to", "greaterThanOrEqual");
2459    m.insert("greater than", "greaterThan");
2460    m.insert("last 7 days", "last7Days");
2461    m.insert("last month", "lastMonth");
2462    m.insert("last week", "lastWeek");
2463    m.insert("less than or equal to", "lessThanOrEqual");
2464    m.insert("less than", "lessThan");
2465    m.insert("not between", "notBetween");
2466    m.insert("not containing", "notContains");
2467    m.insert("not equal to", "notEqual");
2468    m.insert("this month", "thisMonth");
2469    m.insert("this week", "thisWeek");
2470    m.insert("today", "today");
2471    m.insert("tomorrow", "tomorrow");
2472    m.insert("yesterday", "yesterday");
2473    m
2474});
2475
2476static OPERATOR_TYPE: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
2477    let mut m = HashMap::new();
2478    m.insert("beginsWith", "begins with");
2479    m.insert("between", "between");
2480    m.insert("containsText", "containing");
2481    m.insert("endsWith", "ends with");
2482    m.insert("equal", "equal to");
2483    m.insert("greaterThan", "greater than");
2484    m.insert("greaterThanOrEqual", "greater than or equal to");
2485    m.insert("last7Days", "last 7 days");
2486    m.insert("lastMonth", "last month");
2487    m.insert("lastWeek", "last week");
2488    m.insert("lessThan", "less than");
2489    m.insert("lessThanOrEqual", "less than or equal to");
2490    m.insert("nextMonth", "continue month");
2491    m.insert("nextWeek", "continue week");
2492    m.insert("notBetween", "not between");
2493    m.insert("notContains", "not containing");
2494    m.insert("notEqual", "not equal to");
2495    m.insert("thisMonth", "this month");
2496    m.insert("thisWeek", "this week");
2497    m.insert("today", "today");
2498    m.insert("tomorrow", "tomorrow");
2499    m.insert("yesterday", "yesterday");
2500    m
2501});
2502
2503const CELL_IS_CRITERIA_TYPE: &[&str] = &[
2504    "equal",
2505    "notEqual",
2506    "greaterThan",
2507    "lessThan",
2508    "greaterThanOrEqual",
2509    "lessThanOrEqual",
2510    "containsText",
2511    "notContains",
2512    "beginsWith",
2513    "endsWith",
2514];
2515
2516static ICON_SET_PRESETS: LazyLock<HashMap<&'static str, XlsxCfRule>> = LazyLock::new(|| {
2517    let cfvo3 = XlsxIconSet {
2518        cfvo: vec![
2519            XlsxCfvo {
2520                r#type: Some("percent".to_string()),
2521                val: Some("0".to_string()),
2522                ..Default::default()
2523            },
2524            XlsxCfvo {
2525                r#type: Some("percent".to_string()),
2526                val: Some("33".to_string()),
2527                ..Default::default()
2528            },
2529            XlsxCfvo {
2530                r#type: Some("percent".to_string()),
2531                val: Some("67".to_string()),
2532                ..Default::default()
2533            },
2534        ],
2535        ..Default::default()
2536    };
2537    let cfvo4 = XlsxIconSet {
2538        cfvo: vec![
2539            XlsxCfvo {
2540                r#type: Some("percent".to_string()),
2541                val: Some("0".to_string()),
2542                ..Default::default()
2543            },
2544            XlsxCfvo {
2545                r#type: Some("percent".to_string()),
2546                val: Some("25".to_string()),
2547                ..Default::default()
2548            },
2549            XlsxCfvo {
2550                r#type: Some("percent".to_string()),
2551                val: Some("50".to_string()),
2552                ..Default::default()
2553            },
2554            XlsxCfvo {
2555                r#type: Some("percent".to_string()),
2556                val: Some("75".to_string()),
2557                ..Default::default()
2558            },
2559        ],
2560        ..Default::default()
2561    };
2562    let cfvo5 = XlsxIconSet {
2563        cfvo: vec![
2564            XlsxCfvo {
2565                r#type: Some("percent".to_string()),
2566                val: Some("0".to_string()),
2567                ..Default::default()
2568            },
2569            XlsxCfvo {
2570                r#type: Some("percent".to_string()),
2571                val: Some("20".to_string()),
2572                ..Default::default()
2573            },
2574            XlsxCfvo {
2575                r#type: Some("percent".to_string()),
2576                val: Some("40".to_string()),
2577                ..Default::default()
2578            },
2579            XlsxCfvo {
2580                r#type: Some("percent".to_string()),
2581                val: Some("60".to_string()),
2582                ..Default::default()
2583            },
2584            XlsxCfvo {
2585                r#type: Some("percent".to_string()),
2586                val: Some("80".to_string()),
2587                ..Default::default()
2588            },
2589        ],
2590        ..Default::default()
2591    };
2592    let mut m = HashMap::new();
2593    for name in [
2594        "3Arrows",
2595        "3ArrowsGray",
2596        "3Flags",
2597        "3Signs",
2598        "3Symbols",
2599        "3Symbols2",
2600        "3TrafficLights1",
2601        "3TrafficLights2",
2602    ] {
2603        m.insert(
2604            name,
2605            XlsxCfRule {
2606                icon_set: Some(cfvo3.clone()),
2607                ..Default::default()
2608            },
2609        );
2610    }
2611    for name in [
2612        "4Arrows",
2613        "4ArrowsGray",
2614        "4Rating",
2615        "4RedToBlack",
2616        "4TrafficLights",
2617    ] {
2618        m.insert(
2619            name,
2620            XlsxCfRule {
2621                icon_set: Some(cfvo4.clone()),
2622                ..Default::default()
2623            },
2624        );
2625    }
2626    for name in ["5Arrows", "5ArrowsGray", "5Quarters", "5Rating"] {
2627        m.insert(
2628            name,
2629            XlsxCfRule {
2630                icon_set: Some(cfvo5.clone()),
2631                ..Default::default()
2632            },
2633        );
2634    }
2635    m
2636});
2637
2638static X14_ICON_SET_PRESETS: LazyLock<HashMap<&'static str, XlsxX14CfRule>> = LazyLock::new(|| {
2639    let cfvo3 = Xlsx14IconSet {
2640        cfvo: vec![
2641            Xlsx14Cfvo {
2642                r#type: Some("percent".to_string()),
2643                f: Some("0".to_string()),
2644                ..Default::default()
2645            },
2646            Xlsx14Cfvo {
2647                r#type: Some("percent".to_string()),
2648                f: Some("33".to_string()),
2649                ..Default::default()
2650            },
2651            Xlsx14Cfvo {
2652                r#type: Some("percent".to_string()),
2653                f: Some("67".to_string()),
2654                ..Default::default()
2655            },
2656        ],
2657        ..Default::default()
2658    };
2659    let cfvo5 = Xlsx14IconSet {
2660        cfvo: vec![
2661            Xlsx14Cfvo {
2662                r#type: Some("percent".to_string()),
2663                f: Some("0".to_string()),
2664                ..Default::default()
2665            },
2666            Xlsx14Cfvo {
2667                r#type: Some("percent".to_string()),
2668                f: Some("20".to_string()),
2669                ..Default::default()
2670            },
2671            Xlsx14Cfvo {
2672                r#type: Some("percent".to_string()),
2673                f: Some("40".to_string()),
2674                ..Default::default()
2675            },
2676            Xlsx14Cfvo {
2677                r#type: Some("percent".to_string()),
2678                f: Some("60".to_string()),
2679                ..Default::default()
2680            },
2681            Xlsx14Cfvo {
2682                r#type: Some("percent".to_string()),
2683                f: Some("80".to_string()),
2684                ..Default::default()
2685            },
2686        ],
2687        ..Default::default()
2688    };
2689    let mut m = HashMap::new();
2690    m.insert(
2691        "3Stars",
2692        XlsxX14CfRule {
2693            icon_set: Some(cfvo3.clone()),
2694            ..Default::default()
2695        },
2696    );
2697    m.insert(
2698        "3Triangles",
2699        XlsxX14CfRule {
2700            icon_set: Some(cfvo3.clone()),
2701            ..Default::default()
2702        },
2703    );
2704    m.insert(
2705        "5Boxes",
2706        XlsxX14CfRule {
2707            icon_set: Some(cfvo5.clone()),
2708            ..Default::default()
2709        },
2710    );
2711    m
2712});
2713
2714static DRAW_COND_FMT_FUNC: LazyLock<
2715    HashMap<
2716        &'static str,
2717        fn(
2718            i32,
2719            &str,
2720            &str,
2721            &str,
2722            &ConditionalFormatOptions,
2723        ) -> (Option<XlsxCfRule>, Option<XlsxX14CfRule>),
2724    >,
2725> = LazyLock::new(|| {
2726    let mut m: HashMap<
2727        &'static str,
2728        fn(
2729            i32,
2730            &str,
2731            &str,
2732            &str,
2733            &ConditionalFormatOptions,
2734        ) -> (Option<XlsxCfRule>, Option<XlsxX14CfRule>),
2735    > = HashMap::new();
2736    m.insert("cellIs", draw_cond_fmt_cell_is);
2737    m.insert("timePeriod", draw_cond_fmt_time_period);
2738    m.insert("text", draw_cond_fmt_text);
2739    m.insert("top10", draw_cond_fmt_top10);
2740    m.insert("aboveAverage", draw_cond_fmt_above_average);
2741    m.insert("duplicateValues", draw_cond_fmt_duplicate_unique);
2742    m.insert("uniqueValues", draw_cond_fmt_duplicate_unique);
2743    m.insert("containsBlanks", draw_cond_fmt_blanks);
2744    m.insert("notContainsBlanks", draw_cond_fmt_no_blanks);
2745    m.insert("containsErrors", draw_cond_fmt_errors);
2746    m.insert("notContainsErrors", draw_cond_fmt_no_errors);
2747    m.insert("2_color_scale", draw_cond_fmt_color_scale);
2748    m.insert("3_color_scale", draw_cond_fmt_color_scale);
2749    m.insert("dataBar", draw_cond_fmt_data_bar);
2750    m.insert("expression", draw_cond_fmt_exp);
2751    m.insert("iconSet", draw_cond_fmt_icon_set);
2752    m
2753});
2754
2755static EXTRACT_COND_FMT_FUNC: LazyLock<
2756    HashMap<
2757        &'static str,
2758        fn(&File, &str, &XlsxCfRule, Option<&XlsxExtLst>) -> ConditionalFormatOptions,
2759    >,
2760> = LazyLock::new(|| {
2761    let mut m: HashMap<
2762        &'static str,
2763        fn(&File, &str, &XlsxCfRule, Option<&XlsxExtLst>) -> ConditionalFormatOptions,
2764    > = HashMap::new();
2765    m.insert("cellIs", extract_cond_fmt_cell_is);
2766    m.insert("timePeriod", extract_cond_fmt_time_period);
2767    m.insert("containsText", extract_cond_fmt_text);
2768    m.insert("notContainsText", extract_cond_fmt_text);
2769    m.insert("beginsWith", extract_cond_fmt_text);
2770    m.insert("endsWith", extract_cond_fmt_text);
2771    m.insert("top10", extract_cond_fmt_top10);
2772    m.insert("aboveAverage", extract_cond_fmt_above_average);
2773    m.insert("duplicateValues", extract_cond_fmt_duplicate_unique);
2774    m.insert("uniqueValues", extract_cond_fmt_duplicate_unique);
2775    m.insert("containsBlanks", extract_cond_fmt_blanks);
2776    m.insert("notContainsBlanks", extract_cond_fmt_no_blanks);
2777    m.insert("containsErrors", extract_cond_fmt_errors);
2778    m.insert("notContainsErrors", extract_cond_fmt_no_errors);
2779    m.insert("colorScale", extract_cond_fmt_color_scale);
2780    m.insert("dataBar", extract_cond_fmt_data_bar);
2781    m.insert("expression", extract_cond_fmt_exp);
2782    m.insert("iconSet", extract_cond_fmt_icon_set);
2783    m
2784});
2785
2786// ------------------------------------------------------------------
2787// Conditional formatting range preparation
2788// ------------------------------------------------------------------
2789
2790fn prepare_conditional_format_range(range_ref: &str) -> Result<(String, String)> {
2791    if range_ref.is_empty() {
2792        return Err(Box::new(ErrParameterRequired));
2793    }
2794    let range_ref = range_ref.replace(',', " ");
2795    let mut sqref_parts = Vec::new();
2796    let mut mast_cell = String::new();
2797    for (i, cell_range) in range_ref.split_whitespace().enumerate() {
2798        let mut cell_names = Vec::new();
2799        for (j, ref_str) in cell_range.split(':').enumerate() {
2800            if j > 1 {
2801                return Err(Box::new(ErrParameterInvalid));
2802            }
2803            let (mut col, mut row) = parse_conditional_ref(ref_str)?;
2804            if col == -1 {
2805                col = if j == 0 { 1 } else { MAX_COLUMNS };
2806            }
2807            if row == -1 {
2808                row = if j == 0 { 1 } else { TOTAL_ROWS };
2809            }
2810            let cell_name = coordinates_to_cell_name(col, row, false)?;
2811            cell_names.push(cell_name.clone());
2812            if i == 0 && j == 0 {
2813                mast_cell = cell_name;
2814            }
2815        }
2816        sqref_parts.push(cell_names.join(":"));
2817    }
2818    Ok((sqref_parts.join(" "), mast_cell))
2819}
2820
2821fn parse_conditional_ref(ref_str: &str) -> Result<(i32, i32)> {
2822    // Strip an optional sheet qualifier such as 'Sheet1'!A1 or Sheet1!A1.
2823    let cell_ref = ref_str.rsplit_once('!').map(|(_, r)| r).unwrap_or(ref_str);
2824    if let Ok((col, row)) = cell_name_to_coordinates(cell_ref) {
2825        return Ok((col, row));
2826    }
2827    if let Ok(col) = column_name_to_number(cell_ref) {
2828        return Ok((col, -1));
2829    }
2830    if let Ok(row) = cell_ref.parse::<i32>() {
2831        if row < 1 || row > TOTAL_ROWS {
2832            return Err(Box::new(ErrParameterInvalid));
2833        }
2834        return Ok((-1, row));
2835    }
2836    Err(Box::new(ErrParameterInvalid))
2837}
2838
2839// ------------------------------------------------------------------
2840// Conditional formatting x14 extension helpers
2841// ------------------------------------------------------------------
2842
2843fn append_cf_rule(ws: &mut XlsxWorksheet, rule: &XlsxX14CfRule, sqref: &str) -> Result<()> {
2844    let cond_fmt = XlsxX14ConditionalFormatting {
2845        xmlns_xm: Some(NAMESPACE_SPREADSHEET_EXCEL_2006_MAIN.to_string()),
2846        sqref: Some(sqref.to_string()),
2847        cf_rule: vec![rule.clone()],
2848        ..Default::default()
2849    };
2850    let mut rule_xml = xml_to_string(&cond_fmt)?;
2851    if let Some(pos) = rule_xml.find("?>") {
2852        rule_xml = rule_xml[pos + 2..].to_string();
2853    }
2854
2855    let ext_lst = ws.ext_lst.get_or_insert_with(XlsxExtLst::default);
2856    let closing = "</x14:conditionalFormattings>";
2857    let mut append_mode = false;
2858    for ext in &mut ext_lst.ext {
2859        if ext.uri.as_deref() == Some(EXT_URI_CONDITIONAL_FORMATTINGS) {
2860            if ext.content.is_empty() {
2861                ext.content =
2862                    format!("<x14:conditionalFormattings>{rule_xml}</x14:conditionalFormattings>");
2863            } else if let Some(pos) = ext.content.rfind(closing) {
2864                ext.content.insert_str(pos, &rule_xml);
2865            } else {
2866                ext.content.push_str(&rule_xml);
2867            }
2868            append_mode = true;
2869            break;
2870        }
2871    }
2872    if !append_mode {
2873        ext_lst.ext.push(XlsxExt {
2874            uri: Some(EXT_URI_CONDITIONAL_FORMATTINGS.to_string()),
2875            xmlns_x14: Some(NAMESPACE_SPREADSHEET_X14.to_string()),
2876            content: format!("<x14:conditionalFormattings>{rule_xml}</x14:conditionalFormattings>"),
2877            ..Default::default()
2878        });
2879    }
2880    sort_ext_lst(ext_lst);
2881    Ok(())
2882}
2883
2884fn sort_ext_lst(ext_lst: &mut XlsxExtLst) {
2885    ext_lst.ext.sort_by(|a, b| {
2886        let ai = in_str_slice(
2887            WORKSHEET_EXT_URI_PRIORITY,
2888            a.uri.as_deref().unwrap_or(""),
2889            false,
2890        );
2891        let bi = in_str_slice(
2892            WORKSHEET_EXT_URI_PRIORITY,
2893            b.uri.as_deref().unwrap_or(""),
2894            false,
2895        );
2896        ai.cmp(&bi)
2897    });
2898}
2899
2900fn parse_x14_conditional_formattings(content: &str) -> Result<DecodeX14ConditionalFormattingRules> {
2901    let stripped = content
2902        .replace("<x14:", "<")
2903        .replace("</x14:", "</")
2904        .replace("<xm:", "<")
2905        .replace("</xm:", "</");
2906    Ok(xml_from_reader(stripped.as_bytes())?)
2907}
2908
2909fn delete_x14_cf_rule(
2910    rules: &DecodeX14ConditionalFormattingRules,
2911    del_cells: &HashMap<i32, Vec<Vec<i32>>>,
2912) -> Result<String> {
2913    let mut inner = String::new();
2914    for cond_fmt in &rules.cond_fmt {
2915        let sqref = cond_fmt.sqref.as_deref().unwrap_or("").to_string();
2916        let new_sqref = delete_cells_from_sqref(&sqref, del_cells)?;
2917        if new_sqref.is_empty() {
2918            continue;
2919        }
2920        for rule in &cond_fmt.cf_rule {
2921            let x14_rule = build_x14_cf_rule(rule);
2922            let x14_cond_fmt = XlsxX14ConditionalFormatting {
2923                xmlns_xm: Some(NAMESPACE_SPREADSHEET_EXCEL_2006_MAIN.to_string()),
2924                pivot: cond_fmt.pivot,
2925                sqref: Some(new_sqref.clone()),
2926                cf_rule: vec![x14_rule],
2927                ext_lst: cond_fmt.ext_lst.clone(),
2928            };
2929            let mut bytes = xml_to_string(&x14_cond_fmt)?;
2930            if let Some(pos) = bytes.find("?>") {
2931                bytes = bytes[pos + 2..].to_string();
2932            }
2933            inner.push_str(&bytes);
2934        }
2935    }
2936    Ok(format!(
2937        "<x14:conditionalFormattings>{inner}</x14:conditionalFormattings>"
2938    ))
2939}
2940
2941fn build_x14_cf_rule(rule: &DecodeX14CfRule) -> XlsxX14CfRule {
2942    XlsxX14CfRule {
2943        r#type: rule.r#type.clone(),
2944        priority: rule.priority,
2945        stop_if_true: rule.stop_if_true,
2946        above_average: rule.above_average,
2947        percent: rule.percent,
2948        bottom: rule.bottom,
2949        operator: rule.operator.clone(),
2950        text: rule.text.clone(),
2951        time_period: rule.time_period.clone(),
2952        rank: rule.rank,
2953        std_dev: rule.std_dev,
2954        equal_average: rule.equal_average,
2955        active_present: rule.active_present,
2956        id: rule.id.clone(),
2957        f: rule.f.clone(),
2958        color_scale: rule.color_scale.clone(),
2959        dxf: rule.dxf.clone(),
2960        ext_lst: rule.ext_lst.clone(),
2961        data_bar: rule.data_bar.as_ref().map(build_x14_data_bar),
2962        icon_set: rule.icon_set.as_ref().map(build_x14_icon_set),
2963    }
2964}
2965
2966fn build_x14_data_bar(db: &DecodeX14DataBar) -> Xlsx14DataBar {
2967    Xlsx14DataBar {
2968        max_length: db.max_length,
2969        min_length: db.min_length,
2970        border: db.border,
2971        gradient: db.gradient,
2972        show_value: db.show_value,
2973        direction: db.direction.clone(),
2974        cfvo: db.cfvo.iter().map(build_x14_cfvo).collect(),
2975        border_color: db.border_color.clone(),
2976        negative_fill_color: db.negative_fill_color.clone(),
2977        axis_color: db.axis_color.clone(),
2978    }
2979}
2980
2981fn build_x14_icon_set(icon_set: &DecodeX14IconSet) -> Xlsx14IconSet {
2982    Xlsx14IconSet {
2983        icon_set: icon_set.icon_set.clone(),
2984        show_value: icon_set.show_value,
2985        percent: icon_set.percent,
2986        reverse: icon_set.reverse,
2987        custom: icon_set.custom,
2988        cfvo: icon_set.cfvo.iter().map(build_x14_cfvo).collect(),
2989        cf_icon: icon_set.cf_icon.clone(),
2990    }
2991}
2992
2993fn build_x14_cfvo(cfvo: &DecodeX14Cfvo) -> Xlsx14Cfvo {
2994    Xlsx14Cfvo {
2995        r#type: cfvo.r#type.clone(),
2996        gte: cfvo.gte,
2997        f: cfvo.f.clone(),
2998        ext_lst: cfvo.ext_lst.clone(),
2999    }
3000}
3001
3002// ------------------------------------------------------------------
3003// Conditional formatting draw helpers
3004// ------------------------------------------------------------------
3005
3006fn draw_cond_fmt_cell_is(
3007    p: i32,
3008    ct: &str,
3009    _ref: &str,
3010    _guid: &str,
3011    format: &ConditionalFormatOptions,
3012) -> (Option<XlsxCfRule>, Option<XlsxX14CfRule>) {
3013    let mut c = XlsxCfRule {
3014        priority: Some(p as i64 + 1),
3015        stop_if_true: Some(format.stop_if_true),
3016        r#type: Some("cellIs".to_string()),
3017        operator: Some(ct.to_string()),
3018        dxf_id: format.format,
3019        ..Default::default()
3020    };
3021    if ct == "between" || ct == "notBetween" {
3022        c.formula.push(format.min_value.clone());
3023        c.formula.push(format.max_value.clone());
3024    }
3025    if in_str_slice(CELL_IS_CRITERIA_TYPE, ct, true) != -1 {
3026        c.formula.push(format.value.clone());
3027    }
3028    (Some(c), None)
3029}
3030
3031fn draw_cond_fmt_time_period(
3032    p: i32,
3033    ct: &str,
3034    ref_str: &str,
3035    _guid: &str,
3036    format: &ConditionalFormatOptions,
3037) -> (Option<XlsxCfRule>, Option<XlsxX14CfRule>) {
3038    let formula = match ct {
3039        "yesterday" => format!("FLOOR({ref_str},1)=TODAY()-1"),
3040        "today" => format!("FLOOR({ref_str},1)=TODAY()"),
3041        "tomorrow" => format!("FLOOR({ref_str},1)=TODAY()+1"),
3042        "last7Days" => format!("AND(TODAY()-FLOOR({ref_str},1)<=6,FLOOR({ref_str},1)<=TODAY())"),
3043        "lastWeek" => format!(
3044            "AND(TODAY()-ROUNDDOWN({ref_str},0)>=(WEEKDAY(TODAY())),TODAY()-ROUNDDOWN({ref_str},0)<(WEEKDAY(TODAY())+7))"
3045        ),
3046        "thisWeek" => format!(
3047            "AND(TODAY()-ROUNDDOWN({ref_str},0)<=WEEKDAY(TODAY())-1,ROUNDDOWN({ref_str},0)-TODAY()>=7-WEEKDAY(TODAY()))"
3048        ),
3049        "nextWeek" => format!(
3050            "AND(ROUNDDOWN({ref_str},0)-TODAY()>(7-WEEKDAY(TODAY())),ROUNDDOWN({ref_str},0)-TODAY()<(15-WEEKDAY(TODAY())))"
3051        ),
3052        "lastMonth" => format!(
3053            "AND(MONTH({ref_str})=MONTH(TODAY())-1,OR(YEAR({ref_str})=YEAR(TODAY()),AND(MONTH({ref_str})=1,YEAR({ref_str})=YEAR(TODAY())-1)))"
3054        ),
3055        "thisMonth" => {
3056            format!("AND(MONTH({ref_str})=MONTH(TODAY()),YEAR({ref_str})=YEAR(TODAY()))")
3057        }
3058        "nextMonth" => format!(
3059            "AND(MONTH({ref_str})=MONTH(TODAY())+1,OR(YEAR({ref_str})=YEAR(TODAY()),AND(MONTH({ref_str})=12,YEAR({ref_str})=YEAR(TODAY())+1)))"
3060        ),
3061        _ => String::new(),
3062    };
3063    let c = XlsxCfRule {
3064        priority: Some(p as i64 + 1),
3065        stop_if_true: Some(format.stop_if_true),
3066        r#type: Some("timePeriod".to_string()),
3067        formula: vec![formula],
3068        dxf_id: format.format,
3069        ..Default::default()
3070    };
3071    (Some(c), None)
3072}
3073
3074fn draw_cond_fmt_text(
3075    p: i32,
3076    ct: &str,
3077    ref_str: &str,
3078    _guid: &str,
3079    format: &ConditionalFormatOptions,
3080) -> (Option<XlsxCfRule>, Option<XlsxX14CfRule>) {
3081    let rule_type = match ct {
3082        "containsText" => "containsText",
3083        "notContains" => "notContainsText",
3084        "beginsWith" => "beginsWith",
3085        "endsWith" => "endsWith",
3086        _ => "",
3087    };
3088    let escaped = format.value.replace('"', "\"\"");
3089    let formula = match ct {
3090        "containsText" => format!("NOT(ISERROR(SEARCH(\"{escaped}\",{ref_str})))"),
3091        "notContains" => format!("ISERROR(SEARCH(\"{escaped}\",{ref_str}))"),
3092        "beginsWith" => format!("LEFT({ref_str},LEN(\"{escaped}\"))=\"{escaped}\""),
3093        "endsWith" => format!("RIGHT({ref_str},LEN(\"{escaped}\"))=\"{escaped}\""),
3094        _ => String::new(),
3095    };
3096    let c = XlsxCfRule {
3097        priority: Some(p as i64 + 1),
3098        stop_if_true: Some(format.stop_if_true),
3099        r#type: Some(rule_type.to_string()),
3100        operator: Some(ct.to_string()),
3101        text: Some(format.value.clone()),
3102        formula: vec![formula],
3103        dxf_id: format.format,
3104        ..Default::default()
3105    };
3106    (Some(c), None)
3107}
3108
3109fn draw_cond_fmt_top10(
3110    p: i32,
3111    _ct: &str,
3112    _ref: &str,
3113    _guid: &str,
3114    format: &ConditionalFormatOptions,
3115) -> (Option<XlsxCfRule>, Option<XlsxX14CfRule>) {
3116    let rank = format.value.parse::<i64>().unwrap_or(10);
3117    let c = XlsxCfRule {
3118        priority: Some(p as i64 + 1),
3119        stop_if_true: Some(format.stop_if_true),
3120        r#type: Some("top10".to_string()),
3121        bottom: Some(format.r#type == "bottom"),
3122        rank: Some(rank),
3123        percent: Some(format.percent),
3124        dxf_id: format.format,
3125        ..Default::default()
3126    };
3127    (Some(c), None)
3128}
3129
3130fn draw_cond_fmt_above_average(
3131    p: i32,
3132    _ct: &str,
3133    _ref: &str,
3134    _guid: &str,
3135    format: &ConditionalFormatOptions,
3136) -> (Option<XlsxCfRule>, Option<XlsxX14CfRule>) {
3137    let c = XlsxCfRule {
3138        priority: Some(p as i64 + 1),
3139        stop_if_true: Some(format.stop_if_true),
3140        r#type: Some("aboveAverage".to_string()),
3141        above_average: Some(format.above_average),
3142        dxf_id: format.format,
3143        ..Default::default()
3144    };
3145    (Some(c), None)
3146}
3147
3148fn draw_cond_fmt_duplicate_unique(
3149    p: i32,
3150    _ct: &str,
3151    _ref: &str,
3152    _guid: &str,
3153    format: &ConditionalFormatOptions,
3154) -> (Option<XlsxCfRule>, Option<XlsxX14CfRule>) {
3155    let c = XlsxCfRule {
3156        priority: Some(p as i64 + 1),
3157        stop_if_true: Some(format.stop_if_true),
3158        r#type: Some(
3159            VALID_TYPE
3160                .get(format.r#type.as_str())
3161                .copied()
3162                .unwrap_or("duplicateValues")
3163                .to_string(),
3164        ),
3165        dxf_id: format.format,
3166        ..Default::default()
3167    };
3168    (Some(c), None)
3169}
3170
3171fn draw_cond_fmt_color_scale(
3172    p: i32,
3173    _ct: &str,
3174    _ref: &str,
3175    _guid: &str,
3176    format: &ConditionalFormatOptions,
3177) -> (Option<XlsxCfRule>, Option<XlsxX14CfRule>) {
3178    let min_value = if format.min_value.is_empty() {
3179        "0"
3180    } else {
3181        &format.min_value
3182    };
3183    let max_value = if format.max_value.is_empty() {
3184        "0"
3185    } else {
3186        &format.max_value
3187    };
3188    let mid_value = if format.mid_value.is_empty() {
3189        "50"
3190    } else {
3191        &format.mid_value
3192    };
3193    let mut cfvo = vec![XlsxCfvo {
3194        r#type: Some(format.min_type.clone()),
3195        val: Some(min_value.to_string()),
3196        ..Default::default()
3197    }];
3198    let mut color = vec![XlsxColor {
3199        rgb: Some(get_palette_color(&format.min_color)),
3200        ..Default::default()
3201    }];
3202    if VALID_TYPE.get(format.r#type.as_str()).copied() == Some("3_color_scale") {
3203        cfvo.push(XlsxCfvo {
3204            r#type: Some(format.mid_type.clone()),
3205            val: Some(mid_value.to_string()),
3206            ..Default::default()
3207        });
3208        color.push(XlsxColor {
3209            rgb: Some(get_palette_color(&format.mid_color)),
3210            ..Default::default()
3211        });
3212    }
3213    cfvo.push(XlsxCfvo {
3214        r#type: Some(format.max_type.clone()),
3215        val: Some(max_value.to_string()),
3216        ..Default::default()
3217    });
3218    color.push(XlsxColor {
3219        rgb: Some(get_palette_color(&format.max_color)),
3220        ..Default::default()
3221    });
3222    let c = XlsxCfRule {
3223        priority: Some(p as i64 + 1),
3224        stop_if_true: Some(format.stop_if_true),
3225        r#type: Some("colorScale".to_string()),
3226        color_scale: Some(XlsxColorScale { cfvo, color }),
3227        ..Default::default()
3228    };
3229    (Some(c), None)
3230}
3231
3232fn draw_cond_fmt_data_bar(
3233    p: i32,
3234    _ct: &str,
3235    _ref: &str,
3236    guid: &str,
3237    format: &ConditionalFormatOptions,
3238) -> (Option<XlsxCfRule>, Option<XlsxX14CfRule>) {
3239    let mut x14_rule: Option<XlsxX14CfRule> = None;
3240    if format.bar_solid
3241        || format.bar_direction == "leftToRight"
3242        || format.bar_direction == "rightToLeft"
3243        || !format.bar_border_color.is_empty()
3244    {
3245        let mut data_bar = Xlsx14DataBar {
3246            max_length: Some(100),
3247            border: Some(!format.bar_border_color.is_empty()),
3248            gradient: Some(!format.bar_solid),
3249            direction: Some(format.bar_direction.clone()).filter(|s| !s.is_empty()),
3250            cfvo: vec![
3251                Xlsx14Cfvo {
3252                    r#type: Some("autoMin".to_string()),
3253                    ..Default::default()
3254                },
3255                Xlsx14Cfvo {
3256                    r#type: Some("autoMax".to_string()),
3257                    ..Default::default()
3258                },
3259            ],
3260            negative_fill_color: Some(XlsxColor {
3261                rgb: Some("FFFF0000".to_string()),
3262                ..Default::default()
3263            }),
3264            axis_color: Some(XlsxColor {
3265                rgb: Some("FFFF0000".to_string()),
3266                ..Default::default()
3267            }),
3268            ..Default::default()
3269        };
3270        if data_bar.border == Some(true) {
3271            data_bar.border_color = Some(XlsxColor {
3272                rgb: Some(get_palette_color(&format.bar_border_color)),
3273                ..Default::default()
3274            });
3275        }
3276        x14_rule = Some(XlsxX14CfRule {
3277            r#type: Some("dataBar".to_string()),
3278            id: Some(guid.to_string()),
3279            data_bar: Some(data_bar),
3280            ..Default::default()
3281        });
3282    }
3283    let ext_lst = x14_rule.as_ref().map(|_| XlsxExtLst {
3284        ext: vec![XlsxExt {
3285            uri: Some(EXT_URI_CONDITIONAL_FORMATTING_RULE_ID.to_string()),
3286            xmlns_x14: Some(NAMESPACE_SPREADSHEET_X14.to_string()),
3287            content: format!("<x14:id>{guid}</x14:id>"),
3288            ..Default::default()
3289        }],
3290    });
3291    let c = XlsxCfRule {
3292        priority: Some(p as i64 + 1),
3293        stop_if_true: Some(format.stop_if_true),
3294        r#type: Some("dataBar".to_string()),
3295        data_bar: Some(XlsxDataBar {
3296            show_value: Some(!format.bar_only),
3297            cfvo: vec![
3298                XlsxCfvo {
3299                    r#type: Some(format.min_type.clone()).filter(|s| !s.is_empty()),
3300                    val: Some(format.min_value.clone()).filter(|s| !s.is_empty()),
3301                    ..Default::default()
3302                },
3303                XlsxCfvo {
3304                    r#type: Some(format.max_type.clone()).filter(|s| !s.is_empty()),
3305                    val: Some(format.max_value.clone()).filter(|s| !s.is_empty()),
3306                    ..Default::default()
3307                },
3308            ],
3309            color: vec![XlsxColor {
3310                rgb: Some(get_palette_color(&format.bar_color)),
3311                ..Default::default()
3312            }],
3313            ..Default::default()
3314        }),
3315        ext_lst,
3316        ..Default::default()
3317    };
3318    (Some(c), x14_rule)
3319}
3320
3321fn draw_cond_fmt_exp(
3322    p: i32,
3323    _ct: &str,
3324    _ref: &str,
3325    _guid: &str,
3326    format: &ConditionalFormatOptions,
3327) -> (Option<XlsxCfRule>, Option<XlsxX14CfRule>) {
3328    let c = XlsxCfRule {
3329        priority: Some(p as i64 + 1),
3330        stop_if_true: Some(format.stop_if_true),
3331        r#type: Some("expression".to_string()),
3332        formula: vec![format.criteria.clone()],
3333        dxf_id: format.format,
3334        ..Default::default()
3335    };
3336    (Some(c), None)
3337}
3338
3339fn draw_cond_fmt_errors(
3340    p: i32,
3341    _ct: &str,
3342    ref_str: &str,
3343    _guid: &str,
3344    format: &ConditionalFormatOptions,
3345) -> (Option<XlsxCfRule>, Option<XlsxX14CfRule>) {
3346    let c = XlsxCfRule {
3347        priority: Some(p as i64 + 1),
3348        stop_if_true: Some(format.stop_if_true),
3349        r#type: Some("containsErrors".to_string()),
3350        formula: vec![format!("ISERROR({ref_str})")],
3351        dxf_id: format.format,
3352        ..Default::default()
3353    };
3354    (Some(c), None)
3355}
3356
3357fn draw_cond_fmt_no_errors(
3358    p: i32,
3359    _ct: &str,
3360    ref_str: &str,
3361    _guid: &str,
3362    format: &ConditionalFormatOptions,
3363) -> (Option<XlsxCfRule>, Option<XlsxX14CfRule>) {
3364    let c = XlsxCfRule {
3365        priority: Some(p as i64 + 1),
3366        stop_if_true: Some(format.stop_if_true),
3367        r#type: Some("notContainsErrors".to_string()),
3368        formula: vec![format!("NOT(ISERROR({ref_str}))")],
3369        dxf_id: format.format,
3370        ..Default::default()
3371    };
3372    (Some(c), None)
3373}
3374
3375fn draw_cond_fmt_blanks(
3376    p: i32,
3377    _ct: &str,
3378    ref_str: &str,
3379    _guid: &str,
3380    format: &ConditionalFormatOptions,
3381) -> (Option<XlsxCfRule>, Option<XlsxX14CfRule>) {
3382    let c = XlsxCfRule {
3383        priority: Some(p as i64 + 1),
3384        stop_if_true: Some(format.stop_if_true),
3385        r#type: Some("containsBlanks".to_string()),
3386        formula: vec![format!("LEN(TRIM({ref_str}))=0")],
3387        dxf_id: format.format,
3388        ..Default::default()
3389    };
3390    (Some(c), None)
3391}
3392
3393fn draw_cond_fmt_no_blanks(
3394    p: i32,
3395    _ct: &str,
3396    ref_str: &str,
3397    _guid: &str,
3398    format: &ConditionalFormatOptions,
3399) -> (Option<XlsxCfRule>, Option<XlsxX14CfRule>) {
3400    let c = XlsxCfRule {
3401        priority: Some(p as i64 + 1),
3402        stop_if_true: Some(format.stop_if_true),
3403        r#type: Some("notContainsBlanks".to_string()),
3404        formula: vec![format!("LEN(TRIM({ref_str}))>0")],
3405        dxf_id: format.format,
3406        ..Default::default()
3407    };
3408    (Some(c), None)
3409}
3410
3411fn draw_cond_fmt_icon_set(
3412    p: i32,
3413    _ct: &str,
3414    _ref: &str,
3415    guid: &str,
3416    format: &ConditionalFormatOptions,
3417) -> (Option<XlsxCfRule>, Option<XlsxX14CfRule>) {
3418    if let Some(preset) = ICON_SET_PRESETS.get(format.icon_style.as_str()) {
3419        let mut c = preset.clone();
3420        c.priority = Some(p as i64 + 1);
3421        c.r#type = Some("iconSet".to_string());
3422        if let Some(ref mut icon_set) = c.icon_set {
3423            icon_set.icon_set = Some(format.icon_style.clone());
3424            icon_set.reverse = Some(format.reverse_icons);
3425            icon_set.show_value = Some(!format.icons_only);
3426        }
3427        return (Some(c), None);
3428    }
3429    if let Some(preset) = X14_ICON_SET_PRESETS.get(format.icon_style.as_str()) {
3430        let mut x14 = preset.clone();
3431        x14.r#type = Some("iconSet".to_string());
3432        x14.priority = Some(p as i64 + 1);
3433        x14.stop_if_true = Some(format.stop_if_true);
3434        x14.above_average = Some(format.above_average);
3435        x14.percent = Some(format.percent);
3436        x14.id = Some(guid.to_string());
3437        if let Some(ref mut icon_set) = x14.icon_set {
3438            icon_set.icon_set = Some(format.icon_style.clone());
3439            icon_set.reverse = Some(format.reverse_icons);
3440            icon_set.show_value = Some(!format.icons_only);
3441        }
3442        return (None, Some(x14));
3443    }
3444    (None, None)
3445}
3446
3447// ------------------------------------------------------------------
3448// Conditional formatting extract helpers
3449// ------------------------------------------------------------------
3450
3451fn extract_cond_fmt_cell_is(
3452    _f: &File,
3453    _ref: &str,
3454    c: &XlsxCfRule,
3455    _ext_lst: Option<&XlsxExtLst>,
3456) -> ConditionalFormatOptions {
3457    let mut format = ConditionalFormatOptions {
3458        format: c.dxf_id,
3459        stop_if_true: c.stop_if_true.unwrap_or(false),
3460        r#type: "cell".to_string(),
3461        criteria: OPERATOR_TYPE
3462            .get(c.operator.as_deref().unwrap_or(""))
3463            .copied()
3464            .unwrap_or("")
3465            .to_string(),
3466        ..Default::default()
3467    };
3468    if c.formula.len() == 2 {
3469        format.min_value = c.formula[0].clone();
3470        format.max_value = c.formula[1].clone();
3471    } else if !c.formula.is_empty() {
3472        format.value = c.formula[0].clone();
3473    }
3474    format
3475}
3476
3477fn time_period_criteria(ref_str: &str, formula: &str) -> Option<&'static str> {
3478    if formula == format!("FLOOR({ref_str},1)=TODAY()-1") {
3479        return Some("yesterday");
3480    }
3481    if formula == format!("FLOOR({ref_str},1)=TODAY()") {
3482        return Some("today");
3483    }
3484    if formula == format!("FLOOR({ref_str},1)=TODAY()+1") {
3485        return Some("tomorrow");
3486    }
3487    if formula == format!("AND(TODAY()-FLOOR({ref_str},1)<=6,FLOOR({ref_str},1)<=TODAY())") {
3488        return Some("last 7 days");
3489    }
3490    if formula
3491        == format!(
3492            "AND(TODAY()-ROUNDDOWN({ref_str},0)>=(WEEKDAY(TODAY())),TODAY()-ROUNDDOWN({ref_str},0)<(WEEKDAY(TODAY())+7))"
3493        )
3494    {
3495        return Some("last week");
3496    }
3497    if formula
3498        == format!(
3499            "AND(TODAY()-ROUNDDOWN({ref_str},0)<=WEEKDAY(TODAY())-1,ROUNDDOWN({ref_str},0)-TODAY()>=7-WEEKDAY(TODAY()))"
3500        )
3501    {
3502        return Some("this week");
3503    }
3504    if formula
3505        == format!(
3506            "AND(ROUNDDOWN({ref_str},0)-TODAY()>(7-WEEKDAY(TODAY())),ROUNDDOWN({ref_str},0)-TODAY()<(15-WEEKDAY(TODAY())))"
3507        )
3508    {
3509        return Some("continue week");
3510    }
3511    if formula
3512        == format!(
3513            "AND(MONTH({ref_str})=MONTH(TODAY())-1,OR(YEAR({ref_str})=YEAR(TODAY()),AND(MONTH({ref_str})=1,YEAR({ref_str})=YEAR(TODAY())-1)))"
3514        )
3515    {
3516        return Some("last month");
3517    }
3518    if formula == format!("AND(MONTH({ref_str})=MONTH(TODAY()),YEAR({ref_str})=YEAR(TODAY()))") {
3519        return Some("this month");
3520    }
3521    if formula
3522        == format!(
3523            "AND(MONTH({ref_str})=MONTH(TODAY())+1,OR(YEAR({ref_str})=YEAR(TODAY()),AND(MONTH({ref_str})=12,YEAR({ref_str})=YEAR(TODAY())+1)))"
3524        )
3525    {
3526        return Some("continue month");
3527    }
3528    None
3529}
3530
3531fn extract_cond_fmt_time_period(
3532    _f: &File,
3533    ref_str: &str,
3534    c: &XlsxCfRule,
3535    _ext_lst: Option<&XlsxExtLst>,
3536) -> ConditionalFormatOptions {
3537    let mut criteria = String::new();
3538    for formula in &c.formula {
3539        if let Some(cr) = time_period_criteria(ref_str, formula) {
3540            criteria = cr.to_string();
3541            break;
3542        }
3543    }
3544    ConditionalFormatOptions {
3545        format: c.dxf_id,
3546        stop_if_true: c.stop_if_true.unwrap_or(false),
3547        r#type: "time_period".to_string(),
3548        criteria,
3549        ..Default::default()
3550    }
3551}
3552
3553fn extract_cond_fmt_text(
3554    _f: &File,
3555    _ref: &str,
3556    c: &XlsxCfRule,
3557    _ext_lst: Option<&XlsxExtLst>,
3558) -> ConditionalFormatOptions {
3559    ConditionalFormatOptions {
3560        format: c.dxf_id,
3561        stop_if_true: c.stop_if_true.unwrap_or(false),
3562        r#type: "text".to_string(),
3563        criteria: OPERATOR_TYPE
3564            .get(c.operator.as_deref().unwrap_or(""))
3565            .copied()
3566            .unwrap_or("")
3567            .to_string(),
3568        value: c.text.clone().unwrap_or_default(),
3569        ..Default::default()
3570    }
3571}
3572
3573fn extract_cond_fmt_top10(
3574    _f: &File,
3575    _ref: &str,
3576    c: &XlsxCfRule,
3577    _ext_lst: Option<&XlsxExtLst>,
3578) -> ConditionalFormatOptions {
3579    let mut format = ConditionalFormatOptions {
3580        format: c.dxf_id,
3581        stop_if_true: c.stop_if_true.unwrap_or(false),
3582        r#type: "top".to_string(),
3583        criteria: "=".to_string(),
3584        percent: c.percent.unwrap_or(false),
3585        value: c.rank.unwrap_or(10).to_string(),
3586        ..Default::default()
3587    };
3588    if c.bottom == Some(true) {
3589        format.r#type = "bottom".to_string();
3590    }
3591    format
3592}
3593
3594fn extract_cond_fmt_above_average(
3595    _f: &File,
3596    _ref: &str,
3597    c: &XlsxCfRule,
3598    _ext_lst: Option<&XlsxExtLst>,
3599) -> ConditionalFormatOptions {
3600    ConditionalFormatOptions {
3601        format: c.dxf_id,
3602        stop_if_true: c.stop_if_true.unwrap_or(false),
3603        r#type: "average".to_string(),
3604        criteria: "=".to_string(),
3605        above_average: c.above_average.unwrap_or(false),
3606        ..Default::default()
3607    }
3608}
3609
3610fn extract_cond_fmt_duplicate_unique(
3611    _f: &File,
3612    _ref: &str,
3613    c: &XlsxCfRule,
3614    _ext_lst: Option<&XlsxExtLst>,
3615) -> ConditionalFormatOptions {
3616    let t = match c.r#type.as_deref() {
3617        Some("duplicateValues") => "duplicate",
3618        Some("uniqueValues") => "unique",
3619        _ => "",
3620    };
3621    ConditionalFormatOptions {
3622        format: c.dxf_id,
3623        stop_if_true: c.stop_if_true.unwrap_or(false),
3624        r#type: t.to_string(),
3625        criteria: "=".to_string(),
3626        ..Default::default()
3627    }
3628}
3629
3630fn extract_cond_fmt_blanks(
3631    _f: &File,
3632    _ref: &str,
3633    c: &XlsxCfRule,
3634    _ext_lst: Option<&XlsxExtLst>,
3635) -> ConditionalFormatOptions {
3636    ConditionalFormatOptions {
3637        format: c.dxf_id,
3638        stop_if_true: c.stop_if_true.unwrap_or(false),
3639        r#type: "blanks".to_string(),
3640        ..Default::default()
3641    }
3642}
3643
3644fn extract_cond_fmt_no_blanks(
3645    _f: &File,
3646    _ref: &str,
3647    c: &XlsxCfRule,
3648    _ext_lst: Option<&XlsxExtLst>,
3649) -> ConditionalFormatOptions {
3650    ConditionalFormatOptions {
3651        format: c.dxf_id,
3652        stop_if_true: c.stop_if_true.unwrap_or(false),
3653        r#type: "no_blanks".to_string(),
3654        ..Default::default()
3655    }
3656}
3657
3658fn extract_cond_fmt_errors(
3659    _f: &File,
3660    _ref: &str,
3661    c: &XlsxCfRule,
3662    _ext_lst: Option<&XlsxExtLst>,
3663) -> ConditionalFormatOptions {
3664    ConditionalFormatOptions {
3665        format: c.dxf_id,
3666        stop_if_true: c.stop_if_true.unwrap_or(false),
3667        r#type: "errors".to_string(),
3668        ..Default::default()
3669    }
3670}
3671
3672fn extract_cond_fmt_no_errors(
3673    _f: &File,
3674    _ref: &str,
3675    c: &XlsxCfRule,
3676    _ext_lst: Option<&XlsxExtLst>,
3677) -> ConditionalFormatOptions {
3678    ConditionalFormatOptions {
3679        format: c.dxf_id,
3680        stop_if_true: c.stop_if_true.unwrap_or(false),
3681        r#type: "no_errors".to_string(),
3682        ..Default::default()
3683    }
3684}
3685
3686fn extract_cond_fmt_color_scale(
3687    f: &File,
3688    _ref: &str,
3689    c: &XlsxCfRule,
3690    _ext_lst: Option<&XlsxExtLst>,
3691) -> ConditionalFormatOptions {
3692    let mut format = ConditionalFormatOptions {
3693        stop_if_true: c.stop_if_true.unwrap_or(false),
3694        r#type: "2_color_scale".to_string(),
3695        criteria: "=".to_string(),
3696        ..Default::default()
3697    };
3698    if let Some(ref cs) = c.color_scale {
3699        let values = cs.cfvo.len();
3700        let colors = cs.color.len();
3701        if colors > 1 && values > 1 {
3702            format.min_type = cs.cfvo[0].r#type.clone().unwrap_or_default();
3703            if cs.cfvo[0].val.as_deref() != Some("0") {
3704                format.min_value = cs.cfvo[0].val.clone().unwrap_or_default();
3705            }
3706            format.min_color = theme_color_to_hex(f, Some(&cs.color[0]));
3707            format.max_type = cs.cfvo[1].r#type.clone().unwrap_or_default();
3708            if cs.cfvo[1].val.as_deref() != Some("0") {
3709                format.max_value = cs.cfvo[1].val.clone().unwrap_or_default();
3710            }
3711            format.max_color = theme_color_to_hex(f, Some(&cs.color[1]));
3712        }
3713        if colors == 3 {
3714            format.r#type = "3_color_scale".to_string();
3715            format.mid_type = cs.cfvo[1].r#type.clone().unwrap_or_default();
3716            if cs.cfvo[1].val.as_deref() != Some("0") {
3717                format.mid_value = cs.cfvo[1].val.clone().unwrap_or_default();
3718            }
3719            format.mid_color = theme_color_to_hex(f, Some(&cs.color[1]));
3720            format.max_type = cs.cfvo[2].r#type.clone().unwrap_or_default();
3721            if cs.cfvo[2].val.as_deref() != Some("0") {
3722                format.max_value = cs.cfvo[2].val.clone().unwrap_or_default();
3723            }
3724            format.max_color = theme_color_to_hex(f, Some(&cs.color[2]));
3725        }
3726    }
3727    format
3728}
3729
3730fn theme_color_to_hex(f: &File, color: Option<&XlsxColor>) -> String {
3731    let Some(color) = color else {
3732        return String::new();
3733    };
3734    let rgb = f.get_base_color(
3735        color.rgb.as_deref().unwrap_or(""),
3736        color.indexed.unwrap_or(0),
3737        color.theme,
3738    );
3739    if rgb.is_empty() {
3740        return String::new();
3741    }
3742    let tint = color.tint.unwrap_or(0.0);
3743    let hex = if tint != 0.0 {
3744        if let Some(s) = theme_color(&rgb, tint).strip_prefix("FF") {
3745            s.to_string()
3746        } else {
3747            rgb
3748        }
3749    } else {
3750        rgb
3751    };
3752    format!("#{}", hex)
3753}
3754
3755fn extract_cond_fmt_data_bar(
3756    f: &File,
3757    _ref: &str,
3758    c: &XlsxCfRule,
3759    ext_lst: Option<&XlsxExtLst>,
3760) -> ConditionalFormatOptions {
3761    let mut format = ConditionalFormatOptions {
3762        r#type: "data_bar".to_string(),
3763        criteria: "=".to_string(),
3764        stop_if_true: c.stop_if_true.unwrap_or(false),
3765        ..Default::default()
3766    };
3767    if let Some(ref db) = c.data_bar {
3768        if !db.cfvo.is_empty() {
3769            format.min_type = db.cfvo[0].r#type.clone().unwrap_or_default();
3770            format.min_value = db.cfvo[0].val.clone().unwrap_or_default();
3771        }
3772        if db.cfvo.len() > 1 {
3773            format.max_type = db.cfvo[1].r#type.clone().unwrap_or_default();
3774            format.max_value = db.cfvo[1].val.clone().unwrap_or_default();
3775        }
3776        if let Some(ref color) = db.color.first() {
3777            format.bar_color = theme_color_to_hex(f, Some(color));
3778        }
3779        if let Some(show) = db.show_value {
3780            format.bar_only = !show;
3781        }
3782    }
3783    if let Some(ref ext) = c.ext_lst {
3784        if let Some(id_ext) = ext.ext.first() {
3785            let id = x14_rule_id_from_ext(&id_ext.content);
3786            if let Some(ext_lst) = ext_lst {
3787                extract_cond_fmt_data_bar_rule(f, &id, &mut format, ext_lst);
3788            }
3789        }
3790    }
3791    format
3792}
3793
3794fn x14_rule_id_from_ext(content: &str) -> String {
3795    content
3796        .trim_start_matches("<x14:id>")
3797        .trim_end_matches("</x14:id>")
3798        .to_string()
3799}
3800
3801fn extract_cond_fmt_data_bar_rule(
3802    f: &File,
3803    id: &str,
3804    format: &mut ConditionalFormatOptions,
3805    ext_lst: &XlsxExtLst,
3806) {
3807    for ext in &ext_lst.ext {
3808        if ext.uri.as_deref() == Some(EXT_URI_CONDITIONAL_FORMATTINGS) {
3809            if let Ok(decoded) = parse_x14_conditional_formattings(&ext.content) {
3810                for cond_fmt in &decoded.cond_fmt {
3811                    for rule in &cond_fmt.cf_rule {
3812                        if rule.data_bar.is_some() && rule.id.as_deref() == Some(id) {
3813                            if let Some(ref db) = rule.data_bar {
3814                                format.bar_direction = db.direction.clone().unwrap_or_default();
3815                                if let Some(gradient) = db.gradient {
3816                                    if !gradient {
3817                                        format.bar_solid = true;
3818                                    }
3819                                }
3820                                if let Some(ref bc) = db.border_color {
3821                                    format.bar_border_color = theme_color_to_hex(f, Some(bc));
3822                                }
3823                            }
3824                        }
3825                    }
3826                }
3827            }
3828        }
3829    }
3830}
3831
3832fn extract_cond_fmt_exp(
3833    _f: &File,
3834    _ref: &str,
3835    c: &XlsxCfRule,
3836    _ext_lst: Option<&XlsxExtLst>,
3837) -> ConditionalFormatOptions {
3838    let mut format = ConditionalFormatOptions {
3839        format: c.dxf_id,
3840        stop_if_true: c.stop_if_true.unwrap_or(false),
3841        r#type: "formula".to_string(),
3842        ..Default::default()
3843    };
3844    if !c.formula.is_empty() {
3845        format.criteria = c.formula[0].clone();
3846    }
3847    format
3848}
3849
3850fn extract_cond_fmt_icon_set(
3851    _f: &File,
3852    _ref: &str,
3853    c: &XlsxCfRule,
3854    _ext_lst: Option<&XlsxExtLst>,
3855) -> ConditionalFormatOptions {
3856    let mut format = ConditionalFormatOptions {
3857        r#type: "icon_set".to_string(),
3858        ..Default::default()
3859    };
3860    if let Some(ref icon_set) = c.icon_set {
3861        if let Some(show) = icon_set.show_value {
3862            format.icons_only = !show;
3863        }
3864        format.icon_style = icon_set.icon_set.clone().unwrap_or_default();
3865        format.reverse_icons = icon_set.reverse.unwrap_or(false);
3866    }
3867    format
3868}
3869
3870fn extract_x14_cond_fmt_icon_set(c: &DecodeX14CfRule) -> ConditionalFormatOptions {
3871    let mut format = ConditionalFormatOptions {
3872        r#type: "icon_set".to_string(),
3873        ..Default::default()
3874    };
3875    if let Some(ref icon_set) = c.icon_set {
3876        if let Some(show) = icon_set.show_value {
3877            format.icons_only = !show;
3878        }
3879        format.icon_style = icon_set.icon_set.clone().unwrap_or_default();
3880        format.reverse_icons = icon_set.reverse.unwrap_or(false);
3881    }
3882    format
3883}
3884
3885// ------------------------------------------------------------------
3886// Tests
3887// ------------------------------------------------------------------
3888
3889#[cfg(test)]
3890mod tests {
3891    use super::*;
3892    use crate::options::Options;
3893
3894    #[test]
3895    fn create_and_read_style() {
3896        let f = File::new_with_options(Options::default());
3897
3898        let style = Style {
3899            font: Some(Font {
3900                name: Some("Arial".to_string()),
3901                size: Some(12.0),
3902                bold: Some(true),
3903                color: Some("FF0000".to_string()),
3904                ..Default::default()
3905            }),
3906            fill: Fill {
3907                r#type: "pattern".to_string(),
3908                pattern: 1,
3909                color: vec!["FFFF00".to_string()],
3910                ..Default::default()
3911            },
3912            alignment: Some(Alignment {
3913                horizontal: "center".to_string(),
3914                vertical: "middle".to_string(),
3915                ..Default::default()
3916            }),
3917            ..Default::default()
3918        };
3919
3920        let id = f.new_style(&style).unwrap();
3921        assert!(id > 0);
3922
3923        let read = f.get_style(id).unwrap();
3924        assert_eq!(read.font.as_ref().unwrap().name, Some("Arial".to_string()));
3925        assert_eq!(read.font.as_ref().unwrap().size, Some(12.0));
3926        assert_eq!(read.font.as_ref().unwrap().bold, Some(true));
3927        assert_eq!(
3928            read.font.as_ref().unwrap().color,
3929            Some("FF0000".to_string())
3930        );
3931        assert_eq!(read.fill.r#type, "pattern");
3932        assert_eq!(read.fill.pattern, 1);
3933        assert_eq!(read.fill.color, vec!["FFFF00".to_string()]);
3934        assert_eq!(read.alignment.as_ref().unwrap().horizontal, "center");
3935        assert_eq!(read.alignment.as_ref().unwrap().vertical, "middle");
3936    }
3937
3938    #[test]
3939    fn font_color_rgb_strip_alpha() {
3940        // 8-char RGBA with opaque alpha: strip the leading FF.
3941        let fnt = XlsxFont {
3942            color: Some(XlsxColor {
3943                rgb: Some("FFFF0000".to_string()),
3944                ..Default::default()
3945            }),
3946            ..Default::default()
3947        };
3948        assert_eq!(extract_font(&fnt).color, Some("FF0000".to_string()));
3949
3950        // 6-char RGB that happens to start with FF is also trimmed, matching Go.
3951        let fnt6 = XlsxFont {
3952            color: Some(XlsxColor {
3953                rgb: Some("FF0000".to_string()),
3954                ..Default::default()
3955            }),
3956            ..Default::default()
3957        };
3958        assert_eq!(extract_font(&fnt6).color, Some("0000".to_string()));
3959
3960        // 6-char RGB not starting with FF is preserved.
3961        let fnt_green = XlsxFont {
3962            color: Some(XlsxColor {
3963                rgb: Some("00FF00".to_string()),
3964                ..Default::default()
3965            }),
3966            ..Default::default()
3967        };
3968        assert_eq!(extract_font(&fnt_green).color, Some("00FF00".to_string()));
3969    }
3970
3971    #[test]
3972    fn font_color_indexed_only_when_set() {
3973        let font = Font {
3974            color: Some("FF0000".to_string()),
3975            ..Default::default()
3976        };
3977        let color = new_font_color(&font).unwrap();
3978        assert!(color.indexed.is_none());
3979
3980        let font_indexed = Font {
3981            color: Some("FF0000".to_string()),
3982            color_indexed: Some(0),
3983            ..Default::default()
3984        };
3985        let color_indexed = new_font_color(&font_indexed).unwrap();
3986        assert_eq!(color_indexed.indexed, Some(0));
3987    }
3988
3989    #[test]
3990    fn default_font_round_trip() {
3991        let f = File::new_with_options(Options::default());
3992        assert_eq!(f.get_default_font().unwrap(), "Calibri");
3993        f.set_default_font("Arial").unwrap();
3994        assert_eq!(f.get_default_font().unwrap(), "Arial");
3995    }
3996
3997    #[test]
3998    fn set_and_get_conditional_format_data_bar() {
3999        let f = File::new_with_options(Options::default());
4000        f.set_conditional_format(
4001            "Sheet1",
4002            "A1:A10",
4003            &[ConditionalFormatOptions {
4004                r#type: "data_bar".to_string(),
4005                criteria: "=".to_string(),
4006                min_type: "min".to_string(),
4007                max_type: "max".to_string(),
4008                bar_color: "#638EC6".to_string(),
4009                bar_solid: true,
4010                bar_direction: "leftToRight".to_string(),
4011                bar_border_color: "#000000".to_string(),
4012                ..Default::default()
4013            }],
4014        )
4015        .unwrap();
4016
4017        let cfs = f.get_conditional_formats("Sheet1").unwrap();
4018        let rules = cfs.get("A1:A10").unwrap();
4019        eprintln!("rules[0] = {:?}", rules[0]);
4020        assert_eq!(rules[0].r#type, "data_bar");
4021        assert_eq!(rules[0].bar_color, "#638EC6");
4022        assert!(rules[0].bar_solid, "bar_solid should be true");
4023        assert_eq!(rules[0].bar_direction, "leftToRight");
4024        assert_eq!(rules[0].bar_border_color, "#000000");
4025    }
4026
4027    #[test]
4028    fn conditional_format_round_trip() {
4029        let tmp = std::env::temp_dir().join("excelize_rust_conditional_format_round_trip.xlsx");
4030        let _ = std::fs::remove_file(&tmp);
4031
4032        let mut f = File::new_with_options(Options::default());
4033        f.set_conditional_format(
4034            "Sheet1",
4035            "A1:A10",
4036            &[ConditionalFormatOptions {
4037                r#type: "duplicate".to_string(),
4038                criteria: "=".to_string(),
4039                ..Default::default()
4040            }],
4041        )
4042        .unwrap();
4043
4044        f.save_as(tmp.to_str().unwrap()).unwrap();
4045        let f2 = File::open_file(tmp.to_str().unwrap(), Options::default()).unwrap();
4046        let cfs = f2.get_conditional_formats("Sheet1").unwrap();
4047        assert_eq!(cfs.len(), 1);
4048        let rules = cfs.get("A1:A10").unwrap();
4049        assert_eq!(rules[0].r#type, "duplicate");
4050        assert_eq!(rules[0].criteria, "=");
4051        let _ = std::fs::remove_file(&tmp);
4052    }
4053
4054    #[test]
4055    fn conditional_style_round_trip() {
4056        let f = File::new_with_options(Options::default());
4057        let style = Style {
4058            font: Some(Font {
4059                color: Some("9A0511".to_string()),
4060                ..Default::default()
4061            }),
4062            fill: Fill {
4063                r#type: "pattern".to_string(),
4064                pattern: 1,
4065                color: vec!["FEC7CE".to_string()],
4066                ..Default::default()
4067            },
4068            ..Default::default()
4069        };
4070        let id = f.new_conditional_style(&style).unwrap();
4071        let read = f.get_conditional_style(id).unwrap();
4072        assert_eq!(read.fill.r#type, "pattern");
4073        assert_eq!(read.fill.pattern, 1);
4074        assert_eq!(read.fill.color, vec!["FEC7CE".to_string()]);
4075        assert_eq!(
4076            read.font.as_ref().unwrap().color,
4077            Some("9A0511".to_string())
4078        );
4079    }
4080
4081    #[test]
4082    fn set_and_get_conditional_format_cell() {
4083        let f = File::new_with_options(Options::default());
4084        let style = Style {
4085            font: Some(Font {
4086                color: Some("9A0511".to_string()),
4087                ..Default::default()
4088            }),
4089            fill: Fill {
4090                r#type: "pattern".to_string(),
4091                pattern: 1,
4092                color: vec!["FEC7CE".to_string()],
4093                ..Default::default()
4094            },
4095            ..Default::default()
4096        };
4097        let format = f.new_conditional_style(&style).unwrap();
4098        f.set_conditional_format(
4099            "Sheet1",
4100            "A1:A10",
4101            &[ConditionalFormatOptions {
4102                r#type: "cell".to_string(),
4103                criteria: ">".to_string(),
4104                value: "5".to_string(),
4105                format: Some(format as i64),
4106                ..Default::default()
4107            }],
4108        )
4109        .unwrap();
4110
4111        let cfs = f.get_conditional_formats("Sheet1").unwrap();
4112        assert!(cfs.contains_key("A1:A10"));
4113        let rules = cfs.get("A1:A10").unwrap();
4114        assert_eq!(rules.len(), 1);
4115        assert_eq!(rules[0].r#type, "cell");
4116        assert_eq!(rules[0].criteria, "greater than");
4117        assert_eq!(rules[0].value, "5");
4118        assert_eq!(rules[0].format, Some(format as i64));
4119    }
4120
4121    #[test]
4122    fn set_and_get_conditional_format_color_scale() {
4123        let f = File::new_with_options(Options::default());
4124        f.set_conditional_format(
4125            "Sheet1",
4126            "A1:A10",
4127            &[ConditionalFormatOptions {
4128                r#type: "2_color_scale".to_string(),
4129                criteria: "=".to_string(),
4130                min_type: "min".to_string(),
4131                max_type: "max".to_string(),
4132                min_color: "#F8696B".to_string(),
4133                max_color: "#63BE7B".to_string(),
4134                ..Default::default()
4135            }],
4136        )
4137        .unwrap();
4138
4139        let cfs = f.get_conditional_formats("Sheet1").unwrap();
4140        let rules = cfs.get("A1:A10").unwrap();
4141        assert_eq!(rules[0].r#type, "2_color_scale");
4142        assert_eq!(rules[0].min_type, "min");
4143        assert_eq!(rules[0].max_type, "max");
4144        assert_eq!(rules[0].min_color, "#F8696B");
4145        assert_eq!(rules[0].max_color, "#63BE7B");
4146    }
4147
4148    #[test]
4149    fn unset_conditional_format() {
4150        let f = File::new_with_options(Options::default());
4151        f.set_conditional_format(
4152            "Sheet1",
4153            "A1:A10",
4154            &[ConditionalFormatOptions {
4155                r#type: "duplicate".to_string(),
4156                criteria: "=".to_string(),
4157                ..Default::default()
4158            }],
4159        )
4160        .unwrap();
4161        let cfs = f.get_conditional_formats("Sheet1").unwrap();
4162        assert_eq!(cfs.len(), 1);
4163
4164        f.unset_conditional_format("Sheet1", "A1:A5").unwrap();
4165        let cfs = f.get_conditional_formats("Sheet1").unwrap();
4166        assert_eq!(cfs.len(), 1);
4167
4168        f.unset_conditional_format("Sheet1", "A6:A10").unwrap();
4169        let cfs = f.get_conditional_formats("Sheet1").unwrap();
4170        assert!(cfs.is_empty());
4171    }
4172
4173    #[test]
4174    fn get_base_color_basic() {
4175        let f = File::new_with_options(Options::default());
4176        assert_eq!(f.get_base_color("FF0000", 0, None), "FF0000");
4177        assert_eq!(f.get_base_color("FFFF0000", 0, None), "FF0000");
4178        assert_eq!(f.get_base_color("", 3, None), INDEXED_COLOR_MAPPING[3]);
4179        assert_eq!(f.get_base_color("unknown", 99, None), "unknown");
4180    }
4181}