use crate::config::NumberFormat;
use crate::pivot::aggregation::AggregationFn;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PivotZone {
Rows,
Columns,
Values,
Filters,
}
#[derive(Clone, Debug, PartialEq)]
pub struct PivotConfig {
pub row_fields: Vec<usize>,
pub column_fields: Vec<usize>,
pub value_field: Option<usize>,
pub aggregation: AggregationFn,
pub filter_fields: Vec<usize>,
pub show_row_subtotals: bool,
pub show_column_subtotals: bool,
pub show_row_grand_total: bool,
pub show_column_grand_total: bool,
pub blank_label: String,
pub value_format: Option<NumberFormat>,
}
impl Default for PivotConfig {
fn default() -> Self {
Self {
row_fields: vec![],
column_fields: vec![],
value_field: None,
aggregation: AggregationFn::default(),
filter_fields: vec![],
show_row_subtotals: true,
show_column_subtotals: false,
show_row_grand_total: true,
show_column_grand_total: true,
blank_label: "(blank)".into(),
value_format: None,
}
}
}
impl PivotConfig {
#[must_use]
pub fn is_ready(&self) -> bool {
self.value_field.is_some()
&& (!self.row_fields.is_empty() || !self.column_fields.is_empty())
}
#[must_use]
pub fn zone_of(&self, field: usize) -> Option<PivotZone> {
if self.row_fields.contains(&field) {
Some(PivotZone::Rows)
} else if self.column_fields.contains(&field) {
Some(PivotZone::Columns)
} else if self.value_field == Some(field) {
Some(PivotZone::Values)
} else if self.filter_fields.contains(&field) {
Some(PivotZone::Filters)
} else {
None
}
}
#[must_use]
pub fn fields_in(&self, zone: PivotZone) -> Vec<usize> {
match zone {
PivotZone::Rows => self.row_fields.clone(),
PivotZone::Columns => self.column_fields.clone(),
PivotZone::Values => self.value_field.into_iter().collect(),
PivotZone::Filters => self.filter_fields.clone(),
}
}
pub fn move_field(&mut self, field: usize, zone: PivotZone, index: Option<usize>) {
self.remove_field(field);
match zone {
PivotZone::Rows => insert_clamped(&mut self.row_fields, field, index),
PivotZone::Columns => insert_clamped(&mut self.column_fields, field, index),
PivotZone::Values => self.value_field = Some(field),
PivotZone::Filters => insert_clamped(&mut self.filter_fields, field, index),
}
}
pub fn remove_field(&mut self, field: usize) {
self.row_fields.retain(|&f| f != field);
self.column_fields.retain(|&f| f != field);
self.filter_fields.retain(|&f| f != field);
if self.value_field == Some(field) {
self.value_field = None;
}
}
pub fn clamp_to_columns(&mut self, column_count: usize) {
self.row_fields.retain(|&f| f < column_count);
self.column_fields.retain(|&f| f < column_count);
self.filter_fields.retain(|&f| f < column_count);
if self.value_field.is_some_and(|f| f >= column_count) {
self.value_field = None;
}
}
}
fn insert_clamped(list: &mut Vec<usize>, field: usize, index: Option<usize>) {
let at = index.unwrap_or(list.len()).min(list.len());
list.insert(at, field);
}
#[cfg(test)]
#[allow(clippy::field_reassign_with_default)]
mod tests {
use super::*;
#[test]
fn default_is_not_ready() {
let cfg = PivotConfig::default();
assert!(!cfg.is_ready());
assert_eq!(cfg.zone_of(0), None);
}
#[test]
fn ready_needs_value_and_one_axis() {
let mut cfg = PivotConfig::default();
cfg.value_field = Some(2);
assert!(!cfg.is_ready(), "value alone is not enough");
cfg.row_fields = vec![0];
assert!(cfg.is_ready());
cfg.row_fields.clear();
cfg.column_fields = vec![1];
assert!(cfg.is_ready());
}
#[test]
fn move_field_between_zones_is_exclusive() {
let mut cfg = PivotConfig::default();
cfg.move_field(3, PivotZone::Rows, None);
assert_eq!(cfg.zone_of(3), Some(PivotZone::Rows));
cfg.move_field(3, PivotZone::Columns, None);
assert_eq!(cfg.zone_of(3), Some(PivotZone::Columns));
assert!(cfg.row_fields.is_empty());
cfg.move_field(3, PivotZone::Filters, None);
assert_eq!(cfg.zone_of(3), Some(PivotZone::Filters));
assert!(cfg.column_fields.is_empty());
}
#[test]
fn values_zone_holds_exactly_one() {
let mut cfg = PivotConfig::default();
cfg.move_field(1, PivotZone::Values, None);
cfg.move_field(2, PivotZone::Values, None);
assert_eq!(cfg.value_field, Some(2));
assert_eq!(cfg.zone_of(1), None);
}
#[test]
fn move_field_respects_insertion_index() {
let mut cfg = PivotConfig::default();
cfg.move_field(1, PivotZone::Rows, None);
cfg.move_field(2, PivotZone::Rows, None);
cfg.move_field(3, PivotZone::Rows, Some(0));
assert_eq!(cfg.row_fields, vec![3, 1, 2]);
cfg.move_field(4, PivotZone::Rows, Some(99));
assert_eq!(cfg.row_fields, vec![3, 1, 2, 4]);
}
#[test]
fn reorder_within_same_zone() {
let mut cfg = PivotConfig::default();
cfg.row_fields = vec![1, 2, 3];
cfg.move_field(3, PivotZone::Rows, Some(0));
assert_eq!(cfg.row_fields, vec![3, 1, 2]);
}
#[test]
fn remove_field_clears_every_zone() {
let mut cfg = PivotConfig::default();
cfg.row_fields = vec![1];
cfg.value_field = Some(2);
cfg.remove_field(1);
cfg.remove_field(2);
assert!(cfg.row_fields.is_empty());
assert_eq!(cfg.value_field, None);
}
#[test]
fn clamp_to_columns_drops_out_of_range() {
let mut cfg = PivotConfig::default();
cfg.row_fields = vec![0, 5];
cfg.column_fields = vec![9];
cfg.filter_fields = vec![2, 7];
cfg.value_field = Some(6);
cfg.clamp_to_columns(4);
assert_eq!(cfg.row_fields, vec![0]);
assert!(cfg.column_fields.is_empty());
assert_eq!(cfg.filter_fields, vec![2]);
assert_eq!(cfg.value_field, None);
}
#[test]
fn fields_in_reports_zone_contents() {
let mut cfg = PivotConfig::default();
cfg.row_fields = vec![4, 1];
cfg.value_field = Some(0);
assert_eq!(cfg.fields_in(PivotZone::Rows), vec![4, 1]);
assert_eq!(cfg.fields_in(PivotZone::Values), vec![0]);
assert!(cfg.fields_in(PivotZone::Columns).is_empty());
}
}