More tests
authorBen Pfaff <blp@cs.stanford.edu>
Fri, 11 Apr 2025 15:42:11 +0000 (08:42 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Fri, 11 Apr 2025 15:42:11 +0000 (08:42 -0700)
rust/pspp/src/format/mod.rs
rust/pspp/src/output/pivot/mod.rs
rust/pspp/src/output/pivot/test.rs

index 14a6209de1aefa725285109ff6c7923e487472fc..3b87aa4664eafe7943e23d8cc15f58de1981169a 100644 (file)
@@ -471,6 +471,30 @@ impl Format {
         d: 0,
     };
 
+    pub const F40_1: Format = Format {
+        type_: Type::F,
+        w: 40,
+        d: 1,
+    };
+
+    pub const F40_2: Format = Format {
+        type_: Type::F,
+        w: 40,
+        d: 2,
+    };
+
+    pub const F40_3: Format = Format {
+        type_: Type::F,
+        w: 40,
+        d: 3,
+    };
+
+    pub const PCT40_1: Format = Format {
+        type_: Type::Pct,
+        w: 40,
+        d: 1,
+    };
+
     pub const F8_2: Format = Format {
         type_: Type::F,
         w: 8,
index d629cc29e0ceb5b8b463acdd6979af90fa0ae473..ecf71afeab1410d5869e2c66f8d051deb47e3c46 100644 (file)
 //!   a category for each dimension to a value, which is commonly a number but
 //!   could also be a variable name or an arbitrary text string.
 //!
-//! Creating a pivot table usually consists of the following steps:
-//!
-//! 1. Create the table with pivot_table_create(), passing in the title.
-//!
-//! 2. Optionally, set the format to use for "count" values with
-//!    pivot_table_set_weight_var() or pivot_table_set_weight_format().
-//!
-//! 3. Create each dimension with pivot_dimension_create() and populate it with
-//!    categories and, possibly, with groups that contain the categories.  This
-//!    call also assigns the dimension to an axis.
-//!
-//!    In simple cases, only a call to pivot_dimension_create() is needed.
-//!    Other functions such as pivot_category_create_group() can be used for
-//!    hierarchies of categories.
-//!
-//!    Sometimes it's easier to create categories in tandem with inserting data,
-//!    for example by adding a category for a variable just before inserting the
-//!    first cell for that variable.  In that case, creating categories and
-//!    inserting data can be interleaved.
-//!
-//! 4. Insert data.  For each cell, supply the category indexes, which are
-//!    assigned starting from 0 in the order in which the categories were
-//!    created in step 2, and the value to go in the cell.  If the table has a
-//!    small, fixed number of dimensions, functions like, e.g.
-//!    pivot_table_put3() for 3 dimensions, can be used.  The general function
-//!    pivot_table_put() works for other cases.
-//!
-//! 5. Output the table for user consumption.  Use pivot_table_submit().
+//! Use [PivotTable::builder] to create a pivot table.
 
 use std::{
     collections::HashMap,
@@ -298,7 +271,7 @@ impl Iterator for AxisIterator {
 }
 
 impl PivotTable {
-    fn builder(title: impl Into<Value>, dimensions: &[DimensionBuilder]) -> PivotTableBuilder {
+    fn builder(title: impl Into<Value>, dimensions: Vec<DimensionBuilder>) -> PivotTableBuilder {
         PivotTableBuilder::new(title, dimensions)
     }
     fn axis_values(&self, axis: Axis3) -> AxisIterator {
@@ -395,34 +368,29 @@ impl Group {
 #[derive(Clone)]
 pub struct DimensionBuilder {
     axis: Axis3,
-    root: GroupBuilder,
-    len: usize,
-    hide_all_labels: bool,
+    dimension: Dimension,
 }
 
 impl DimensionBuilder {
     pub fn new(axis: Axis3, root: GroupBuilder) -> Self {
-        let len = root.len();
+        let mut leaves = Vec::with_capacity(root.len());
+        let root = root.build(None, &mut leaves);
         Self {
             axis,
-            root,
-            len,
-            hide_all_labels: false,
+            dimension: Dimension {
+                root,
+                presentation_order: (0..leaves.len()).collect(),
+                data_leaves: leaves,
+                hide_all_labels: false,
+            },
         }
     }
     pub fn with_all_labels_hidden(mut self) -> Self {
-        self.hide_all_labels = true;
+        self.dimension.hide_all_labels = true;
         self
     }
     fn build(self) -> Dimension {
-        let mut leaves = Vec::with_capacity(self.len);
-        let root = self.root.build(None, &mut leaves);
-        Dimension {
-            root,
-            presentation_order: (0..leaves.len()).collect(),
-            data_leaves: leaves,
-            hide_all_labels: self.hide_all_labels,
-        }
+        self.dimension
     }
 }
 
@@ -551,16 +519,26 @@ impl From<&str> for CategoryBuilder {
 pub struct PivotTableBuilder {
     look: Arc<Look>,
     title: Box<Value>,
-    dimensions: Vec<DimensionBuilder>,
+    axes: EnumMap<Axis3, Axis>,
+    dimensions: Vec<Dimension>,
     cells: HashMap<usize, Value>,
 }
 
 impl PivotTableBuilder {
-    pub fn new(title: impl Into<Value>, dimensions: &[DimensionBuilder]) -> Self {
+    pub fn new(title: impl Into<Value>, dimension_builders: Vec<DimensionBuilder>) -> Self {
+        let mut dimensions = Vec::with_capacity(dimension_builders.len());
+        let mut axes = EnumMap::from_fn(|_axis| Axis {
+            dimensions: Vec::with_capacity(dimension_builders.len()),
+        });
+        for d in dimension_builders {
+            axes[d.axis].dimensions.push(dimensions.len());
+            dimensions.push(d.build());
+        }
         Self {
             look: Settings::global().look.clone(),
             title: Box::new(title.into()),
-            dimensions: dimensions.to_vec(),
+            axes,
+            dimensions,
             cells: HashMap::new(),
         }
     }
@@ -568,24 +546,36 @@ impl PivotTableBuilder {
         self.look = look;
         self
     }
+    pub fn insert_number(&mut self, data_indexes: &[usize], number: Option<f64>, class: Class) {
+        let format = match class {
+            Class::Other => Settings::global().default_format,
+            Class::Integer => Format::F40,
+            Class::Correlations => Format::F40_3,
+            Class::Significance => Format::F40_3,
+            Class::Percent => Format::PCT40_1,
+            Class::Residual => Format::F40_2,
+            Class::Count => Format::F40, // XXX
+        };
+        let value = Value::new(ValueInner::Number {
+            show: None,
+            format,
+            honor_small: class == Class::Other,
+            value: number,
+            var_name: None,
+            value_label: None,
+        });
+        self.insert(data_indexes, value);
+    }
     pub fn insert(&mut self, data_indexes: &[usize], value: Value) {
         self.cells.insert(
-            cell_index(data_indexes, self.dimensions.iter().map(|d| d.len)),
+            cell_index(data_indexes, self.dimensions.iter().map(|d| d.len())),
             value,
         );
     }
     pub fn build(self) -> PivotTable {
         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(|_axis| Axis {
-            dimensions: Vec::with_capacity(self.dimensions.len()),
-        });
-        for d in self.dimensions {
-            axes[d.axis].dimensions.push(dimensions.len());
-            dimensions.push(d.build());
-        }
-        table.dimensions = dimensions;
-        table.axes = axes;
+        table.dimensions = self.dimensions;
+        table.axes = self.axes;
         table.cells = self.cells;
         table.current_layer = repeat_n(0, table.axes[Axis3::Z].dimensions.len()).collect();
         table
index b683992ae435bee018b77ba4825d02f19b7e82ff..b4748c007c5d7a90bde12b5ebe3d5001abd61a2f 100644 (file)
@@ -3,8 +3,8 @@ use std::sync::Arc;
 use enum_map::EnumMap;
 
 use crate::output::pivot::{
-    Area, Axis2, Border, BorderStyle, Color, Dimension, Group, HeadingRegion, LabelPosition, Look,
-    PivotTable, RowColBorder, Stroke,
+    Area, Axis2, Border, BorderStyle, Class, Color, Dimension, Group, HeadingRegion, LabelPosition,
+    Look, PivotTable, RowColBorder, Stroke,
 };
 
 use super::{Axis3, DimensionBuilder, GroupBuilder, PivotTableBuilder, Value};
@@ -28,7 +28,7 @@ fn d1(title: &str, axis: Axis3) -> PivotTable {
         group.push(Value::new_text(name));
     }
     let dimension = DimensionBuilder::new(axis, group);
-    let mut pt = PivotTableBuilder::new(Value::new_text(title), &[dimension]);
+    let mut pt = PivotTableBuilder::new(Value::new_text(title), vec![dimension]);
     for i in 0..3 {
         pt.insert(&[i], Value::new_integer(Some(i as f64)));
     }
@@ -89,7 +89,7 @@ fn d2(title: &str, axes: [Axis3; 2], dimension_labels: Option<LabelPosition>) ->
     }
     let d2 = DimensionBuilder::new(axes[1], b);
 
-    let mut pt = PivotTableBuilder::new(Value::new_text(title), &[d1, d2]);
+    let mut pt = PivotTableBuilder::new(Value::new_text(title), vec![d1, d2]);
     let mut i = 0;
     for b in 0..3 {
         for a in 0..3 {
@@ -544,7 +544,7 @@ Caption
 
 #[test]
 fn no_dimension() {
-    let pivot_table = PivotTableBuilder::new(Value::new_text("No Dimensions"), &[])
+    let pivot_table = PivotTableBuilder::new(Value::new_text("No Dimensions"), vec![])
         .with_look(Arc::new(test_look()))
         .build();
     assert_rendering(
@@ -561,7 +561,7 @@ fn empty_dimensions() {
     let look = Arc::new(test_look().with_omit_empty(false));
 
     let d1 = DimensionBuilder::new(Axis3::X, GroupBuilder::new(Value::new_text("a")));
-    let pivot_table = PivotTableBuilder::new(Value::new_text("One Empty Dimension"), &[d1])
+    let pivot_table = PivotTableBuilder::new(Value::new_text("One Empty Dimension"), vec![d1])
         .with_look(look.clone())
         .build();
     assert_rendering(&pivot_table, "One Empty Dimension\n");
@@ -571,7 +571,7 @@ fn empty_dimensions() {
         Axis3::X,
         GroupBuilder::new(Value::new_text("b")).with_label_shown(),
     );
-    let pivot_table = PivotTableBuilder::new(Value::new_text("Two Empty Dimensions"), &[d1, d2])
+    let pivot_table = PivotTableBuilder::new(Value::new_text("Two Empty Dimensions"), vec![d1, d2])
         .with_look(look.clone())
         .build();
     assert_rendering(&pivot_table, "Two Empty Dimensions\n");
@@ -589,7 +589,7 @@ fn empty_dimensions() {
     );
     let pivot_table = PivotTableBuilder::new(
         Value::new_text("Three Dimensions, Two Empty"),
-        &[d1, d2, d3],
+        vec![d1, d2, d3],
     )
     .with_look(look.clone())
     .build();
@@ -610,7 +610,7 @@ fn empty_groups() {
         .with(Value::new_text("b3"));
     let d2 = DimensionBuilder::new(Axis3::Y, b);
 
-    let mut pt = PivotTableBuilder::new(Value::new_text("Empty Groups"), &[d1, d2]);
+    let mut pt = PivotTableBuilder::new(Value::new_text("Empty Groups"), vec![d1, d2]);
     let mut i = 0;
     for b in 0..2 {
         for a in 0..2 {
@@ -668,7 +668,7 @@ fn d4(
             .with(Group::builder("dg1").with("d1").with("d2"))
             .with("d3"),
     );
-    let mut pivot_table = PivotTable::builder(title, &[a, b, c, d])
+    let mut pivot_table = PivotTable::builder(title, vec![a, b, c, d])
         .with_look(Arc::new(test_look().with_borders(borders)));
     let mut i = 0;
     for d in 0..3 {
@@ -976,3 +976,104 @@ dg1┊d1│    c1  0│ 1┊ 2│ 3│ 4┊ 5│ 6│ 7┊ 8
 ",
     );
 }
+
+#[test]
+fn small_numbers() {
+    let exponent = Dimension::builder(
+        Axis3::Y,
+        Group::builder("exponent")
+            .with("0")
+            .with("-1")
+            .with("-2")
+            .with("-3")
+            .with("-4")
+            .with("-5")
+            .with("-6")
+            .with("-7")
+            .with("-8")
+            .with("-9")
+            .with_label_shown(),
+    );
+    let sign = Dimension::builder(
+        Axis3::X,
+        Group::builder("sign")
+            .with("positive")
+            .with("negative")
+            .with_label_shown(),
+    );
+    let rc = Dimension::builder(
+        Axis3::X,
+        Group::builder("result class")
+            .with((Value::new_text("general"), Class::Other))
+            .with((Value::new_text("specific"), Class::Residual))
+            .with_label_shown(),
+    );
+    let mut pt = PivotTable::builder("small numbers", vec![exponent, sign, rc]);
+    pt.insert_number(&[0, 0, 0], Some(1.0), Class::Other);
+    pt.insert_number(&[1, 0, 0], Some(0.1), Class::Other);
+    pt.insert_number(&[2, 0, 0], Some(0.01), Class::Other);
+    pt.insert_number(&[3, 0, 0], Some(0.001), Class::Other);
+    pt.insert_number(&[4, 0, 0], Some(0.0001), Class::Other);
+    pt.insert_number(&[5, 0, 0], Some(0.00001), Class::Other);
+    pt.insert_number(&[6, 0, 0], Some(0.000001), Class::Other);
+    pt.insert_number(&[7, 0, 0], Some(0.0000001), Class::Other);
+    pt.insert_number(&[8, 0, 0], Some(0.00000001), Class::Other);
+    pt.insert_number(&[9, 0, 0], Some(0.000000001), Class::Other);
+    pt.insert_number(&[0, 0, 1], Some(-1.0), Class::Residual);
+    pt.insert_number(&[1, 0, 1], Some(-0.1), Class::Residual);
+    pt.insert_number(&[2, 0, 1], Some(-0.01), Class::Residual);
+    pt.insert_number(&[3, 0, 1], Some(-0.001), Class::Residual);
+    pt.insert_number(&[4, 0, 1], Some(-0.0001), Class::Residual);
+    pt.insert_number(&[5, 0, 1], Some(-0.00001), Class::Residual);
+    pt.insert_number(&[6, 0, 1], Some(-0.000001), Class::Residual);
+    pt.insert_number(&[7, 0, 1], Some(-0.0000001), Class::Residual);
+    pt.insert_number(&[8, 0, 1], Some(-0.00000001), Class::Residual);
+    pt.insert_number(&[9, 0, 1], Some(-0.000000001), Class::Residual);
+    pt.insert_number(&[0, 1, 0], Some(1.0), Class::Other);
+    pt.insert_number(&[1, 1, 0], Some(0.1), Class::Other);
+    pt.insert_number(&[2, 1, 0], Some(0.01), Class::Other);
+    pt.insert_number(&[3, 1, 0], Some(0.001), Class::Other);
+    pt.insert_number(&[4, 1, 0], Some(0.0001), Class::Other);
+    pt.insert_number(&[5, 1, 0], Some(0.00001), Class::Other);
+    pt.insert_number(&[6, 1, 0], Some(0.000001), Class::Other);
+    pt.insert_number(&[7, 1, 0], Some(0.0000001), Class::Other);
+    pt.insert_number(&[8, 1, 0], Some(0.00000001), Class::Other);
+    pt.insert_number(&[9, 1, 0], Some(0.000000001), Class::Other);
+    pt.insert_number(&[0, 1, 1], Some(-1.0), Class::Residual);
+    pt.insert_number(&[1, 1, 1], Some(-0.1), Class::Residual);
+    pt.insert_number(&[2, 1, 1], Some(-0.01), Class::Residual);
+    pt.insert_number(&[3, 1, 1], Some(-0.001), Class::Residual);
+    pt.insert_number(&[4, 1, 1], Some(-0.0001), Class::Residual);
+    pt.insert_number(&[5, 1, 1], Some(-0.00001), Class::Residual);
+    pt.insert_number(&[6, 1, 1], Some(-0.000001), Class::Residual);
+    pt.insert_number(&[7, 1, 1], Some(-0.0000001), Class::Residual);
+    pt.insert_number(&[8, 1, 1], Some(-0.00000001), Class::Residual);
+    pt.insert_number(&[9, 1, 1], Some(-0.000000001), Class::Residual);
+    let pivot_table = pt.with_look(Arc::new(test_look())).build();
+    assert_rendering(
+        &pivot_table,
+        "\
+small numbers
+╭────────┬─────────────────────────────────────╮
+│        │             result class            │
+│        ├───────────────────┬─────────────────┤
+│        │      general      │     specific    │
+│        ├───────────────────┼─────────────────┤
+│        │        sign       │       sign      │
+│        ├─────────┬─────────┼────────┬────────┤
+│exponent│ positive│ negative│positive│negative│
+├────────┼─────────┼─────────┼────────┼────────┤
+│0       │     1.00│     1.00│   -1.00│   -1.00│
+│-1      │      .10│      .10│    -.10│    -.10│
+│-2      │      .01│      .01│    -.01│    -.01│
+│-3      │      .00│      .00│     .00│     .00│
+│-4      │      .00│      .00│     .00│     .00│
+│-5      │1.00E-005│1.00E-005│     .00│     .00│
+│-6      │1.00E-006│1.00E-006│     .00│     .00│
+│-7      │1.00E-007│1.00E-007│     .00│     .00│
+│-8      │1.00E-008│1.00E-008│     .00│     .00│
+│-9      │1.00E-009│1.00E-009│     .00│     .00│
+╰────────┴─────────┴─────────┴────────┴────────╯
+",
+    );
+}