work on pivot table builder
authorBen Pfaff <blp@cs.stanford.edu>
Thu, 13 Mar 2025 09:17:45 +0000 (02:17 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Thu, 13 Mar 2025 09:17:45 +0000 (02:17 -0700)
rust/pspp/src/output/pivot/mod.rs
rust/pspp/src/output/pivot/output.rs
rust/pspp/src/output/pivot/test.rs [new file with mode: 0644]
rust/pspp/src/output/pivot/tlo.rs
rust/pspp/src/settings.rs

index 8209a010fc6fc2390efcfc5a5cf931189381a2bf..7ea269e88d30e14937ebf080c81acd476428b059 100644 (file)
@@ -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<Range<usize>>,
 }
 
-#[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<Group>,
 
     ///  All of the leaves reachable via the root.
     ///
@@ -376,11 +386,232 @@ pub struct Group {
     children: Vec<Category>,
 
     /// 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<Value>,
+    children: Vec<CategoryBuilder>,
+    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<T>(&mut self, value: T)
+    where
+        T: Into<CategoryBuilder>,
+    {
+        self.children.push(value.into());
+    }
+    pub fn with<T>(mut self, value: T) -> Self
+    where
+        T: Into<CategoryBuilder>,
+    {
+        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<Weak<Group>>,
+        leaves: &mut Vec<Arc<Leaf>>,
+    ) -> Arc<Group> {
+        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<GroupBuilder>),
+    Leaf {
+        name: Box<Value>,
+        class: Option<Class>,
+    },
+}
+
+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>,
+        group_index: usize,
+        leaves: &mut Vec<Arc<Leaf>>,
+    ) -> 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<Value> 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<Look>,
+    title: Box<Value>,
+    dimensions: Vec<DimensionBuilder>,
+    cells: HashMap<usize, Value>,
+}
+
+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<Look>) -> 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<Group>,
@@ -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<Class>,
+}
+
+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<D>(deserializer: D) -> Result<Self, D::Error>
     where
@@ -1170,8 +1417,8 @@ pub struct PivotTable {
     pub cells: HashMap<usize, Value>,
 }
 
-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<I>(data_indexes: &[usize], dimensions: I) -> usize
+where
+    I: ExactSizeIterator<Item = usize>,
+{
+    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<Value>, look: Arc<Look>) -> 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<String>) -> Self {
+        Self::new_user_text(s)
+    }
     pub fn new_user_text(s: impl Into<String>) -> Self {
         let s: String = s.into();
         Self {
index c15b569d01ac820a1a0aee362c68d0718aa17934..2c1dd2b9dea31faf2b2b8b5fc12c394e67a31d15 100644 (file)
@@ -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 (file)
index 0000000..3446f56
--- /dev/null
@@ -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();
+
+}
index 198c1aad4e4c952de25f72518947cfe23f578153..ece35c3c8480e98228a2d97b8532b6e98f257d3f 100644 (file)
@@ -61,7 +61,7 @@ impl From<TableLook> 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,
index 20933aba0535fa59ffadf6660013b9e8ad9fc469..01e7e8c5401100635165811043c1c47643f1d5b9 100644 (file)
@@ -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<Look>,
+
     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,