Skip to main content

spreadsheet_mcp/
styles.rs

1use crate::model::{
2    AlignmentDescriptor, AlignmentPatch, BorderSideDescriptor, BorderSidePatch, BordersDescriptor,
3    BordersPatch, FillDescriptor, FillPatch, FontDescriptor, FontPatch, GradientFillDescriptor,
4    GradientFillPatch, GradientStopDescriptor, PatternFillDescriptor, PatternFillPatch,
5    StyleDescriptor, StylePatch,
6};
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9use sha2::{Digest, Sha256};
10use std::collections::BTreeMap;
11use std::str::FromStr;
12use umya_spreadsheet::structs::{EnumTrait, HorizontalAlignmentValues, VerticalAlignmentValues};
13use umya_spreadsheet::{Border, Fill, Font, PatternValues, Style};
14
15pub fn normalize_color_hex(input: &str) -> Option<(String, bool)> {
16    let trimmed = input.trim();
17    if trimmed.is_empty() {
18        return None;
19    }
20    let hex = trimmed.strip_prefix('#').unwrap_or(trimmed);
21    if hex.is_empty() {
22        return None;
23    }
24    if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
25        return None;
26    }
27
28    match hex.len() {
29        3 => {
30            let mut expanded = String::with_capacity(6);
31            for ch in hex.chars() {
32                expanded.push(ch);
33                expanded.push(ch);
34            }
35            Some((format!("FF{}", expanded.to_ascii_uppercase()), true))
36        }
37        6 => Some((format!("FF{}", hex.to_ascii_uppercase()), true)),
38        8 => Some((hex.to_ascii_uppercase(), false)),
39        _ => None,
40    }
41}
42
43pub fn descriptor_from_style(style: &Style) -> StyleDescriptor {
44    let font = style.get_font().and_then(descriptor_from_font);
45    let fill = style.get_fill().and_then(descriptor_from_fill);
46    let borders = style.get_borders().and_then(|borders| {
47        let left = descriptor_from_border_side(borders.get_left_border());
48        let right = descriptor_from_border_side(borders.get_right_border());
49        let top = descriptor_from_border_side(borders.get_top_border());
50        let bottom = descriptor_from_border_side(borders.get_bottom_border());
51        let diagonal = descriptor_from_border_side(borders.get_diagonal_border());
52        let vertical = descriptor_from_border_side(borders.get_vertical_border());
53        let horizontal = descriptor_from_border_side(borders.get_horizontal_border());
54
55        let diagonal_up = if *borders.get_diagonal_up() {
56            Some(true)
57        } else {
58            None
59        };
60        let diagonal_down = if *borders.get_diagonal_down() {
61            Some(true)
62        } else {
63            None
64        };
65
66        let descriptor = BordersDescriptor {
67            left,
68            right,
69            top,
70            bottom,
71            diagonal,
72            vertical,
73            horizontal,
74            diagonal_up,
75            diagonal_down,
76        };
77
78        if descriptor.is_empty() {
79            None
80        } else {
81            Some(descriptor)
82        }
83    });
84    let alignment = style.get_alignment().and_then(descriptor_from_alignment);
85    let number_format = style.get_number_format().and_then(|fmt| {
86        let code = fmt.get_format_code();
87        if code.eq_ignore_ascii_case("general") {
88            None
89        } else {
90            Some(code.to_string())
91        }
92    });
93
94    StyleDescriptor {
95        font,
96        fill,
97        borders,
98        alignment,
99        number_format,
100    }
101}
102
103pub fn stable_style_id(descriptor: &StyleDescriptor) -> String {
104    let bytes = serde_json::to_vec(descriptor).unwrap_or_default();
105    let mut hasher = Sha256::new();
106    hasher.update(bytes);
107    let digest = hasher.finalize();
108    let hex = format!("{digest:x}");
109    hex.chars().take(12).collect()
110}
111
112pub fn compress_positions_to_ranges(positions: &[(u32, u32)], limit: usize) -> (Vec<String>, bool) {
113    if positions.is_empty() {
114        return (Vec::new(), false);
115    }
116
117    let mut rows: BTreeMap<u32, Vec<u32>> = BTreeMap::new();
118    for &(row, col) in positions {
119        rows.entry(row).or_default().push(col);
120    }
121    for cols in rows.values_mut() {
122        cols.sort_unstable();
123        cols.dedup();
124    }
125
126    let mut spans_by_cols: BTreeMap<(u32, u32), Vec<u32>> = BTreeMap::new();
127    for (row, cols) in rows {
128        if cols.is_empty() {
129            continue;
130        }
131        let mut start = cols[0];
132        let mut prev = cols[0];
133        for col in cols.into_iter().skip(1) {
134            if col == prev + 1 {
135                prev = col;
136            } else {
137                spans_by_cols.entry((start, prev)).or_default().push(row);
138                start = col;
139                prev = col;
140            }
141        }
142        spans_by_cols.entry((start, prev)).or_default().push(row);
143    }
144
145    let mut ranges = Vec::new();
146    let mut truncated = false;
147
148    'outer: for ((start_col, end_col), mut span_rows) in spans_by_cols {
149        span_rows.sort_unstable();
150        span_rows.dedup();
151        if span_rows.is_empty() {
152            continue;
153        }
154        let mut run_start = span_rows[0];
155        let mut prev_row = span_rows[0];
156        for row in span_rows.into_iter().skip(1) {
157            if row == prev_row + 1 {
158                prev_row = row;
159                continue;
160            }
161            ranges.push(format_range(start_col, end_col, run_start, prev_row));
162            if ranges.len() >= limit {
163                truncated = true;
164                break 'outer;
165            }
166            run_start = row;
167            prev_row = row;
168        }
169        ranges.push(format_range(start_col, end_col, run_start, prev_row));
170        if ranges.len() >= limit {
171            truncated = true;
172            break;
173        }
174    }
175
176    if truncated {
177        ranges.truncate(limit);
178    }
179    (ranges, truncated)
180}
181
182fn format_range(start_col: u32, end_col: u32, start_row: u32, end_row: u32) -> String {
183    let start_addr = crate::utils::cell_address(start_col, start_row);
184    let end_addr = crate::utils::cell_address(end_col, end_row);
185    if start_addr == end_addr {
186        start_addr
187    } else {
188        format!("{start_addr}:{end_addr}")
189    }
190}
191
192fn descriptor_from_font(font: &Font) -> Option<FontDescriptor> {
193    let bold = *font.get_bold();
194    let italic = *font.get_italic();
195    let underline = font.get_underline();
196    let strikethrough = *font.get_strikethrough();
197    let color = font.get_color().get_argb();
198
199    let descriptor = FontDescriptor {
200        name: Some(font.get_name().to_string()).filter(|s| !s.is_empty()),
201        size: Some(*font.get_size()).filter(|s| *s > 0.0),
202        bold: if bold { Some(true) } else { None },
203        italic: if italic { Some(true) } else { None },
204        underline: if underline.eq_ignore_ascii_case("none") {
205            None
206        } else {
207            Some(underline.to_string())
208        },
209        strikethrough: if strikethrough { Some(true) } else { None },
210        color: Some(color.to_string()).filter(|s| !s.is_empty()),
211    };
212
213    if descriptor.is_empty() {
214        None
215    } else {
216        Some(descriptor)
217    }
218}
219
220fn descriptor_from_fill(fill: &Fill) -> Option<FillDescriptor> {
221    if let Some(pattern) = fill.get_pattern_fill() {
222        let pattern_type = pattern.get_pattern_type();
223        let kind = pattern_type.get_value_string();
224        let fg = pattern
225            .get_foreground_color()
226            .map(|c| c.get_argb().to_string())
227            .filter(|s| !s.is_empty());
228        let bg = pattern
229            .get_background_color()
230            .map(|c| c.get_argb().to_string())
231            .filter(|s| !s.is_empty());
232
233        if kind.eq_ignore_ascii_case("none") && fg.is_none() && bg.is_none() {
234            return None;
235        }
236
237        return Some(FillDescriptor::Pattern(PatternFillDescriptor {
238            pattern_type: if kind.eq_ignore_ascii_case("none") {
239                None
240            } else {
241                Some(kind.to_string())
242            },
243            foreground_color: fg,
244            background_color: bg,
245        }));
246    }
247
248    if let Some(gradient) = fill.get_gradient_fill() {
249        let stops: Vec<GradientStopDescriptor> = gradient
250            .get_gradient_stop()
251            .iter()
252            .map(|stop| GradientStopDescriptor {
253                position: *stop.get_position(),
254                color: stop.get_color().get_argb().to_string(),
255            })
256            .collect();
257
258        let degree = *gradient.get_degree();
259        if stops.is_empty() && degree == 0.0 {
260            return None;
261        }
262
263        return Some(FillDescriptor::Gradient(GradientFillDescriptor {
264            degree: Some(degree).filter(|d| *d != 0.0),
265            stops,
266        }));
267    }
268
269    None
270}
271
272fn descriptor_from_border_side(border: &Border) -> Option<BorderSideDescriptor> {
273    let style = border.get_border_style();
274    let style = if style.eq_ignore_ascii_case("none") {
275        None
276    } else {
277        Some(style.to_string())
278    };
279    let color = Some(border.get_color().get_argb().to_string()).filter(|s| !s.is_empty());
280
281    let descriptor = BorderSideDescriptor { style, color };
282    if descriptor.is_empty() {
283        None
284    } else {
285        Some(descriptor)
286    }
287}
288
289fn descriptor_from_alignment(
290    alignment: &umya_spreadsheet::Alignment,
291) -> Option<AlignmentDescriptor> {
292    let horizontal = if alignment.get_horizontal() != &HorizontalAlignmentValues::General {
293        Some(alignment.get_horizontal().get_value_string().to_string())
294    } else {
295        None
296    };
297    let vertical = if alignment.get_vertical() != &VerticalAlignmentValues::Bottom {
298        Some(alignment.get_vertical().get_value_string().to_string())
299    } else {
300        None
301    };
302    let wrap_text = if *alignment.get_wrap_text() {
303        Some(true)
304    } else {
305        None
306    };
307    let text_rotation = if *alignment.get_text_rotation() != 0 {
308        Some(*alignment.get_text_rotation())
309    } else {
310        None
311    };
312
313    let descriptor = AlignmentDescriptor {
314        horizontal,
315        vertical,
316        wrap_text,
317        text_rotation,
318    };
319    if descriptor.is_empty() {
320        None
321    } else {
322        Some(descriptor)
323    }
324}
325
326trait IsEmpty {
327    fn is_empty(&self) -> bool;
328}
329
330impl IsEmpty for FontDescriptor {
331    fn is_empty(&self) -> bool {
332        self.name.is_none()
333            && self.size.is_none()
334            && self.bold.is_none()
335            && self.italic.is_none()
336            && self.underline.is_none()
337            && self.strikethrough.is_none()
338            && self.color.is_none()
339    }
340}
341
342impl IsEmpty for BorderSideDescriptor {
343    fn is_empty(&self) -> bool {
344        self.style.is_none() && self.color.is_none()
345    }
346}
347
348impl IsEmpty for BordersDescriptor {
349    fn is_empty(&self) -> bool {
350        self.left.is_none()
351            && self.right.is_none()
352            && self.top.is_none()
353            && self.bottom.is_none()
354            && self.diagonal.is_none()
355            && self.vertical.is_none()
356            && self.horizontal.is_none()
357            && self.diagonal_up.is_none()
358            && self.diagonal_down.is_none()
359    }
360}
361
362impl IsEmpty for AlignmentDescriptor {
363    fn is_empty(&self) -> bool {
364        self.horizontal.is_none()
365            && self.vertical.is_none()
366            && self.wrap_text.is_none()
367            && self.text_rotation.is_none()
368    }
369}
370
371#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
372#[serde(rename_all = "snake_case")]
373pub enum StylePatchMode {
374    Merge,
375    Set,
376    Clear,
377}
378
379pub fn apply_style_patch(current: &Style, patch: &StylePatch, mode: StylePatchMode) -> Style {
380    match mode {
381        StylePatchMode::Clear => Style::default(),
382        StylePatchMode::Set | StylePatchMode::Merge => {
383            let mut desc = match mode {
384                StylePatchMode::Merge => descriptor_from_style(current),
385                StylePatchMode::Set => StyleDescriptor::default(),
386                StylePatchMode::Clear => StyleDescriptor::default(),
387            };
388            merge_style_patch(&mut desc, patch);
389            let mut style = Style::default();
390            apply_descriptor_to_style(&mut style, &desc);
391            style
392        }
393    }
394}
395
396fn merge_style_patch(desc: &mut StyleDescriptor, patch: &StylePatch) {
397    if let Some(font_patch) = &patch.font {
398        match font_patch {
399            None => desc.font = None,
400            Some(p) => {
401                let mut font_desc = desc.font.take().unwrap_or_default();
402                apply_font_patch(&mut font_desc, p);
403                if font_desc.is_empty() {
404                    desc.font = None;
405                } else {
406                    desc.font = Some(font_desc);
407                }
408            }
409        }
410    }
411
412    if let Some(fill_patch) = &patch.fill {
413        match fill_patch {
414            None => desc.fill = None,
415            Some(p) => {
416                let fill_desc = apply_fill_patch(desc.fill.take(), p);
417                desc.fill = fill_desc;
418            }
419        }
420    }
421
422    if let Some(borders_patch) = &patch.borders {
423        match borders_patch {
424            None => desc.borders = None,
425            Some(p) => {
426                let mut borders_desc = desc.borders.take().unwrap_or_default();
427                apply_borders_patch(&mut borders_desc, p);
428                if borders_desc.is_empty() {
429                    desc.borders = None;
430                } else {
431                    desc.borders = Some(borders_desc);
432                }
433            }
434        }
435    }
436
437    if let Some(alignment_patch) = &patch.alignment {
438        match alignment_patch {
439            None => desc.alignment = None,
440            Some(p) => {
441                let mut align_desc = desc.alignment.take().unwrap_or_default();
442                apply_alignment_patch(&mut align_desc, p);
443                if align_desc.is_empty() {
444                    desc.alignment = None;
445                } else {
446                    desc.alignment = Some(align_desc);
447                }
448            }
449        }
450    }
451
452    if let Some(nf_patch) = &patch.number_format {
453        match nf_patch {
454            None => desc.number_format = None,
455            Some(fmt) => {
456                let fmt = fmt.trim();
457                if fmt.is_empty() || fmt.eq_ignore_ascii_case("general") {
458                    desc.number_format = None;
459                } else {
460                    desc.number_format = Some(fmt.to_string());
461                }
462            }
463        }
464    }
465}
466
467fn apply_font_patch(desc: &mut FontDescriptor, patch: &FontPatch) {
468    apply_double(&mut desc.name, &patch.name);
469    if desc.name.as_deref().is_some_and(|s| s.trim().is_empty()) {
470        desc.name = None;
471    }
472
473    apply_double(&mut desc.size, &patch.size);
474    if desc.size.is_some_and(|s| s <= 0.0) {
475        desc.size = None;
476    }
477
478    apply_double(&mut desc.bold, &patch.bold);
479    if desc.bold == Some(false) {
480        desc.bold = None;
481    }
482
483    apply_double(&mut desc.italic, &patch.italic);
484    if desc.italic == Some(false) {
485        desc.italic = None;
486    }
487
488    apply_double(&mut desc.underline, &patch.underline);
489    if desc
490        .underline
491        .as_deref()
492        .is_some_and(|u| u.trim().is_empty() || u.eq_ignore_ascii_case("none"))
493    {
494        desc.underline = None;
495    }
496
497    apply_double(&mut desc.strikethrough, &patch.strikethrough);
498    if desc.strikethrough == Some(false) {
499        desc.strikethrough = None;
500    }
501
502    apply_double(&mut desc.color, &patch.color);
503    if desc.color.as_deref().is_some_and(|c| c.trim().is_empty()) {
504        desc.color = None;
505    }
506}
507
508fn apply_fill_patch(existing: Option<FillDescriptor>, patch: &FillPatch) -> Option<FillDescriptor> {
509    match patch {
510        FillPatch::Pattern(patch_pattern) => {
511            let mut desc = match existing {
512                Some(FillDescriptor::Pattern(p)) => p,
513                _ => PatternFillDescriptor::default(),
514            };
515            apply_pattern_fill_patch(&mut desc, patch_pattern);
516            if desc.pattern_type.is_none()
517                && desc.foreground_color.is_none()
518                && desc.background_color.is_none()
519            {
520                None
521            } else {
522                Some(FillDescriptor::Pattern(desc))
523            }
524        }
525        FillPatch::Gradient(patch_gradient) => {
526            let mut desc = match existing {
527                Some(FillDescriptor::Gradient(g)) => g,
528                _ => GradientFillDescriptor::default(),
529            };
530            apply_gradient_fill_patch(&mut desc, patch_gradient);
531            if desc.degree.is_none() && desc.stops.is_empty() {
532                None
533            } else {
534                Some(FillDescriptor::Gradient(desc))
535            }
536        }
537    }
538}
539
540fn apply_pattern_fill_patch(desc: &mut PatternFillDescriptor, patch: &PatternFillPatch) {
541    apply_double(&mut desc.pattern_type, &patch.pattern_type);
542    if desc
543        .pattern_type
544        .as_deref()
545        .is_some_and(|p| p.trim().is_empty() || p.eq_ignore_ascii_case("none"))
546    {
547        desc.pattern_type = None;
548    }
549
550    apply_double(&mut desc.foreground_color, &patch.foreground_color);
551    if desc
552        .foreground_color
553        .as_deref()
554        .is_some_and(|c| c.trim().is_empty())
555    {
556        desc.foreground_color = None;
557    }
558
559    apply_double(&mut desc.background_color, &patch.background_color);
560    if desc
561        .background_color
562        .as_deref()
563        .is_some_and(|c| c.trim().is_empty())
564    {
565        desc.background_color = None;
566    }
567}
568
569fn apply_gradient_fill_patch(desc: &mut GradientFillDescriptor, patch: &GradientFillPatch) {
570    apply_double(&mut desc.degree, &patch.degree);
571    if desc.degree == Some(0.0) {
572        desc.degree = None;
573    }
574
575    if let Some(stops) = &patch.stops {
576        desc.stops = stops
577            .iter()
578            .map(|s| GradientStopDescriptor {
579                position: s.position,
580                color: s.color.clone(),
581            })
582            .collect();
583    }
584}
585
586fn apply_borders_patch(desc: &mut BordersDescriptor, patch: &BordersPatch) {
587    apply_border_side_patch(&mut desc.left, &patch.left);
588    apply_border_side_patch(&mut desc.right, &patch.right);
589    apply_border_side_patch(&mut desc.top, &patch.top);
590    apply_border_side_patch(&mut desc.bottom, &patch.bottom);
591    apply_border_side_patch(&mut desc.diagonal, &patch.diagonal);
592    apply_border_side_patch(&mut desc.vertical, &patch.vertical);
593    apply_border_side_patch(&mut desc.horizontal, &patch.horizontal);
594
595    apply_double(&mut desc.diagonal_up, &patch.diagonal_up);
596    if desc.diagonal_up == Some(false) {
597        desc.diagonal_up = None;
598    }
599    apply_double(&mut desc.diagonal_down, &patch.diagonal_down);
600    if desc.diagonal_down == Some(false) {
601        desc.diagonal_down = None;
602    }
603}
604
605fn apply_border_side_patch(
606    target: &mut Option<BorderSideDescriptor>,
607    patch: &Option<Option<BorderSidePatch>>,
608) {
609    match patch {
610        None => {}
611        Some(None) => *target = None,
612        Some(Some(p)) => {
613            let mut side = target.take().unwrap_or_default();
614            apply_double(&mut side.style, &p.style);
615            if side
616                .style
617                .as_deref()
618                .is_some_and(|s| s.trim().is_empty() || s.eq_ignore_ascii_case("none"))
619            {
620                side.style = None;
621            }
622            apply_double(&mut side.color, &p.color);
623            if side.color.as_deref().is_some_and(|c| c.trim().is_empty()) {
624                side.color = None;
625            }
626            if side.is_empty() {
627                *target = None;
628            } else {
629                *target = Some(side);
630            }
631        }
632    }
633}
634
635fn apply_alignment_patch(desc: &mut AlignmentDescriptor, patch: &AlignmentPatch) {
636    apply_double(&mut desc.horizontal, &patch.horizontal);
637    if desc
638        .horizontal
639        .as_deref()
640        .is_some_and(|h| h.trim().is_empty() || h.eq_ignore_ascii_case("general"))
641    {
642        desc.horizontal = None;
643    }
644
645    apply_double(&mut desc.vertical, &patch.vertical);
646    if desc
647        .vertical
648        .as_deref()
649        .is_some_and(|v| v.trim().is_empty() || v.eq_ignore_ascii_case("bottom"))
650    {
651        desc.vertical = None;
652    }
653
654    apply_double(&mut desc.wrap_text, &patch.wrap_text);
655    if desc.wrap_text == Some(false) {
656        desc.wrap_text = None;
657    }
658
659    apply_double(&mut desc.text_rotation, &patch.text_rotation);
660    if desc.text_rotation == Some(0) {
661        desc.text_rotation = None;
662    }
663}
664
665fn apply_descriptor_to_style(style: &mut Style, desc: &StyleDescriptor) {
666    if let Some(font_desc) = &desc.font {
667        let font = style.get_font_mut();
668        if let Some(name) = &font_desc.name {
669            font.set_name(name.clone());
670        }
671        if let Some(size) = font_desc.size {
672            font.set_size(size);
673        }
674        if let Some(bold) = font_desc.bold {
675            font.set_bold(bold);
676        }
677        if let Some(italic) = font_desc.italic {
678            font.set_italic(italic);
679        }
680        if let Some(underline) = &font_desc.underline {
681            font.set_underline(underline.clone());
682        }
683        if let Some(strike) = font_desc.strikethrough {
684            font.set_strikethrough(strike);
685        }
686        if let Some(color) = &font_desc.color {
687            font.get_color_mut().set_argb(color.clone());
688        }
689    }
690
691    if let Some(fill_desc) = &desc.fill {
692        match fill_desc {
693            FillDescriptor::Pattern(p) => {
694                let pat = style.get_fill_mut().get_pattern_fill_mut();
695                if let Some(kind) = &p.pattern_type
696                    && let Ok(pv) = PatternValues::from_str(kind)
697                {
698                    pat.set_pattern_type(pv);
699                }
700                if let Some(fg) = &p.foreground_color {
701                    pat.get_foreground_color_mut().set_argb(fg.clone());
702                }
703                if let Some(bg) = &p.background_color {
704                    pat.get_background_color_mut().set_argb(bg.clone());
705                }
706            }
707            FillDescriptor::Gradient(g) => {
708                let grad = style.get_fill_mut().get_gradient_fill_mut();
709                if let Some(deg) = g.degree {
710                    grad.set_degree(deg);
711                }
712                grad.get_gradient_stop_mut().clear();
713                for stop in &g.stops {
714                    let mut st = umya_spreadsheet::GradientStop::default();
715                    st.set_position(stop.position);
716                    st.get_color_mut().set_argb(stop.color.clone());
717                    grad.set_gradient_stop(st);
718                }
719            }
720        }
721    }
722
723    if let Some(border_desc) = &desc.borders {
724        let borders = style.get_borders_mut();
725        apply_border_side_descriptor(borders.get_left_border_mut(), &border_desc.left);
726        apply_border_side_descriptor(borders.get_right_border_mut(), &border_desc.right);
727        apply_border_side_descriptor(borders.get_top_border_mut(), &border_desc.top);
728        apply_border_side_descriptor(borders.get_bottom_border_mut(), &border_desc.bottom);
729        apply_border_side_descriptor(borders.get_diagonal_border_mut(), &border_desc.diagonal);
730        apply_border_side_descriptor(borders.get_vertical_border_mut(), &border_desc.vertical);
731        apply_border_side_descriptor(borders.get_horizontal_border_mut(), &border_desc.horizontal);
732        if let Some(up) = border_desc.diagonal_up {
733            borders.set_diagonal_up(up);
734        }
735        if let Some(down) = border_desc.diagonal_down {
736            borders.set_diagonal_down(down);
737        }
738    }
739
740    if let Some(align_desc) = &desc.alignment {
741        let align = style.get_alignment_mut();
742        if let Some(h) = &align_desc.horizontal
743            && let Ok(val) = HorizontalAlignmentValues::from_str(h)
744        {
745            align.set_horizontal(val);
746        }
747        if let Some(v) = &align_desc.vertical
748            && let Ok(val) = VerticalAlignmentValues::from_str(v)
749        {
750            align.set_vertical(val);
751        }
752        if let Some(wrap) = align_desc.wrap_text {
753            align.set_wrap_text(wrap);
754        }
755        if let Some(rot) = align_desc.text_rotation {
756            align.set_text_rotation(rot);
757        }
758    }
759
760    if let Some(fmt) = &desc.number_format {
761        style.get_number_format_mut().set_format_code(fmt.clone());
762    }
763}
764
765fn apply_border_side_descriptor(border: &mut Border, desc: &Option<BorderSideDescriptor>) {
766    if let Some(side) = desc {
767        if let Some(style_name) = &side.style {
768            border.set_border_style(style_name.clone());
769        }
770        if let Some(color) = &side.color {
771            border.get_color_mut().set_argb(color.clone());
772        }
773    }
774}
775
776fn apply_double<T: Clone>(target: &mut Option<T>, patch: &Option<Option<T>>) {
777    match patch {
778        None => {}
779        Some(None) => *target = None,
780        Some(Some(v)) => *target = Some(v.clone()),
781    }
782}