1use crate::config::NumberFormat;
10use crate::pivot::aggregation::AggregationFn;
11use std::collections::HashMap;
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
15pub enum PivotZone {
16 Rows,
18 Columns,
20 Values,
22 Filters,
24}
25
26#[derive(Clone, Debug, PartialEq)]
31pub struct PivotConfig {
32 pub row_fields: Vec<usize>,
34 pub column_fields: Vec<usize>,
36 pub value_field: Option<usize>,
39 pub aggregation: AggregationFn,
41 pub filter_fields: Vec<usize>,
44 pub show_row_subtotals: bool,
46 pub show_column_subtotals: bool,
48 pub show_row_grand_total: bool,
50 pub show_column_grand_total: bool,
52 pub blank_label: String,
54 pub value_format: Option<NumberFormat>,
57 pub field_formats: HashMap<usize, NumberFormat>,
64}
65
66impl Default for PivotConfig {
67 fn default() -> Self {
68 Self {
69 row_fields: vec![],
70 column_fields: vec![],
71 value_field: None,
72 aggregation: AggregationFn::default(),
73 filter_fields: vec![],
74 show_row_subtotals: true,
75 show_column_subtotals: false,
76 show_row_grand_total: true,
77 show_column_grand_total: true,
78 blank_label: "(blank)".into(),
79 value_format: None,
80 field_formats: HashMap::new(),
81 }
82 }
83}
84
85impl PivotConfig {
86 #[must_use]
89 pub fn is_ready(&self) -> bool {
90 self.value_field.is_some()
91 && (!self.row_fields.is_empty() || !self.column_fields.is_empty())
92 }
93
94 #[must_use]
96 pub fn zone_of(&self, field: usize) -> Option<PivotZone> {
97 if self.row_fields.contains(&field) {
98 Some(PivotZone::Rows)
99 } else if self.column_fields.contains(&field) {
100 Some(PivotZone::Columns)
101 } else if self.value_field == Some(field) {
102 Some(PivotZone::Values)
103 } else if self.filter_fields.contains(&field) {
104 Some(PivotZone::Filters)
105 } else {
106 None
107 }
108 }
109
110 #[must_use]
112 pub fn fields_in(&self, zone: PivotZone) -> Vec<usize> {
113 match zone {
114 PivotZone::Rows => self.row_fields.clone(),
115 PivotZone::Columns => self.column_fields.clone(),
116 PivotZone::Values => self.value_field.into_iter().collect(),
117 PivotZone::Filters => self.filter_fields.clone(),
118 }
119 }
120
121 pub fn move_field(&mut self, field: usize, zone: PivotZone, index: Option<usize>) {
127 self.remove_field(field);
128 match zone {
129 PivotZone::Rows => insert_clamped(&mut self.row_fields, field, index),
130 PivotZone::Columns => insert_clamped(&mut self.column_fields, field, index),
131 PivotZone::Values => self.value_field = Some(field),
132 PivotZone::Filters => insert_clamped(&mut self.filter_fields, field, index),
133 }
134 }
135
136 pub fn remove_field(&mut self, field: usize) {
138 self.row_fields.retain(|&f| f != field);
139 self.column_fields.retain(|&f| f != field);
140 self.filter_fields.retain(|&f| f != field);
141 if self.value_field == Some(field) {
142 self.value_field = None;
143 }
144 }
145
146 pub fn clamp_to_columns(&mut self, column_count: usize) {
149 self.row_fields.retain(|&f| f < column_count);
150 self.column_fields.retain(|&f| f < column_count);
151 self.filter_fields.retain(|&f| f < column_count);
152 if self.value_field.is_some_and(|f| f >= column_count) {
153 self.value_field = None;
154 }
155 self.field_formats.retain(|&f, _| f < column_count);
156 }
157}
158
159fn insert_clamped(list: &mut Vec<usize>, field: usize, index: Option<usize>) {
160 let at = index.unwrap_or(list.len()).min(list.len());
161 list.insert(at, field);
162}
163
164#[cfg(test)]
165#[allow(clippy::field_reassign_with_default)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn default_is_not_ready() {
171 let cfg = PivotConfig::default();
172 assert!(!cfg.is_ready());
173 assert_eq!(cfg.zone_of(0), None);
174 }
175
176 #[test]
177 fn ready_needs_value_and_one_axis() {
178 let mut cfg = PivotConfig::default();
179 cfg.value_field = Some(2);
180 assert!(!cfg.is_ready(), "value alone is not enough");
181 cfg.row_fields = vec![0];
182 assert!(cfg.is_ready());
183 cfg.row_fields.clear();
184 cfg.column_fields = vec![1];
185 assert!(cfg.is_ready());
186 }
187
188 #[test]
189 fn move_field_between_zones_is_exclusive() {
190 let mut cfg = PivotConfig::default();
191 cfg.move_field(3, PivotZone::Rows, None);
192 assert_eq!(cfg.zone_of(3), Some(PivotZone::Rows));
193 cfg.move_field(3, PivotZone::Columns, None);
194 assert_eq!(cfg.zone_of(3), Some(PivotZone::Columns));
195 assert!(cfg.row_fields.is_empty());
196 cfg.move_field(3, PivotZone::Filters, None);
197 assert_eq!(cfg.zone_of(3), Some(PivotZone::Filters));
198 assert!(cfg.column_fields.is_empty());
199 }
200
201 #[test]
202 fn values_zone_holds_exactly_one() {
203 let mut cfg = PivotConfig::default();
204 cfg.move_field(1, PivotZone::Values, None);
205 cfg.move_field(2, PivotZone::Values, None);
206 assert_eq!(cfg.value_field, Some(2));
207 assert_eq!(cfg.zone_of(1), None);
209 }
210
211 #[test]
212 fn move_field_respects_insertion_index() {
213 let mut cfg = PivotConfig::default();
214 cfg.move_field(1, PivotZone::Rows, None);
215 cfg.move_field(2, PivotZone::Rows, None);
216 cfg.move_field(3, PivotZone::Rows, Some(0));
217 assert_eq!(cfg.row_fields, vec![3, 1, 2]);
218 cfg.move_field(4, PivotZone::Rows, Some(99));
220 assert_eq!(cfg.row_fields, vec![3, 1, 2, 4]);
221 }
222
223 #[test]
224 fn reorder_within_same_zone() {
225 let mut cfg = PivotConfig::default();
226 cfg.row_fields = vec![1, 2, 3];
227 cfg.move_field(3, PivotZone::Rows, Some(0));
228 assert_eq!(cfg.row_fields, vec![3, 1, 2]);
229 }
230
231 #[test]
232 fn remove_field_clears_every_zone() {
233 let mut cfg = PivotConfig::default();
234 cfg.row_fields = vec![1];
235 cfg.value_field = Some(2);
236 cfg.remove_field(1);
237 cfg.remove_field(2);
238 assert!(cfg.row_fields.is_empty());
239 assert_eq!(cfg.value_field, None);
240 }
241
242 #[test]
243 fn clamp_to_columns_drops_out_of_range() {
244 let mut cfg = PivotConfig::default();
245 cfg.row_fields = vec![0, 5];
246 cfg.column_fields = vec![9];
247 cfg.filter_fields = vec![2, 7];
248 cfg.value_field = Some(6);
249 cfg.field_formats.insert(0, NumberFormat::default());
250 cfg.field_formats.insert(8, NumberFormat::default());
251 cfg.clamp_to_columns(4);
252 assert_eq!(cfg.row_fields, vec![0]);
253 assert!(cfg.column_fields.is_empty());
254 assert_eq!(cfg.filter_fields, vec![2]);
255 assert_eq!(cfg.value_field, None);
256 assert!(cfg.field_formats.contains_key(&0));
257 assert!(!cfg.field_formats.contains_key(&8));
258 }
259
260 #[test]
261 fn fields_in_reports_zone_contents() {
262 let mut cfg = PivotConfig::default();
263 cfg.row_fields = vec![4, 1];
264 cfg.value_field = Some(0);
265 assert_eq!(cfg.fields_in(PivotZone::Rows), vec![4, 1]);
266 assert_eq!(cfg.fields_in(PivotZone::Values), vec![0]);
267 assert!(cfg.fields_in(PivotZone::Columns).is_empty());
268 }
269}