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}