add lightiweight test
authorBen Pfaff <blp@cs.stanford.edu>
Mon, 5 Jan 2026 21:26:28 +0000 (13:26 -0800)
committerBen Pfaff <blp@cs.stanford.edu>
Mon, 5 Jan 2026 21:26:28 +0000 (13:26 -0800)
rust/pspp/src/output/pivot.rs
rust/pspp/src/output/pivot/output.rs
rust/pspp/src/spv/read/light.rs
rust/pspp/src/spv/testdata/light1.expected [new file with mode: 0644]
rust/pspp/src/spv/testdata/light1.spv [new file with mode: 0644]
rust/pspp/src/spv/write.rs

index dd261b3eca76e861b22eb561f11ef5ab4f90d6f8..d37fc0732f098e8894072b03cb51a06ad4129a3c 100644 (file)
@@ -485,7 +485,7 @@ impl PivotTable {
                 .iter()
                 .zip(presentation_indexes.iter())
             {
-                data_indexes[dim_index] = self.dimensions[dim_index].presentation_order[pindex];
+                data_indexes[dim_index] = self.dimensions[dim_index].ptod[pindex];
             }
         }
         data_indexes
@@ -616,23 +616,17 @@ pub struct Dimension {
     /// subcategories.
     root: Group,
 
-    /// Ordering of leaves for presentation.
+    /// Maps from an index in presentation order to a data index ("p" to "d").
     ///
-    /// This is a permutation of `0..n` where `n` is the number of leaves.  It
-    /// maps from an index in presentation order to an index in data order.
-    pub presentation_order: Vec<usize>,
+    /// This is a permutation of `0..n` where `n` is the number of leaves.
+    /// Given a [Leaf] that can be found as via `dimension.nth_leaf(leaf_idx)`,
+    /// the corresponding data index is `ptod[leaf_idx]`.
+    ptod: Vec<usize>,
 
     /// Display.
     pub hide_all_labels: bool,
 }
 
-impl Dimension {
-    /// Returns the root [Group] of the dimension.
-    pub fn root(&self) -> &Group {
-        &self.root
-    }
-}
-
 /// A vector of references to [Group]s.
 ///
 /// Used to represent a sequence of groups along a [Path].  This is a [SmallVec]
@@ -653,16 +647,50 @@ pub struct Path<'a> {
 /// Group indexes visited along a [Path].
 pub type IndexVec = SmallVec<[usize; 4]>;
 
+/// Indicates that the argument to [Dimension::set_ptod] was not a valid
+/// permutation.
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub struct InvalidPermutation;
+
 impl Dimension {
     /// Constructs a new [Dimension] with the given `root`.
     pub fn new(root: Group) -> Self {
         Dimension {
-            presentation_order: (0..root.len()).collect(),
+            ptod: (0..root.len()).collect(),
             root,
             hide_all_labels: false,
         }
     }
 
+    /// Returns the root [Group] of the dimension.
+    pub fn root(&self) -> &Group {
+        &self.root
+    }
+
+    /// Returns the presentation-to-data index mapping.
+    pub fn ptod(&self) -> &[usize] {
+        &self.ptod
+    }
+
+    /// Returns this dimension with its presentation-to-data index mapping
+    /// replaced by `ptod`, which must be a permutation of `0..self.len()`.
+    pub fn set_ptod(&mut self, ptod: Vec<usize>) -> Result<(), InvalidPermutation> {
+        if ptod.len() != self.ptod.len() {
+            return Err(InvalidPermutation);
+        }
+
+        let mut seen = vec![false; ptod.len()];
+        for element in ptod.iter().copied() {
+            if element >= ptod.len() || seen[element] {
+                return Err(InvalidPermutation);
+            }
+            seen[element] = true;
+        }
+
+        self.ptod = ptod;
+        Ok(())
+    }
+
     /// Returns this dimension with [Dimension::hide_all_labels] set to true.
     pub fn with_all_labels_hidden(self) -> Self {
         self.with_hide_all_labels(true)
index 29b2664e2e6bdd82367e39519dbd62a3d174fe42..e918401cef8b6258a7415b38746210018ac9f68d 100644 (file)
@@ -465,9 +465,7 @@ impl<'a> Heading<'a> {
         let mut columns = Vec::new();
         let mut height = 0;
         for indexes in column_enumeration.iter() {
-            let mut path = dimension
-                .leaf_path(dimension.presentation_order[indexes[dim_index]])
-                .unwrap();
+            let mut path = dimension.leaf_path(indexes[dim_index]).unwrap();
             path.groups.retain(|group| group.show_label);
             height = height.max(1 + path.groups.len());
             columns.push(path);
index 9b6ede22b0d1b4198af8f710cb3971c7f4966714..e603b589887482540d099d3b3e9c9a8ea94a3afe 100644 (file)
@@ -72,6 +72,9 @@ pub enum LightWarning {
 
     /// Dimension with index {0} appears twice in table axes.
     DuplicateDimensionIndex(usize),
+
+    /// Dimension {0} has invalid leaf index permutation (so categories may appear out of order).
+    InvalidPermutation(usize),
 }
 
 struct Context {
@@ -159,7 +162,6 @@ impl LightTable {
     }
 
     pub fn decode(&self, mut warn: &mut dyn FnMut(LightWarning)) -> PivotTable {
-        dbg!(self);
         let encoding = self.formats.encoding(warn);
 
         let n1 = self.formats.n1();
@@ -185,13 +187,20 @@ impl LightTable {
         let dimensions = self
             .dimensions
             .iter()
-            .map(|d| {
+            .enumerate()
+            .map(|(dim_index, d)| {
                 let mut root = Group::new(d.name.decode(encoding, &footnotes, warn))
                     .with_show_label(!d.hide_dim_label);
+                let mut ptod = Vec::new();
                 for category in &d.categories {
-                    category.decode(encoding, &footnotes, &mut root, warn);
+                    category.decode(encoding, &footnotes, &mut root, &mut ptod, warn);
                 }
-                pivot::Dimension::new(root).with_hide_all_labels(d.hide_all_labels)
+                let mut dimension =
+                    pivot::Dimension::new(root).with_hide_all_labels(d.hide_all_labels);
+                if dimension.set_ptod(ptod).is_err() {
+                    warn(LightWarning::InvalidPermutation(dim_index));
+                };
+                dimension
             })
             .collect::<Vec<_>>();
         let dimensions = match self.axes.decode(dimensions) {
@@ -1675,11 +1684,13 @@ impl Category {
         encoding: &'static Encoding,
         footnotes: &Footnotes,
         group: &mut pivot::Group,
+        ptod: &mut Vec<usize>,
         warn: &mut dyn FnMut(LightWarning),
     ) {
         let name = self.name.decode(encoding, footnotes, warn);
         match &self.child {
-            Child::Leaf { leaf_index: _ } => {
+            Child::Leaf { leaf_index } => {
+                ptod.push(*leaf_index as usize);
                 group.push(pivot::Leaf::new(name));
             }
             Child::Group {
@@ -1687,7 +1698,7 @@ impl Category {
                 subcategories,
             } => {
                 for subcategory in subcategories {
-                    subcategory.decode(encoding, footnotes, group, warn);
+                    subcategory.decode(encoding, footnotes, group, ptod, warn);
                 }
             }
             Child::Group {
@@ -1696,7 +1707,7 @@ impl Category {
             } => {
                 let mut subgroup = Group::new(name).with_label_shown();
                 for subcategory in subcategories {
-                    subcategory.decode(encoding, footnotes, &mut subgroup, warn);
+                    subcategory.decode(encoding, footnotes, &mut subgroup, ptod, warn);
                 }
                 group.push(subgroup);
             }
diff --git a/rust/pspp/src/spv/testdata/light1.expected b/rust/pspp/src/spv/testdata/light1.expected
new file mode 100644 (file)
index 0000000..1a91ccc
--- /dev/null
@@ -0,0 +1,12 @@
+                                                      Chi-Square Tests
+╭────────────────────────────┬────────┬──┬─────────────────────┬────────────────────┬────────────────────┬─────────────────╮
+│                            │  Value │df│Asymp. Sig. (2-sided)│Exact Sig. (2-sided)│Exact Sig. (1-sided)│Point Probability│
+├────────────────────────────┼────────┼──┼─────────────────────┼────────────────────┼────────────────────┼─────────────────┤
+│Pearson Chi-Square          │4.496[a]│ 2│                 .106│                .115│                    │                 │
+│Likelihood Ratio            │   4.646│ 2│                 .098│                .108│                    │                 │
+│Fisher's Exact Test         │   4.446│  │                     │                .108│                    │                 │
+│Linear-by-Linear Association│4.403[b]│ 1│                 .036│                .047│                .026│             .015│
+│N of Valid Cases            │      60│  │                     │                    │                    │                 │
+╰────────────────────────────┴────────┴──┴─────────────────────┴────────────────────┴────────────────────┴─────────────────╯
+a. 0 cells (.0%) have expected count less than 5. The minimum expected count is 6.97.
+b. The standardized statistic is 2.098.
diff --git a/rust/pspp/src/spv/testdata/light1.spv b/rust/pspp/src/spv/testdata/light1.spv
new file mode 100644 (file)
index 0000000..1095ced
Binary files /dev/null and b/rust/pspp/src/spv/testdata/light1.spv differ
index 7f241d92ccf596b9b4b17a194fe915f5463b51f7..1276a29a7ceeb0c5a3f7d71f7f8a20ab5d1ef0e5 100644 (file)
@@ -691,7 +691,7 @@ impl BinWrite for Dimension {
         )
             .write_options(writer, endian, ())?;
 
-        let mut data_indexes = self.presentation_order.iter().copied();
+        let mut data_indexes = self.ptod().iter().copied();
         for child in self.root().children() {
             child.write_le(writer, &mut data_indexes)?;
         }