From: Ben Pfaff Date: Thu, 13 Mar 2025 09:17:45 +0000 (-0700) Subject: work on pivot table builder X-Git-Url: https://pintos-os.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=2b1528e1494886a232ad928fdeaa1d4af99eecca;p=pspp work on pivot table builder --- diff --git a/rust/pspp/src/output/pivot/mod.rs b/rust/pspp/src/output/pivot/mod.rs index 8209a010fc..7ea269e88d 100644 --- a/rust/pspp/src/output/pivot/mod.rs +++ b/rust/pspp/src/output/pivot/mod.rs @@ -72,6 +72,7 @@ use color::{palette::css::TRANSPARENT, AlphaColor, Rgba8, Srgb}; use encoding_rs::UTF_8; use enum_iterator::Sequence; use enum_map::{enum_map, Enum, EnumMap}; +use look_xml::TableProperties; use quick_xml::{de::from_str, DeError}; use serde::{de::Visitor, Deserialize}; use smallstr::SmallString; @@ -89,8 +90,9 @@ use crate::{ pub mod output; mod look_xml; +#[cfg(test)] +mod test; mod tlo; -pub use look_xml::TableProperties; /// Areas of a pivot table for styling purposes. #[derive(Copy, Clone, Debug, Default, Enum, PartialEq, Eq)] @@ -231,7 +233,7 @@ pub struct Sizing { keeps: Vec>, } -#[derive(Copy, Clone, Debug, Enum, Sequence)] +#[derive(Copy, Clone, Debug, Enum, PartialEq, Eq, Sequence)] pub enum Axis3 { X, Y, @@ -317,9 +319,17 @@ impl Axis { /// data.) #[derive(Clone, Debug)] pub struct Dimension { - axis_type: Axis3, + /// Our axis within the pivot table. + axis: Axis3, + + /// The index for this dimension within its axis. + /// + /// This [Dimension] is `pivot_table.axes[axis].dimensions[level]`. level: usize, + /// The index within the dimension. + /// + /// This [Dimension] is `pivot_table.dimensions[top_index]`. top_index: usize, /// Hierarchy of categories within the dimension. The groups and categories @@ -329,7 +339,7 @@ pub struct Dimension { /// /// The root must always be a group, although it is allowed to have no /// subcategories. - root: Group, + pub root: Arc, /// All of the leaves reachable via the root. /// @@ -376,11 +386,232 @@ pub struct Group { children: Vec, /// Display a label for the group itself? - show_label: bool, + pub show_label: bool, show_label_in_corner: bool, } +#[derive(Clone)] +pub struct DimensionBuilder { + axis: Axis3, + root: GroupBuilder, + len: usize, + hide_all_labels: bool, +} + +impl DimensionBuilder { + pub fn new(axis: Axis3, root: GroupBuilder) -> Self { + let len = root.len(); + Self { + axis, + root, + len, + hide_all_labels: false, + } + } + pub fn with_all_labels_hidden(mut self) -> Self { + self.hide_all_labels = true; + self + } + fn build(self, level: usize, top_index: usize, dimension_labels_in_corner: bool) -> Dimension { + let mut leaves = Vec::with_capacity(self.len); + let root = self + .root + .build(dimension_labels_in_corner, None, &mut leaves); + Dimension { + axis: self.axis, + level, + top_index, + root, + data_leaves: leaves.clone(), + presentation_leaves: leaves, + hide_all_labels: self.hide_all_labels, + label_depth: 0, + } + } +} + +#[derive(Clone)] +pub struct GroupBuilder { + name: Box, + children: Vec, + show_label: bool, +} + +impl GroupBuilder { + pub fn new(name: Value) -> Self { + Self { + name: Box::new(name), + children: Vec::new(), + show_label: true, + } + } + pub fn push(&mut self, value: T) + where + T: Into, + { + self.children.push(value.into()); + } + pub fn with(mut self, value: T) -> Self + where + T: Into, + { + self.push(value); + self + } + pub fn with_label_hidden(mut self) -> Self { + self.show_label = false; + self + } + fn len(&self) -> usize { + self.children.iter().map(|category| category.len()).sum() + } + fn build( + self, + dimension_labels_in_corner: bool, + parent: Option>, + leaves: &mut Vec>, + ) -> Arc { + Arc::new_cyclic(|weak| Group { + parent, + name: self.name, + label_depth: 0, + extra_depth: 0, + children: self + .children + .into_iter() + .enumerate() + .map(|(group_index, c)| { + c.build( + dimension_labels_in_corner, + weak.clone(), + group_index, + leaves, + ) + }) + .collect(), + show_label: self.show_label, + show_label_in_corner: self.show_label && dimension_labels_in_corner, + }) + } +} + +#[derive(Clone)] +pub enum CategoryBuilder { + Group(Box), + Leaf { + name: Box, + class: Option, + }, +} + +impl CategoryBuilder { + fn len(&self) -> usize { + match self { + CategoryBuilder::Group(group) => group.len(), + CategoryBuilder::Leaf { .. } => 1, + } + } + fn build( + self, + dimension_labels_in_corner: bool, + parent: Weak, + group_index: usize, + leaves: &mut Vec>, + ) -> Category { + match self { + Self::Group(group) => { + Category::Group(group.build(dimension_labels_in_corner, Some(parent), leaves)) + } + Self::Leaf { name, class } => { + let leaf = Arc::new(Leaf { + parent, + name, + label_depth: 0, + extra_depth: 0, + group_index, + data_index: leaves.len(), + presentation_index: leaves.len(), + class, + }); + leaves.push(leaf.clone()); + Category::Leaf(leaf) + } + } + } +} + +impl From for CategoryBuilder { + fn from(name: Value) -> Self { + Self::Leaf { + name: Box::new(name), + class: None, + } + } +} + +impl From<(Value, Class)> for CategoryBuilder { + fn from((name, class): (Value, Class)) -> Self { + Self::Leaf { + name: Box::new(name), + class: Some(class), + } + } +} + +pub struct PivotTableBuilder { + look: Arc, + title: Box, + dimensions: Vec, + cells: HashMap, +} + +impl PivotTableBuilder { + pub fn new(title: Value, dimensions: &[DimensionBuilder]) -> Self { + Self { + look: Settings::global().look.clone(), + title: Box::new(title), + dimensions: dimensions.to_vec(), + cells: HashMap::new(), + } + } + pub fn with_look(mut self, look: Arc) -> Self { + self.look = look; + self + } + pub fn insert(&mut self, data_indexes: &[usize], value: Value) { + self.cells.insert( + cell_index(data_indexes, self.dimensions.iter().map(|d| d.len)), + value, + ); + } + pub fn build(self) -> PivotTable { + let row_label_position = self.look.row_label_position; + let corner_text = false; + let mut table = PivotTable::new(self.title, self.look.clone()); + let mut dimensions = Vec::with_capacity(self.dimensions.len()); + let mut axes = EnumMap::from_fn(|_key| Vec::with_capacity(self.dimensions.len())); + for (top_index, d) in self.dimensions.into_iter().enumerate() { + let axis = d.axis; + let d = Arc::new(d.build( + axes[axis].len(), + top_index, + axis == Axis3::Y && row_label_position == RowLabelPosition::Corner && !corner_text, + )); + axes[d.axis].push(d.clone()); + dimensions.push(d); + } + table.dimensions = dimensions; + table.axes = axes.map(|_axis, dimensions| Axis { + dimensions, + extent: 0, + label_depth: 0, + }); + table.cells = self.cells; + table + } +} + #[derive(Clone, Debug)] pub struct Leaf { parent: Weak, @@ -388,15 +619,49 @@ pub struct Leaf { label_depth: usize, extra_depth: usize, + /// `parent.children[group_index]` is this. group_index: usize, data_index: usize, presentation_index: usize, /// Default format for values in this category. - format: Format, + class: Option, +} + +impl Leaf { + pub fn new(name: Value) -> Self { + Self { + parent: Weak::new(), + name: Box::new(name), + label_depth: 0, + extra_depth: 0, + group_index: 0, + data_index: 0, + presentation_index: 0, + class: None, + } + } + pub fn with_class(self, class: Class) -> Self { + Self { + class: Some(class), + ..self + } + } +} - /// Honor [Table]'s `small` setting? - honor_small: bool, +/// Pivot result classes. +/// +/// These are used to mark [Leaf] categories as having particular types of data, +/// to set their numeric formats. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Class { + Other, + Integer, + Correlations, + Significance, + Percent, + Residual, + Count, } /// A pivot_category is a leaf (a category) or a group. @@ -636,7 +901,7 @@ pub enum RowLabelPosition { Nested, #[default] - InCorner, + Corner, } /// The heading region of a rendered pivot table: @@ -802,24 +1067,6 @@ impl FromStr for Color { } } -#[cfg(test)] -mod test { - use crate::output::pivot::Color; - - #[test] - fn color() { - assert_eq!("#112233".parse(), Ok(Color::new(0x11, 0x22, 0x33))); - assert_eq!("112233".parse(), Ok(Color::new(0x11, 0x22, 0x33))); - assert_eq!("rgb(11,22,33)".parse(), Ok(Color::new(11, 22, 33))); - assert_eq!( - "rgba(11,22,33, 0.25)".parse(), - Ok(Color::new(11, 22, 33).with_alpha(64)) - ); - assert_eq!("lavender".parse(), Ok(Color::new(230, 230, 250))); - assert_eq!("transparent".parse(), Ok(Color::new(0, 0, 0).with_alpha(0))); - } -} - impl<'de> Deserialize<'de> for Color { fn deserialize(deserializer: D) -> Result where @@ -1170,8 +1417,8 @@ pub struct PivotTable { pub cells: HashMap, } -impl PivotTable { - fn new() -> Self { +impl Default for PivotTable { + fn default() -> Self { Self { look: Look::shared_default(), rotate_inner_column_labels: false, @@ -1205,15 +1452,30 @@ impl PivotTable { cells: HashMap::new(), } } +} + +fn cell_index(data_indexes: &[usize], dimensions: I) -> usize +where + I: ExactSizeIterator, +{ + debug_assert_eq!(data_indexes.len(), dimensions.len()); + let mut index = 0; + for (dimension, data_index) in dimensions.zip(data_indexes.iter()) { + debug_assert!(*data_index < dimension); + index = dimension * index + data_index; + } + index +} +impl PivotTable { + fn new(title: Box, look: Arc) -> Self { + let mut this = Self::default(); + this.title = Some(title); + this.look = look; + this + } fn cell_index(&self, data_indexes: &[usize]) -> usize { - debug_assert_eq!(data_indexes.len(), self.dimensions.len()); - let mut index = 0; - for (dimension, data_index) in self.dimensions.iter().zip(data_indexes.iter()) { - debug_assert!(*data_index < dimension.len()); - index = dimension.len() * index + data_index; - } - index + cell_index(data_indexes, self.dimensions.iter().map(|d| d.len())) } fn insert(&mut self, data_indexes: &[usize], value: Value) { @@ -1371,6 +1633,9 @@ pub struct Value { } impl Value { + pub fn new_text(s: impl Into) -> Self { + Self::new_user_text(s) + } pub fn new_user_text(s: impl Into) -> Self { let s: String = s.into(); Self { diff --git a/rust/pspp/src/output/pivot/output.rs b/rust/pspp/src/output/pivot/output.rs index c15b569d01..2c1dd2b9de 100644 --- a/rust/pspp/src/output/pivot/output.rs +++ b/rust/pspp/src/output/pivot/output.rs @@ -220,7 +220,7 @@ impl PivotTable { } } if (self.corner_text.is_some() - || self.look.row_label_position == RowLabelPosition::InCorner) + || self.look.row_label_position == RowLabelPosition::Corner) && stub.x() > 0 && stub.y() > 0 { diff --git a/rust/pspp/src/output/pivot/test.rs b/rust/pspp/src/output/pivot/test.rs new file mode 100644 index 0000000000..3446f56b2b --- /dev/null +++ b/rust/pspp/src/output/pivot/test.rs @@ -0,0 +1,27 @@ +use crate::output::pivot::Color; + +use super::{Axis3, DimensionBuilder, GroupBuilder, PivotTableBuilder, Value}; + +#[test] +fn color() { + assert_eq!("#112233".parse(), Ok(Color::new(0x11, 0x22, 0x33))); + assert_eq!("112233".parse(), Ok(Color::new(0x11, 0x22, 0x33))); + assert_eq!("rgb(11,22,33)".parse(), Ok(Color::new(11, 22, 33))); + assert_eq!( + "rgba(11,22,33, 0.25)".parse(), + Ok(Color::new(11, 22, 33).with_alpha(64)) + ); + assert_eq!("lavender".parse(), Ok(Color::new(230, 230, 250))); + assert_eq!("transparent".parse(), Ok(Color::new(0, 0, 0).with_alpha(0))); +} + +#[test] +fn pivot_table_1d() { + let mut group = GroupBuilder::new(Value::new_text("a")); + for name in ["a1", "a2", "a3"] { + group.push(Value::new_text(name)); + } + let dimension = DimensionBuilder::new(Axis3::X, group); + let pt = PivotTableBuilder::new(Value::new_text("Default Title"), &[dimension]).build(); + +} diff --git a/rust/pspp/src/output/pivot/tlo.rs b/rust/pspp/src/output/pivot/tlo.rs index 198c1aad4e..ece35c3c84 100644 --- a/rust/pspp/src/output/pivot/tlo.rs +++ b/rust/pspp/src/output/pivot/tlo.rs @@ -61,7 +61,7 @@ impl From for Look { row_label_position: if look.pt_table_look.nested_row_labels { RowLabelPosition::Nested } else { - RowLabelPosition::InCorner + RowLabelPosition::Corner }, heading_widths: enum_map! { HeadingRegion::ColumnHeadings => look.v2_styles.min_column_width..=look.v2_styles.max_column_width, diff --git a/rust/pspp/src/settings.rs b/rust/pspp/src/settings.rs index 20933aba05..01e7e8c540 100644 --- a/rust/pspp/src/settings.rs +++ b/rust/pspp/src/settings.rs @@ -1,11 +1,11 @@ -use std::sync::OnceLock; +use std::sync::{Arc, OnceLock}; use enum_map::EnumMap; use crate::{ endian::Endian, format::{Format, Settings as FormatSettings}, - message::Severity, + message::Severity, output::pivot::Look, }; /// Whether to show variable or value labels or the underlying value or variable @@ -38,6 +38,8 @@ impl Show { } pub struct Settings { + pub look: Arc, + pub input_integer_format: Endian, pub input_float_format: Endian, pub output_integer_format: Endian, @@ -76,6 +78,7 @@ pub struct Settings { impl Default for Settings { fn default() -> Self { Self { + look: Arc::new(Look::default()), input_integer_format: Endian::NATIVE, input_float_format: Endian::NATIVE, output_integer_format: Endian::NATIVE,