1use crate::config::NumberFormat;
10use crate::pivot::aggregation::AggregationFn;
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
14pub enum PivotZone {
15 Rows,
17 Columns,
19 Values,
21 Filters,
23}
24
25#[derive(Clone, Debug, PartialEq)]
30pub struct PivotConfig {
31 pub row_fields: Vec<usize>,
33 pub column_fields: Vec<usize>,
35 pub value_field: Option<usize>,
38 pub aggregation: AggregationFn,
40 pub filter_fields: Vec<usize>,
43 pub show_row_subtotals: bool,
45 pub show_column_subtotals: bool,
47 pub show_row_grand_total: bool,
49 pub show_column_grand_total: bool,
51 pub blank_label: String,
53 pub value_format: Option<NumberFormat>,
56}
57
58impl Default for PivotConfig {
59 fn default() -> Self {
60 Self {
61 row_fields: vec![],
62 column_fields: vec![],
63 value_field: None,
64 aggregation: AggregationFn::default(),
65 filter_fields: vec![],
66 show_row_subtotals: true,
67 show_column_subtotals: false,
68 show_row_grand_total: true,
69 show_column_grand_total: true,
70 blank_label: "(blank)".into(),
71 value_format: None,
72 }
73 }
74}
75
76impl PivotConfig {
77 #[must_use]
80 pub fn is_ready(&self) -> bool {
81 self.value_field.is_some()
82 && (!self.row_fields.is_empty() || !self.column_fields.is_empty())
83 }
84
85 #[must_use]
87 pub fn zone_of(&self, field: usize) -> Option<PivotZone> {
88 if self.row_fields.contains(&field) {
89 Some(PivotZone::Rows)
90 } else if self.column_fields.contains(&field) {
91 Some(PivotZone::Columns)
92 } else if self.value_field == Some(field) {
93 Some(PivotZone::Values)
94 } else if self.filter_fields.contains(&field) {
95 Some(PivotZone::Filters)
96 } else {
97 None
98 }
99 }
100
101 #[must_use]
103 pub fn fields_in(&self, zone: PivotZone) -> Vec<usize> {
104 match zone {
105 PivotZone::Rows => self.row_fields.clone(),
106 PivotZone::Columns => self.column_fields.clone(),
107 PivotZone::Values => self.value_field.into_iter().collect(),
108 PivotZone::Filters => self.filter_fields.clone(),
109 }
110 }
111
112 pub fn move_field(&mut self, field: usize, zone: PivotZone, index: Option<usize>) {
118 self.remove_field(field);
119 match zone {
120 PivotZone::Rows => insert_clamped(&mut self.row_fields, field, index),
121 PivotZone::Columns => insert_clamped(&mut self.column_fields, field, index),
122 PivotZone::Values => self.value_field = Some(field),
123 PivotZone::Filters => insert_clamped(&mut self.filter_fields, field, index),
124 }
125 }
126
127 pub fn remove_field(&mut self, field: usize) {
129 self.row_fields.retain(|&f| f != field);
130 self.column_fields.retain(|&f| f != field);
131 self.filter_fields.retain(|&f| f != field);
132 if self.value_field == Some(field) {
133 self.value_field = None;
134 }
135 }
136
137 pub fn clamp_to_columns(&mut self, column_count: usize) {
140 self.row_fields.retain(|&f| f < column_count);
141 self.column_fields.retain(|&f| f < column_count);
142 self.filter_fields.retain(|&f| f < column_count);
143 if self.value_field.is_some_and(|f| f >= column_count) {
144 self.value_field = None;
145 }
146 }
147}
148
149fn insert_clamped(list: &mut Vec<usize>, field: usize, index: Option<usize>) {
150 let at = index.unwrap_or(list.len()).min(list.len());
151 list.insert(at, field);
152}
153
154#[cfg(test)]
155#[allow(clippy::field_reassign_with_default)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn default_is_not_ready() {
161 let cfg = PivotConfig::default();
162 assert!(!cfg.is_ready());
163 assert_eq!(cfg.zone_of(0), None);
164 }
165
166 #[test]
167 fn ready_needs_value_and_one_axis() {
168 let mut cfg = PivotConfig::default();
169 cfg.value_field = Some(2);
170 assert!(!cfg.is_ready(), "value alone is not enough");
171 cfg.row_fields = vec![0];
172 assert!(cfg.is_ready());
173 cfg.row_fields.clear();
174 cfg.column_fields = vec![1];
175 assert!(cfg.is_ready());
176 }
177
178 #[test]
179 fn move_field_between_zones_is_exclusive() {
180 let mut cfg = PivotConfig::default();
181 cfg.move_field(3, PivotZone::Rows, None);
182 assert_eq!(cfg.zone_of(3), Some(PivotZone::Rows));
183 cfg.move_field(3, PivotZone::Columns, None);
184 assert_eq!(cfg.zone_of(3), Some(PivotZone::Columns));
185 assert!(cfg.row_fields.is_empty());
186 cfg.move_field(3, PivotZone::Filters, None);
187 assert_eq!(cfg.zone_of(3), Some(PivotZone::Filters));
188 assert!(cfg.column_fields.is_empty());
189 }
190
191 #[test]
192 fn values_zone_holds_exactly_one() {
193 let mut cfg = PivotConfig::default();
194 cfg.move_field(1, PivotZone::Values, None);
195 cfg.move_field(2, PivotZone::Values, None);
196 assert_eq!(cfg.value_field, Some(2));
197 assert_eq!(cfg.zone_of(1), None);
199 }
200
201 #[test]
202 fn move_field_respects_insertion_index() {
203 let mut cfg = PivotConfig::default();
204 cfg.move_field(1, PivotZone::Rows, None);
205 cfg.move_field(2, PivotZone::Rows, None);
206 cfg.move_field(3, PivotZone::Rows, Some(0));
207 assert_eq!(cfg.row_fields, vec![3, 1, 2]);
208 cfg.move_field(4, PivotZone::Rows, Some(99));
210 assert_eq!(cfg.row_fields, vec![3, 1, 2, 4]);
211 }
212
213 #[test]
214 fn reorder_within_same_zone() {
215 let mut cfg = PivotConfig::default();
216 cfg.row_fields = vec![1, 2, 3];
217 cfg.move_field(3, PivotZone::Rows, Some(0));
218 assert_eq!(cfg.row_fields, vec![3, 1, 2]);
219 }
220
221 #[test]
222 fn remove_field_clears_every_zone() {
223 let mut cfg = PivotConfig::default();
224 cfg.row_fields = vec![1];
225 cfg.value_field = Some(2);
226 cfg.remove_field(1);
227 cfg.remove_field(2);
228 assert!(cfg.row_fields.is_empty());
229 assert_eq!(cfg.value_field, None);
230 }
231
232 #[test]
233 fn clamp_to_columns_drops_out_of_range() {
234 let mut cfg = PivotConfig::default();
235 cfg.row_fields = vec![0, 5];
236 cfg.column_fields = vec![9];
237 cfg.filter_fields = vec![2, 7];
238 cfg.value_field = Some(6);
239 cfg.clamp_to_columns(4);
240 assert_eq!(cfg.row_fields, vec![0]);
241 assert!(cfg.column_fields.is_empty());
242 assert_eq!(cfg.filter_fields, vec![2]);
243 assert_eq!(cfg.value_field, None);
244 }
245
246 #[test]
247 fn fields_in_reports_zone_contents() {
248 let mut cfg = PivotConfig::default();
249 cfg.row_fields = vec![4, 1];
250 cfg.value_field = Some(0);
251 assert_eq!(cfg.fields_in(PivotZone::Rows), vec![4, 1]);
252 assert_eq!(cfg.fields_in(PivotZone::Values), vec![0]);
253 assert!(cfg.fields_in(PivotZone::Columns).is_empty());
254 }
255}