1use crate::config::ResolvedColumnFormat;
11use crate::data::{compare_cells, CellValue, Column};
12use crate::format::format_cell;
13use crate::pivot::aggregation::Accumulator;
14use crate::pivot::config::PivotConfig;
15use std::collections::HashMap;
16
17pub const TOTAL_KEY: usize = usize::MAX;
20
21#[derive(Clone, Debug, PartialEq)]
23pub struct PivotNode {
24 pub label: String,
26 pub sort_key: CellValue,
29 pub depth: usize,
31 pub parent: Option<usize>,
33 pub children: Vec<usize>,
35 pub total: CellValue,
37}
38
39impl PivotNode {
40 #[must_use]
42 pub fn is_leaf(&self) -> bool {
43 self.children.is_empty()
44 }
45}
46
47#[derive(Clone, Debug, Default)]
50pub struct PivotResult {
51 pub row_nodes: Vec<PivotNode>,
53 pub col_nodes: Vec<PivotNode>,
55 pub row_roots: Vec<usize>,
57 pub col_roots: Vec<usize>,
59 pub values: HashMap<(usize, usize), CellValue>,
63 pub grand_total: CellValue,
65 pub row_depth: usize,
67 pub col_depth: usize,
69 pub row_field_names: Vec<String>,
71 pub col_field_names: Vec<String>,
73 pub value_caption: String,
75 pub source_row_count: usize,
77}
78
79impl PivotResult {
80 #[must_use]
83 pub fn value(&self, row_key: usize, col_key: usize) -> &CellValue {
84 self.values
85 .get(&(row_key, col_key))
86 .unwrap_or(&CellValue::None)
87 }
88
89 #[must_use]
91 pub fn row_leaves(&self) -> Vec<usize> {
92 collect_leaves(&self.row_nodes, &self.row_roots)
93 }
94
95 #[must_use]
97 pub fn col_leaves(&self) -> Vec<usize> {
98 collect_leaves(&self.col_nodes, &self.col_roots)
99 }
100}
101
102fn collect_leaves(nodes: &[PivotNode], roots: &[usize]) -> Vec<usize> {
103 let mut out = Vec::new();
104 let mut stack: Vec<usize> = roots.iter().rev().copied().collect();
105 while let Some(id) = stack.pop() {
106 let node = &nodes[id];
107 if node.is_leaf() {
108 out.push(id);
109 } else {
110 for &child in node.children.iter().rev() {
111 stack.push(child);
112 }
113 }
114 }
115 out
116}
117
118struct AxisBuilder {
120 nodes: Vec<PivotNode>,
121 roots: Vec<usize>,
122 index: HashMap<(usize, String), usize>,
124}
125
126impl AxisBuilder {
127 fn new() -> Self {
128 Self {
129 nodes: Vec::new(),
130 roots: Vec::new(),
131 index: HashMap::new(),
132 }
133 }
134
135 fn path_for_row(
139 &mut self,
140 row: &[CellValue],
141 fields: &[usize],
142 formats: &[ResolvedColumnFormat],
143 blank_label: &str,
144 ) -> Vec<usize> {
145 let mut path = Vec::with_capacity(fields.len());
146 let mut parent_key = TOTAL_KEY;
147 for (depth, &field) in fields.iter().enumerate() {
148 let cell = row.get(field).unwrap_or(&CellValue::None);
149 let label = if matches!(cell, CellValue::None) {
150 blank_label.to_owned()
151 } else {
152 format_cell(cell, &formats[field]).0
153 };
154 let id = match self.index.get(&(parent_key, label.clone())) {
155 Some(&id) => id,
156 None => {
157 let id = self.nodes.len();
158 self.nodes.push(PivotNode {
159 label: label.clone(),
160 sort_key: cell.clone(),
161 depth,
162 parent: (parent_key != TOTAL_KEY).then_some(parent_key),
163 children: Vec::new(),
164 total: CellValue::None,
165 });
166 self.index.insert((parent_key, label), id);
167 if parent_key == TOTAL_KEY {
168 self.roots.push(id);
169 } else {
170 self.nodes[parent_key].children.push(id);
171 }
172 id
173 }
174 };
175 path.push(id);
176 parent_key = id;
177 }
178 path
179 }
180
181 fn sort_canonical(&mut self) {
184 let keys: Vec<CellValue> = self.nodes.iter().map(|n| n.sort_key.clone()).collect();
185 let by_key = |a: &usize, b: &usize| compare_cells(&keys[*a], &keys[*b]);
186 self.roots.sort_by(by_key);
187 for node in &mut self.nodes {
188 node.children.sort_by(by_key);
189 }
190 }
191}
192
193#[must_use]
203pub fn compute_pivot(
204 columns: &[Column],
205 rows: &[Vec<CellValue>],
206 source_rows: &[usize],
207 config: &PivotConfig,
208 formats: &[ResolvedColumnFormat],
209) -> PivotResult {
210 let mut row_axis = AxisBuilder::new();
211 let mut col_axis = AxisBuilder::new();
212 let mut accs: HashMap<(usize, usize), Accumulator> = HashMap::new();
213
214 let value_field = config.value_field;
215 let agg = config.aggregation;
216
217 for &row_idx in source_rows {
218 let Some(row) = rows.get(row_idx) else {
219 continue;
220 };
221 let row_path = row_axis.path_for_row(row, &config.row_fields, formats, &config.blank_label);
222 let col_path =
223 col_axis.path_for_row(row, &config.column_fields, formats, &config.blank_label);
224 let Some(vf) = value_field else {
225 continue;
226 };
227 let value = row.get(vf).unwrap_or(&CellValue::None);
228 for &rk in row_path.iter().chain(std::iter::once(&TOTAL_KEY)) {
231 for &ck in col_path.iter().chain(std::iter::once(&TOTAL_KEY)) {
232 accs.entry((rk, ck))
233 .or_insert_with(|| Accumulator::new(agg))
234 .ingest(value);
235 }
236 }
237 }
238
239 row_axis.sort_canonical();
240 col_axis.sort_canonical();
241
242 let values: HashMap<(usize, usize), CellValue> =
243 accs.iter().map(|(k, acc)| (*k, acc.finish())).collect();
244
245 let mut row_nodes = row_axis.nodes;
246 let mut col_nodes = col_axis.nodes;
247 for (id, node) in row_nodes.iter_mut().enumerate() {
248 node.total = values
249 .get(&(id, TOTAL_KEY))
250 .cloned()
251 .unwrap_or(CellValue::None);
252 }
253 for (id, node) in col_nodes.iter_mut().enumerate() {
254 node.total = values
255 .get(&(TOTAL_KEY, id))
256 .cloned()
257 .unwrap_or(CellValue::None);
258 }
259 let grand_total = values
260 .get(&(TOTAL_KEY, TOTAL_KEY))
261 .cloned()
262 .unwrap_or(CellValue::None);
263
264 let field_name = |idx: usize| {
265 columns
266 .get(idx)
267 .map(|c| c.name.clone())
268 .unwrap_or_else(|| format!("column {idx}"))
269 };
270 let value_caption = match value_field {
271 Some(vf) => agg.caption(&field_name(vf)),
272 None => "Values".to_owned(),
273 };
274
275 PivotResult {
276 row_roots: row_axis.roots,
277 col_roots: col_axis.roots,
278 row_nodes,
279 col_nodes,
280 values,
281 grand_total,
282 row_depth: config.row_fields.len(),
283 col_depth: config.column_fields.len(),
284 row_field_names: config.row_fields.iter().map(|&f| field_name(f)).collect(),
285 col_field_names: config
286 .column_fields
287 .iter()
288 .map(|&f| field_name(f))
289 .collect(),
290 value_caption,
291 source_row_count: source_rows.len(),
292 }
293}
294
295#[cfg(test)]
296#[allow(clippy::unwrap_used)]
297mod tests {
298 use super::*;
299 use crate::config::GridConfig;
300 use crate::data::ColumnKind;
301 use crate::pivot::aggregation::AggregationFn;
302 use CellValue::{Decimal, Integer, None as Null, Text};
303
304 fn fixture() -> (Vec<Column>, Vec<Vec<CellValue>>, Vec<ResolvedColumnFormat>) {
306 let columns = vec![
307 Column::new("region", ColumnKind::Text, 100.0),
308 Column::new("product", ColumnKind::Text, 100.0),
309 Column::new("year", ColumnKind::Integer, 80.0),
310 Column::new("amount", ColumnKind::Decimal, 100.0),
311 ];
312 let r = |region: &str, product: &str, year: i64, amount: f64| {
313 vec![
314 Text(region.into()),
315 Text(product.into()),
316 Integer(year),
317 Decimal(amount),
318 ]
319 };
320 let rows = vec![
321 r("Europe", "Widget", 2023, 10.0),
322 r("Europe", "Widget", 2024, 20.0),
323 r("Europe", "Gadget", 2023, 5.0),
324 r("Asia", "Widget", 2023, 7.0),
325 r("Asia", "Gadget", 2024, 3.0),
326 r("Asia", "Gadget", 2024, 4.0),
327 ];
328 let formats = GridConfig::default().resolve_all(&columns);
329 (columns, rows, formats)
330 }
331
332 fn all_rows(rows: &[Vec<CellValue>]) -> Vec<usize> {
333 (0..rows.len()).collect()
334 }
335
336 fn config(rows: &[usize], cols: &[usize], value: usize, agg: AggregationFn) -> PivotConfig {
337 PivotConfig {
338 row_fields: rows.to_vec(),
339 column_fields: cols.to_vec(),
340 value_field: Some(value),
341 aggregation: agg,
342 ..PivotConfig::default()
343 }
344 }
345
346 fn node_by_label<'a>(result: &'a PivotResult, label: &str) -> (usize, &'a PivotNode) {
347 result
348 .row_nodes
349 .iter()
350 .enumerate()
351 .find(|(_, n)| n.label == label)
352 .unwrap()
353 }
354
355 #[test]
356 fn single_row_and_column_field_sum() {
357 let (columns, rows, formats) = fixture();
358 let cfg = config(&[0], &[2], 3, AggregationFn::Sum);
359 let result = compute_pivot(&columns, &rows, &all_rows(&rows), &cfg, &formats);
360
361 assert_eq!(result.row_roots.len(), 2); assert_eq!(result.col_roots.len(), 2); assert_eq!(result.row_nodes[result.row_roots[0]].label, "Asia");
365 assert_eq!(result.row_nodes[result.row_roots[1]].label, "Europe");
366
367 let (europe, europe_node) = node_by_label(&result, "Europe");
368 let y2023 = result.col_roots[0];
369 let y2024 = result.col_roots[1];
370 assert_eq!(result.value(europe, y2023), &Decimal(15.0));
371 assert_eq!(result.value(europe, y2024), &Decimal(20.0));
372 assert_eq!(europe_node.total, Decimal(35.0));
373
374 let (asia, asia_node) = node_by_label(&result, "Asia");
375 assert_eq!(result.value(asia, y2023), &Decimal(7.0));
376 assert_eq!(result.value(asia, y2024), &Decimal(7.0));
377 assert_eq!(asia_node.total, Decimal(14.0));
378
379 assert_eq!(result.col_nodes[y2023].total, Decimal(22.0));
381 assert_eq!(result.col_nodes[y2024].total, Decimal(27.0));
382 assert_eq!(result.grand_total, Decimal(49.0));
383 }
384
385 #[test]
386 fn two_row_fields_build_two_level_tree_with_subtotals() {
387 let (columns, rows, formats) = fixture();
388 let cfg = config(&[0, 1], &[], 3, AggregationFn::Sum);
389 let result = compute_pivot(&columns, &rows, &all_rows(&rows), &cfg, &formats);
390
391 assert_eq!(result.row_depth, 2);
392 let (europe, europe_node) = node_by_label(&result, "Europe");
393 assert_eq!(europe_node.depth, 0);
394 assert_eq!(europe_node.children.len(), 2); let child_labels: Vec<&str> = europe_node
396 .children
397 .iter()
398 .map(|&c| result.row_nodes[c].label.as_str())
399 .collect();
400 assert_eq!(child_labels, vec!["Gadget", "Widget"]);
401 assert_eq!(europe_node.total, Decimal(35.0));
403 let widget = europe_node
405 .children
406 .iter()
407 .copied()
408 .find(|&c| result.row_nodes[c].label == "Widget")
409 .unwrap();
410 assert_eq!(result.value(widget, TOTAL_KEY), &Decimal(30.0));
411 assert_eq!(result.row_nodes[widget].parent, Some(europe));
412 let leaves = result.row_leaves();
414 let leaf_labels: Vec<&str> = leaves
415 .iter()
416 .map(|&l| result.row_nodes[l].label.as_str())
417 .collect();
418 assert_eq!(leaf_labels, vec!["Gadget", "Widget", "Gadget", "Widget"]);
419 }
420
421 #[test]
422 fn count_and_avg_levels_are_computed_from_source_not_from_child_aggregates() {
423 let (columns, rows, formats) = fixture();
424 let cfg = config(&[0], &[], 3, AggregationFn::Avg);
425 let result = compute_pivot(&columns, &rows, &all_rows(&rows), &cfg, &formats);
426 let (_, europe) = node_by_label(&result, "Europe");
427 match &europe.total {
429 Decimal(v) => assert!((v - 35.0 / 3.0).abs() < 1e-9),
430 other => panic!("expected Decimal, got {other:?}"),
431 }
432 match &result.grand_total {
433 Decimal(v) => assert!((v - 49.0 / 6.0).abs() < 1e-9),
434 other => panic!("expected Decimal, got {other:?}"),
435 }
436 }
437
438 #[test]
439 fn count_aggregation_reports_integers() {
440 let (columns, rows, formats) = fixture();
441 let cfg = config(&[0], &[2], 3, AggregationFn::Count);
442 let result = compute_pivot(&columns, &rows, &all_rows(&rows), &cfg, &formats);
443 let (asia, _) = node_by_label(&result, "Asia");
444 let y2024 = result.col_roots[1];
445 assert_eq!(result.value(asia, y2024), &Integer(2));
446 assert_eq!(result.grand_total, Integer(6));
447 }
448
449 #[test]
450 fn min_max_aggregation_on_decimal_values() {
451 let (columns, rows, formats) = fixture();
452
453 let cfg = config(&[0], &[2], 3, AggregationFn::Min);
454 let result = compute_pivot(&columns, &rows, &all_rows(&rows), &cfg, &formats);
455 let (europe, europe_node) = node_by_label(&result, "Europe");
456 let y2023 = result.col_roots[0];
457 assert_eq!(result.value(europe, y2023), &Decimal(5.0));
459 assert_eq!(europe_node.total, Decimal(5.0));
460 assert_eq!(result.grand_total, Decimal(3.0));
461
462 let cfg = config(&[0], &[2], 3, AggregationFn::Max);
463 let result = compute_pivot(&columns, &rows, &all_rows(&rows), &cfg, &formats);
464 let (europe, europe_node) = node_by_label(&result, "Europe");
465 assert_eq!(result.value(europe, y2023), &Decimal(10.0));
466 assert_eq!(europe_node.total, Decimal(20.0));
467 assert_eq!(result.grand_total, Decimal(20.0));
468 }
469
470 #[test]
471 fn empty_source_produces_empty_result_without_panic() {
472 let (columns, _, formats) = fixture();
473 let rows: Vec<Vec<CellValue>> = vec![];
474 let cfg = config(&[0], &[2], 3, AggregationFn::Sum);
475 let result = compute_pivot(&columns, &rows, &[], &cfg, &formats);
476 assert!(result.row_roots.is_empty());
477 assert!(result.col_roots.is_empty());
478 assert_eq!(result.grand_total, Null);
479 assert_eq!(result.source_row_count, 0);
480 }
481
482 #[test]
483 fn null_grouping_values_bucket_under_blank_label() {
484 let (columns, mut rows, formats) = fixture();
485 rows.push(vec![
486 Null,
487 Text("Widget".into()),
488 Integer(2023),
489 Decimal(2.0),
490 ]);
491 rows.push(vec![
492 Null,
493 Text("Gadget".into()),
494 Integer(2023),
495 Decimal(3.0),
496 ]);
497 let cfg = config(&[0], &[], 3, AggregationFn::Sum);
498 let result = compute_pivot(&columns, &rows, &all_rows(&rows), &cfg, &formats);
499 let (blank, blank_node) = node_by_label(&result, "(blank)");
500 assert_eq!(blank_node.sort_key, Null);
501 assert_eq!(result.value(blank, TOTAL_KEY), &Decimal(5.0));
502 assert_eq!(result.row_roots[0], blank);
504 }
505
506 #[test]
507 fn source_row_subset_excludes_filtered_rows() {
508 let (columns, rows, formats) = fixture();
509 let cfg = config(&[0], &[], 3, AggregationFn::Sum);
510 let result = compute_pivot(&columns, &rows, &[3, 4, 5], &cfg, &formats);
512 assert_eq!(result.row_roots.len(), 1);
513 assert_eq!(result.row_nodes[result.row_roots[0]].label, "Asia");
514 assert_eq!(result.grand_total, Decimal(14.0));
515 assert_eq!(result.source_row_count, 3);
516 }
517
518 #[test]
519 fn missing_intersections_read_as_none() {
520 let (columns, rows, formats) = fixture();
521 let cfg = config(&[0, 1], &[2], 3, AggregationFn::Sum);
522 let result = compute_pivot(&columns, &rows, &all_rows(&rows), &cfg, &formats);
523 let (asia, asia_node) = node_by_label(&result, "Asia");
525 let widget = asia_node
526 .children
527 .iter()
528 .copied()
529 .find(|&c| result.row_nodes[c].label == "Widget")
530 .unwrap();
531 let y2024 = result.col_roots[1];
532 assert_eq!(result.value(widget, y2024), &Null);
533 assert_eq!(result.row_nodes[widget].parent, Some(asia));
534 }
535
536 #[test]
537 fn value_caption_reflects_aggregation_and_field() {
538 let (columns, rows, formats) = fixture();
539 let cfg = config(&[0], &[], 3, AggregationFn::Sum);
540 let result = compute_pivot(&columns, &rows, &all_rows(&rows), &cfg, &formats);
541 assert_eq!(result.value_caption, "Sum of amount");
542 assert_eq!(result.row_field_names, vec!["region".to_owned()]);
543 }
544
545 #[test]
546 fn source_rows_are_not_mutated() {
547 let (columns, rows, formats) = fixture();
548 let snapshot = rows.clone();
549 let cfg = config(&[0, 1], &[2], 3, AggregationFn::Avg);
550 let _ = compute_pivot(&columns, &rows, &all_rows(&rows), &cfg, &formats);
551 assert_eq!(rows, snapshot);
552 }
553
554 #[test]
555 fn boolean_and_date_grouping_fields_work() {
556 let columns = vec![
557 Column::new("flag", ColumnKind::Boolean, 80.0),
558 Column::new("when", ColumnKind::Date, 120.0),
559 Column::new("n", ColumnKind::Integer, 80.0),
560 ];
561 let rows = vec![
562 vec![CellValue::Boolean(true), CellValue::Date(0), Integer(1)],
563 vec![CellValue::Boolean(false), CellValue::Date(0), Integer(2)],
564 vec![
565 CellValue::Boolean(true),
566 CellValue::Date(86_400),
567 Integer(3),
568 ],
569 ];
570 let formats = GridConfig::default().resolve_all(&columns);
571 let cfg = config(&[0], &[1], 2, AggregationFn::Sum);
572 let result = compute_pivot(&columns, &rows, &all_rows(&rows), &cfg, &formats);
573 assert_eq!(result.row_roots.len(), 2);
574 assert_eq!(result.col_roots.len(), 2);
575 assert_eq!(result.col_nodes[result.col_roots[0]].label, "1970-01-01");
577 let t = result
578 .row_roots
579 .iter()
580 .copied()
581 .find(|&r| result.row_nodes[r].label == "true")
582 .unwrap();
583 assert_eq!(result.row_nodes[t].total, Integer(4));
584 }
585}