1use crate::base::{CellRef, RangeRef};
5use serde::{Deserialize, Serialize};
6
7use crate::model::style::{Color, Style};
8
9mod pivot;
10mod print;
11
12pub use pivot::{PivotCalcField, PivotDateGroup, PivotField, PivotFilter, PivotFunc, PivotLayout, PivotShowAs, PivotSort, PivotTable, PivotValue};
13pub use print::{EditRange, HfPage, LineInfo, Orientation, PageOrder, PrintSettings, SheetProtection, password_hash};
14
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
18pub enum TotalsFn {
19 #[default]
20 None,
21 Average,
22 Count,
23 CountNums,
24 Max,
25 Min,
26 Sum,
27 StdDev,
28 Var,
29 Custom,
30}
31
32impl TotalsFn {
33 pub fn subtotal_code(&self) -> Option<u32> {
35 Some(match self {
36 TotalsFn::Average => 101,
37 TotalsFn::Count => 103,
38 TotalsFn::CountNums => 102,
39 TotalsFn::Max => 104,
40 TotalsFn::Min => 105,
41 TotalsFn::Sum => 109,
42 TotalsFn::StdDev => 107,
43 TotalsFn::Var => 110,
44 _ => return None,
45 })
46 }
47}
48
49#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
50pub struct TableColumn {
51 pub name: String,
52 pub totals: TotalsFn,
53 pub totals_label: Option<String>,
55 pub formula: Option<String>,
57 #[serde(default)]
59 pub unique_name: Option<String>,
60 #[serde(default)]
63 pub xml: Option<XmlColumnPr>,
64}
65
66#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
68pub struct XmlColumnPr {
69 pub map_id: u32,
70 pub xpath: String,
71 pub data_type: String,
73 pub denormalized: bool,
74}
75
76#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
77pub struct Table {
78 pub id: u32,
79 pub name: String,
80 pub range: RangeRef,
82 pub header_row: bool,
83 pub totals_row: bool,
84 pub columns: Vec<TableColumn>,
85 pub style: String,
86 pub banded_rows: bool,
87 pub banded_cols: bool,
88 pub first_col: bool,
89 pub last_col: bool,
90 pub filter_button: bool,
91 #[serde(default)]
95 pub filter: Vec<(u32, FilterCriterion)>,
96 #[serde(default)]
98 pub table_type: Option<String>,
99 #[serde(default)]
101 pub connection_id: Option<u32>,
102}
103
104impl Table {
105 pub fn data_range(&self) -> Option<RangeRef> {
106 let r0 = self.range.start.row + self.header_row as u32;
107 let r1 = self.range.end.row.checked_sub(self.totals_row as u32)?;
108 if r1 < r0 {
109 return None;
110 }
111 Some(RangeRef::new(CellRef::new(r0, self.range.start.col), CellRef::new(r1, self.range.end.col)))
112 }
113 pub fn header_range(&self) -> Option<RangeRef> {
114 self.header_row.then(|| RangeRef::new(self.range.start, CellRef::new(self.range.start.row, self.range.end.col)))
115 }
116 pub fn totals_range(&self) -> Option<RangeRef> {
117 self.totals_row.then(|| RangeRef::new(CellRef::new(self.range.end.row, self.range.start.col), self.range.end))
118 }
119 pub fn column_index(&self, name: &str) -> Option<usize> {
120 self.columns.iter().position(|c| c.name.eq_ignore_ascii_case(name))
121 }
122}
123
124#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
127pub enum CfOperator {
128 Between,
129 NotBetween,
130 Equal,
131 NotEqual,
132 Greater,
133 Less,
134 GreaterOrEqual,
135 LessOrEqual,
136}
137
138#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
139pub enum CfValueKind {
140 Min,
141 Max,
142 Number(f64),
143 Percent(f64),
144 Percentile(f64),
145 Formula(String),
146}
147
148#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
149pub enum CfRule {
150 CellIs {
152 op: CfOperator,
153 a: String,
154 b: Option<String>,
155 style: Box<Style>,
156 },
157 Expression {
159 formula: String,
160 style: Box<Style>,
161 },
162 ContainsText {
163 text: String,
164 style: Box<Style>,
165 },
166 NotContainsText {
167 text: String,
168 style: Box<Style>,
169 },
170 BeginsWith {
171 text: String,
172 style: Box<Style>,
173 },
174 EndsWith {
175 text: String,
176 style: Box<Style>,
177 },
178 Blanks {
179 style: Box<Style>,
180 },
181 NoBlanks {
182 style: Box<Style>,
183 },
184 Errors {
185 style: Box<Style>,
186 },
187 NoErrors {
188 style: Box<Style>,
189 },
190 Duplicate {
191 style: Box<Style>,
192 },
193 Unique {
194 style: Box<Style>,
195 },
196 Top10 {
198 bottom: bool,
199 percent: bool,
200 rank: u32,
201 style: Box<Style>,
202 },
203 AboveAverage {
204 below: bool,
205 equal: bool,
206 std_dev: u8,
207 style: Box<Style>,
208 },
209 TimePeriod {
211 period: String,
212 style: Box<Style>,
213 },
214 ColorScale {
215 stops: Vec<(CfValueKind, Color)>,
216 },
217 DataBar {
218 min: CfValueKind,
219 max: CfValueKind,
220 color: Color,
221 gradient: bool,
222 show_value: bool,
223 },
224 IconSet {
226 set: String,
227 thresholds: Vec<CfValueKind>,
228 reverse: bool,
229 show_value: bool,
230 },
231}
232
233pub fn icon_count(set: &str) -> usize {
235 set.chars().next().and_then(|c| c.to_digit(10)).unwrap_or(3).clamp(3, 5) as usize
236}
237
238pub fn default_icon_thresholds(set: &str) -> Vec<CfValueKind> {
241 let k = icon_count(set);
242 (0..k).map(|i| CfValueKind::Percent((i as f64 * 100.0 / k as f64).round())).collect()
243}
244
245#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
246pub struct CondFormat {
247 pub ranges: Vec<RangeRef>,
248 pub rule: CfRule,
249 pub priority: u32,
250 pub stop_if_true: bool,
251}
252
253#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
256pub enum ValidationKind {
257 #[default]
258 Any,
259 Whole,
260 Decimal,
261 List,
262 Date,
263 Time,
264 TextLength,
265 Custom,
266}
267
268#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
269pub enum ErrorStyle {
270 #[default]
271 Stop,
272 Warning,
273 Information,
274}
275
276#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
277pub struct Validation {
278 pub ranges: Vec<RangeRef>,
279 pub kind: ValidationKind,
280 pub op: CfOperator,
281 pub f1: String,
283 pub f2: Option<String>,
284 pub allow_blank: bool,
285 pub in_cell_dropdown: bool,
286 pub input_title: String,
287 pub input_message: String,
288 pub show_input: bool,
289 pub error_title: String,
290 pub error_message: String,
291 pub error_style: ErrorStyle,
292 pub show_error: bool,
293}
294
295impl Default for Validation {
296 fn default() -> Self {
297 Validation {
298 ranges: vec![],
299 kind: ValidationKind::Any,
300 op: CfOperator::Between,
301 f1: String::new(),
302 f2: None,
303 allow_blank: true,
304 in_cell_dropdown: true,
305 input_title: String::new(),
306 input_message: String::new(),
307 show_input: true,
308 error_title: String::new(),
309 error_message: String::new(),
310 error_style: ErrorStyle::Stop,
311 show_error: true,
312 }
313 }
314}
315
316#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
320pub struct Comment {
321 pub author: String,
322 pub text: String,
323 pub replies: Vec<(String, String)>,
325 pub threaded: bool,
326 pub resolved: bool,
327 pub visible: bool,
328 #[serde(default, skip_serializing_if = "Vec::is_empty")]
331 pub dates: Vec<String>,
332 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub note_box: Option<NoteBox>,
336}
337
338#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
342pub struct NoteBox {
343 pub from: NoteCorner,
346 pub to: NoteCorner,
348 #[serde(default, skip_serializing_if = "Option::is_none")]
350 pub size_pt: Option<(f64, f64)>,
351}
352
353#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
355pub struct NoteCorner {
356 pub col: i64,
357 pub col_px: u32,
358 pub row: i64,
359 pub row_px: u32,
360}
361
362#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
363pub struct Hyperlink {
364 pub target: String,
367 pub tooltip: Option<String>,
368 #[serde(default)]
371 pub external: bool,
372}
373
374#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
380pub enum AnchorMode {
381 #[default]
383 MoveAndSize,
384 MoveOnly,
386 Absolute,
388}
389
390#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
392pub struct Anchor {
393 pub cell: CellRef,
394 pub dx: f32,
395 pub dy: f32,
396 pub width: f32,
397 pub height: f32,
398 #[serde(default)]
400 pub mode: AnchorMode,
401}
402
403#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
404pub enum ChartKind {
405 #[default]
406 ColumnClustered,
407 ColumnStacked,
408 ColumnStacked100,
409 BarClustered,
410 BarStacked,
411 BarStacked100,
412 Line,
413 LineMarkers,
414 LineStacked,
415 Pie,
416 Doughnut,
417 Area,
418 AreaStacked,
419 AreaStacked100,
420 Scatter,
421 ScatterLines,
422 Bubble,
423 Radar,
424 Histogram,
425 Waterfall,
426 Funnel,
427 Treemap,
428 Sunburst,
429 BoxWhisker,
430 Stock,
431 Combo,
432}
433
434#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
435pub enum LegendPos {
436 None,
437 #[default]
438 Bottom,
439 Top,
440 Left,
441 Right,
442}
443
444#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
446pub enum QuartileMethod {
447 #[default]
448 Exclusive,
449 Inclusive,
450}
451
452#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
453pub struct Series {
454 pub name: Option<String>,
456 pub categories: Option<String>,
457 pub values: String,
458 pub bubble_sizes: Option<String>,
460 pub color: Option<Color>,
461 pub secondary: bool,
463 pub kind: Option<ChartKind>,
464 #[serde(default)]
466 pub smooth: bool,
467 #[serde(default)]
469 pub quartiles: QuartileMethod,
470 #[serde(default)]
472 pub trendlines: Vec<Trendline>,
473}
474
475#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
477pub enum TrendlineKind {
478 #[default]
479 Linear,
480 Exponential,
482 Logarithmic,
484 Polynomial(u8),
486 Power,
488 MovingAverage(u32),
490}
491
492#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
494pub enum LineDash {
495 #[default]
496 Solid,
497 Dot,
498 Dash,
499 LgDash,
500 DashDot,
501 LgDashDot,
502 LgDashDotDot,
503 SysDash,
504 SysDot,
505 SysDashDot,
506 SysDashDotDot,
507}
508
509#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
511pub struct Trendline {
512 pub kind: TrendlineKind,
513 pub name: Option<String>,
515 pub forward: f64,
517 pub backward: f64,
519 pub intercept: Option<f64>,
521 pub display_equation: bool,
522 pub display_r_squared: bool,
523 pub color: Option<Color>,
525 pub width: Option<f32>,
527 pub dash: Option<LineDash>,
529}
530
531impl Trendline {
532 pub const MAX_FORECAST: f64 = 100_000.0;
535
536 pub fn clamp_forecast(v: f64) -> f64 {
539 if v.is_finite() && v > 0.0 { v.min(Self::MAX_FORECAST) } else { 0.0 }
540 }
541}
542
543fn default_hole_size() -> u8 {
546 50
547}
548
549#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
551#[serde(default)]
552pub struct ChartDataTable {
553 pub show_keys: bool,
554 pub horizontal_border: bool,
555 pub vertical_border: bool,
556 pub outline: bool,
557}
558
559impl Default for ChartDataTable {
560 fn default() -> Self {
561 Self { show_keys: true, horizontal_border: true, vertical_border: true, outline: true }
562 }
563}
564
565#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
566pub struct Chart {
567 pub id: u32,
568 pub kind: ChartKind,
569 pub anchor: Anchor,
570 pub title: Option<String>,
571 pub series: Vec<Series>,
572 pub legend: LegendPos,
573 pub data_labels: bool,
574 #[serde(default)]
575 pub data_table: Option<ChartDataTable>,
576 pub gridlines: bool,
577 pub style: u32,
578 #[serde(default = "default_hole_size")]
581 pub hole_size: u8,
582 #[serde(default)]
584 pub first_slice_angle: u16,
585 pub x_title: Option<String>,
586 pub y_title: Option<String>,
587 pub source: Option<String>,
589 pub by_rows: bool,
590 #[serde(default)]
593 pub pivot: Option<String>,
594}
595
596impl Chart {
597 pub fn supports_data_table(&self) -> bool {
599 let eligible = |kind| {
600 matches!(
601 kind,
602 ChartKind::ColumnClustered
603 | ChartKind::ColumnStacked
604 | ChartKind::ColumnStacked100
605 | ChartKind::BarClustered
606 | ChartKind::BarStacked
607 | ChartKind::BarStacked100
608 | ChartKind::Line
609 | ChartKind::LineMarkers
610 | ChartKind::LineStacked
611 | ChartKind::Area
612 | ChartKind::AreaStacked
613 | ChartKind::AreaStacked100
614 )
615 };
616 eligible(self.kind) || (self.kind == ChartKind::Combo && self.series.iter().all(|s| eligible(s.kind.unwrap_or(ChartKind::Line))))
617 }
618}
619
620#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
621pub struct Image {
622 pub id: u32,
623 pub anchor: Anchor,
624 #[serde(with = "crate::model::b64::shared")]
627 pub data: std::sync::Arc<Vec<u8>>,
628 pub mime: String,
629 pub alt: String,
630}
631
632#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
633pub enum ShapeKind {
634 #[default]
635 Rectangle,
636 RoundedRectangle,
637 Ellipse,
638 Triangle,
639 Line,
640 Arrow,
641 TextBox,
642 Icon,
644 Ink,
647 Equation,
650}
651
652#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
653pub struct Shape {
654 pub id: u32,
655 pub kind: ShapeKind,
656 pub anchor: Anchor,
657 pub fill: Color,
658 pub line: Color,
659 pub text: String,
660 #[serde(default)]
662 pub format: ShapeFormat,
663 #[serde(skip)]
665 pub math: EquationCache,
666}
667
668#[derive(Clone, Debug, Default)]
680pub struct EquationCache {
681 key: u64,
683 len: usize,
685 source: Option<std::sync::Arc<str>>,
687 layout: Option<Result<std::sync::Arc<crate::math::MathList>, crate::math::MathError>>,
688}
689
690impl PartialEq for EquationCache {
691 fn eq(&self, _: &Self) -> bool {
692 true
693 }
694}
695
696fn source_key(src: &str) -> u64 {
697 use std::hash::{Hash, Hasher};
698 #[cfg(test)]
699 equation_cache_tests::HASHES.with(|h| h.set(h.get() + 1));
700 let mut h = std::collections::hash_map::DefaultHasher::new();
701 src.hash(&mut h);
702 h.finish()
703}
704
705impl EquationCache {
706 fn holds(&self, src: &str) -> bool {
709 if self.layout.is_none() || self.len != src.len() {
710 return false;
711 }
712 if src.len() > crate::math::MAX_SOURCE_BYTES {
713 return self.source.is_none();
714 }
715 self.key == source_key(src) && self.source.as_deref() == Some(src)
716 }
717
718 fn store(&mut self, src: &str, layout: Result<std::sync::Arc<crate::math::MathList>, crate::math::MathError>) {
721 let within = src.len() <= crate::math::MAX_SOURCE_BYTES;
722 self.key = if within { source_key(src) } else { 0 };
723 self.len = src.len();
724 self.source = within.then(|| std::sync::Arc::from(src));
725 self.layout = Some(layout);
726 }
727
728 pub fn get(&self, src: &str) -> Option<Result<&crate::math::MathList, &crate::math::MathError>> {
732 if src.len() > crate::math::MAX_SOURCE_BYTES {
733 return Some(Err(&*crate::math::SOURCE_TOO_LONG));
734 }
735 if !self.holds(src) {
736 return None;
737 }
738 self.layout.as_ref().map(|r| r.as_ref().map(|l| l.as_ref()))
739 }
740
741 pub fn refresh(&mut self, src: &str) -> bool {
743 if self.holds(src) {
744 return false;
745 }
746 self.store(src, crate::math::layout(src).map(std::sync::Arc::new));
747 true
748 }
749
750 pub fn refuse(&mut self, src: &str, message: &str) {
752 self.store(src, Err(crate::math::MathError { span: None, message: message.to_string() }));
753 }
754}
755
756#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
758#[serde(rename_all = "camelCase")]
759pub enum ControlKind {
760 #[default]
761 Button,
762 Slider,
763 Spin,
764}
765
766impl ControlKind {
767 pub fn label(self) -> &'static str {
769 match self {
770 ControlKind::Button => "Button",
771 ControlKind::Slider => "Slider",
772 ControlKind::Spin => "Spin Button",
773 }
774 }
775}
776
777pub const CONTROL_MAX: u32 = 30_000;
780
781#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
783#[serde(tag = "type", rename_all = "camelCase")]
784pub enum ControlAction {
785 Macro {
787 name: String,
788 #[serde(default)]
789 input: MacroInput,
790 #[serde(default, skip_serializing_if = "Option::is_none")]
791 output: Option<String>,
792 },
793 Command {
795 id: String,
796 #[serde(default)]
797 params: serde_json::Value,
798 },
799}
800
801#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
803#[serde(rename_all = "camelCase")]
804pub enum MacroInput {
805 #[default]
807 Selection,
808 Fixed(String),
810}
811
812#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
814#[serde(default)]
815pub struct FormControl {
816 pub id: u32,
817 pub kind: ControlKind,
818 pub anchor: Anchor,
819 pub name: String,
821 pub text: String,
823 pub link: Option<String>,
826 pub value: u32,
828 pub min: u32,
829 pub max: u32,
830 pub step: u32,
832 pub page: u32,
834 pub horizontal: bool,
836 pub action: Option<ControlAction>,
838 pub print: bool,
840 pub locked: bool,
842}
843
844impl Default for FormControl {
845 fn default() -> Self {
846 FormControl {
847 id: 0,
848 kind: ControlKind::default(),
849 anchor: Anchor::default(),
850 name: String::new(),
851 text: String::new(),
852 link: None,
853 value: 0,
854 min: 0,
855 max: 100,
856 step: 1,
857 page: 10,
858 horizontal: true,
859 action: None,
860 print: true,
861 locked: true,
862 }
863 }
864}
865
866impl FormControl {
867 pub fn display_name(&self) -> String {
869 if self.name.trim().is_empty() { format!("{} {}", self.kind.label(), self.id) } else { self.name.clone() }
870 }
871
872 pub fn check_numbers(&self) -> Result<(), String> {
875 if self.kind == ControlKind::Button {
876 return Ok(());
877 }
878 if self.max > CONTROL_MAX {
879 return Err(format!("Maximum value can be at most {CONTROL_MAX}."));
880 }
881 if self.min > CONTROL_MAX {
882 return Err(format!("Minimum value must be a whole number from 0 to {CONTROL_MAX}."));
883 }
884 if self.min >= self.max {
885 return Err(format!("Maximum value ({}) must be greater than Minimum value ({}).", self.max, self.min));
886 }
887 if self.step < 1 {
888 return Err("Incremental change must be at least 1.".into());
889 }
890 let span = self.max - self.min;
891 if self.step > span {
892 return Err(format!("Incremental change ({}) can be at most Maximum minus Minimum ({span}).", self.step));
893 }
894 if self.kind == ControlKind::Slider {
895 if self.page < self.step {
896 return Err(format!("Page change ({}) must be at least the incremental change ({}).", self.page, self.step));
897 }
898 if self.page > CONTROL_MAX {
899 return Err(format!("Page change can be at most {CONTROL_MAX}."));
900 }
901 }
902 Ok(())
903 }
904
905 pub fn snap(&self, v: f64) -> u32 {
907 let (min, max) = (self.min.min(CONTROL_MAX), self.max.min(CONTROL_MAX).max(self.min.min(CONTROL_MAX)));
908 if !v.is_finite() || v <= min as f64 {
909 return min;
910 }
911 if v >= max as f64 {
912 return max;
913 }
914 let step = self.step.max(1) as f64;
915 let snapped = (min as f64 + ((v - min as f64) / step).round() * step).clamp(min as f64, max as f64);
916 if (max as f64 - v).abs() < (snapped - v).abs() { max } else { snapped as u32 }
918 }
919}
920
921#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
926#[serde(rename_all = "camelCase")]
927pub enum ObjKind {
928 Chart,
929 Image,
930 Shape,
931 Slicer,
932 Equation,
933 Control,
934}
935
936impl ObjKind {
937 pub const ALL: [ObjKind; 6] = [ObjKind::Chart, ObjKind::Image, ObjKind::Shape, ObjKind::Slicer, ObjKind::Equation, ObjKind::Control];
938
939 pub fn name(self) -> &'static str {
941 match self {
942 ObjKind::Chart => "chart",
943 ObjKind::Image => "image",
944 ObjKind::Shape => "shape",
945 ObjKind::Slicer => "slicer",
946 ObjKind::Equation => "equation",
947 ObjKind::Control => "control",
948 }
949 }
950
951 pub fn parse(s: &str) -> Option<ObjKind> {
953 ObjKind::ALL.into_iter().find(|k| k.name().eq_ignore_ascii_case(s))
954 }
955}
956
957#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
959pub struct ObjRef {
960 pub kind: ObjKind,
961 pub id: u32,
962}
963
964impl ObjRef {
965 pub fn new(kind: ObjKind, id: u32) -> ObjRef {
966 ObjRef { kind, id }
967 }
968}
969
970#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
972pub enum ShapeTextAlign {
973 #[default]
974 Left,
975 Center,
976 Right,
977}
978
979#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
981pub enum ShapeTextAnchor {
982 #[default]
983 Top,
984 Middle,
985 Bottom,
986}
987
988#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
991#[serde(default)]
992pub struct ShapeFormat {
993 pub no_line: bool,
995 pub font_size: Option<f32>,
997 pub bold: bool,
998 pub italic: bool,
999 pub font: Option<String>,
1001 pub text_color: Option<Color>,
1003 pub text_outline: Option<(Color, f32)>,
1005 pub align: Option<ShapeTextAlign>,
1007 pub anchor: Option<ShapeTextAnchor>,
1009 pub rtl: bool,
1011}
1012
1013#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1014pub enum SparklineKind {
1015 #[default]
1016 Line,
1017 Column,
1018 WinLoss,
1019}
1020
1021#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1022pub struct Sparkline {
1023 pub cell: CellRef,
1024 pub source: String,
1025 pub kind: SparklineKind,
1026 pub color: Color,
1027 pub markers: bool,
1028}
1029
1030#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1033pub enum FilterCriterion {
1034 Values {
1036 values: Vec<String>,
1037 blanks: bool,
1038 },
1039 Custom {
1041 a: (String, String),
1042 b: Option<(String, String)>,
1043 and: bool,
1044 },
1045 Top10 {
1046 bottom: bool,
1047 percent: bool,
1048 count: u32,
1049 },
1050 AboveAverage(bool),
1051 FillColor(Color),
1052 FontColor(Color),
1053 Icon {
1056 set: String,
1057 id: Option<u32>,
1058 },
1059}
1060
1061#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1062pub struct AutoFilter {
1063 pub range: RangeRef,
1064 pub criteria: Vec<(u32, FilterCriterion)>,
1066}
1067
1068#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1072pub struct Slicer {
1073 pub id: u32,
1074 pub table: String,
1076 pub column: String,
1078 pub caption: String,
1080 pub anchor: Anchor,
1081 pub selected: Option<Vec<String>>,
1084 pub columns: u32,
1086}
1087
1088#[cfg(test)]
1089mod trendline_forecast_tests {
1090 use super::Trendline;
1091
1092 #[test]
1093 fn clamp_forecast_bounds_hostile_values() {
1094 assert_eq!(Trendline::clamp_forecast(2.5), 2.5);
1095 assert_eq!(Trendline::clamp_forecast(0.0), 0.0);
1096 assert_eq!(Trendline::clamp_forecast(-4.0), 0.0);
1097 assert_eq!(Trendline::clamp_forecast(f64::NAN), 0.0);
1098 assert_eq!(Trendline::clamp_forecast(f64::INFINITY), 0.0);
1099 assert_eq!(Trendline::clamp_forecast(f64::NEG_INFINITY), 0.0);
1100 assert_eq!(Trendline::clamp_forecast(1e300), Trendline::MAX_FORECAST);
1101 }
1102}
1103
1104#[cfg(test)]
1105mod tests {
1106 use super::*;
1107
1108 #[test]
1109 fn chart_without_hole_size_keeps_the_legacy_hole() {
1110 let chart = Chart {
1111 id: 1,
1112 kind: ChartKind::Doughnut,
1113 anchor: Anchor::default(),
1114 title: None,
1115 series: vec![],
1116 legend: LegendPos::Right,
1117 data_labels: false,
1118 data_table: None,
1119 gridlines: false,
1120 style: 1,
1121 hole_size: 75,
1122 first_slice_angle: 0,
1123 x_title: None,
1124 y_title: None,
1125 source: None,
1126 by_rows: false,
1127 pivot: None,
1128 };
1129 let mut json = serde_json::to_value(&chart).expect("serialize chart");
1130 json.as_object_mut().expect("chart object").remove("hole_size");
1131 let legacy: Chart = serde_json::from_value(json).expect("deserialize legacy chart");
1132 assert_eq!(legacy.hole_size, 50, "documents saved before hole_size existed keep the old 50% hole");
1133 assert_eq!(chart.hole_size, 75, "new charts keep Excel's 75% default");
1134 }
1135}
1136
1137#[cfg(test)]
1138mod equation_cache_tests {
1139 use super::{EquationCache, source_key};
1140
1141 thread_local! {
1142 pub(super) static HASHES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1144 }
1145
1146 fn glyphs(c: &EquationCache, src: &str) -> Option<String> {
1147 let list = c.get(src)?.ok()?;
1148 Some(list.items.iter().filter_map(|i| if let crate::math::Item::Glyph(g) = i { Some(g.ch) } else { None }).collect())
1149 }
1150
1151 #[test]
1152 fn a_hash_collision_never_returns_another_sources_layout() {
1153 let (a, b) = ("x^2", "y^3");
1154 let mut c = EquationCache::default();
1155 assert!(c.refresh(a));
1156 assert_eq!(glyphs(&c, a).as_deref(), Some("x2"));
1157 c.key = source_key(b);
1160 assert!(c.get(b).is_none(), "`b` is a miss, not `a`'s layout");
1161 assert!(c.refresh(b), "`b` is laid out afresh");
1162 assert_eq!(glyphs(&c, b).as_deref(), Some("y3"));
1163 assert!(c.get(a).is_none(), "the entry is `b`'s now");
1164 c.refuse(a, "not laid out");
1166 c.key = source_key(b);
1167 assert!(c.get(b).is_none());
1168 }
1169
1170 #[test]
1171 fn sources_over_the_cap_are_not_copied_hashed_or_laid_out() {
1172 let long = "x".repeat(crate::math::MAX_SOURCE_BYTES + 1);
1173 let mut c = EquationCache::default();
1174 HASHES.with(|h| h.set(0));
1175 assert!(matches!(c.get(&long), Some(Err(e)) if e.message.contains("4096")), "answered before any refresh");
1176 assert!(c.refresh(&long));
1177 assert!(c.source.is_none(), "an over-cap source is not kept");
1178 assert_eq!(c.key, 0, "nor hashed");
1179 assert!(matches!(c.get(&long), Some(Err(_))));
1180 assert!(!c.refresh(&long), "still cached");
1181 let other = "y".repeat(crate::math::MAX_SOURCE_BYTES + 1);
1182 assert!(matches!(c.get(&other), Some(Err(_))), "another over-cap source gets the same answer");
1183 assert_eq!(HASHES.with(|h| h.get()), 0, "nothing over the cap is hashed");
1184 assert!(c.refresh("x"));
1186 assert!(c.get("x").is_some_and(|r| r.is_ok()));
1187 assert!(HASHES.with(|h| h.get()) >= 2);
1188 }
1189
1190 #[test]
1191 #[allow(clippy::disallowed_methods)] fn a_source_of_a_megabyte_is_looked_up_without_being_read() {
1193 let big = "l".repeat(1 << 20);
1196 let mut c = EquationCache::default();
1197 c.refuse(&big, "budget");
1198 HASHES.with(|h| h.set(0));
1199 let start = std::time::Instant::now();
1200 for _ in 0..1000 {
1201 assert!(matches!(c.get(&big), Some(Err(_))));
1202 }
1203 let took = start.elapsed();
1204 assert_eq!(HASHES.with(|h| h.get()), 0, "a thousand lookups read nothing");
1205 assert!(took < std::time::Duration::from_millis(500), "{took:?}");
1206 }
1207}