From 083a542d26fbbc7732204cc118506173598df520 Mon Sep 17 00:00:00 2001 From: Ben Pfaff Date: Mon, 5 Jan 2026 13:26:28 -0800 Subject: [PATCH] add lightiweight test --- rust/pspp/src/output/pivot.rs | 54 ++++++++++++++++----- rust/pspp/src/output/pivot/output.rs | 4 +- rust/pspp/src/spv/read/light.rs | 25 +++++++--- rust/pspp/src/spv/testdata/light1.expected | 12 +++++ rust/pspp/src/spv/testdata/light1.spv | Bin 0 -> 2187 bytes rust/pspp/src/spv/write.rs | 2 +- 6 files changed, 73 insertions(+), 24 deletions(-) create mode 100644 rust/pspp/src/spv/testdata/light1.expected create mode 100644 rust/pspp/src/spv/testdata/light1.spv diff --git a/rust/pspp/src/output/pivot.rs b/rust/pspp/src/output/pivot.rs index dd261b3eca..d37fc0732f 100644 --- a/rust/pspp/src/output/pivot.rs +++ b/rust/pspp/src/output/pivot.rs @@ -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, + /// 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, /// 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) -> 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) diff --git a/rust/pspp/src/output/pivot/output.rs b/rust/pspp/src/output/pivot/output.rs index 29b2664e2e..e918401cef 100644 --- a/rust/pspp/src/output/pivot/output.rs +++ b/rust/pspp/src/output/pivot/output.rs @@ -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); diff --git a/rust/pspp/src/spv/read/light.rs b/rust/pspp/src/spv/read/light.rs index 9b6ede22b0..e603b58988 100644 --- a/rust/pspp/src/spv/read/light.rs +++ b/rust/pspp/src/spv/read/light.rs @@ -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::>(); 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, 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 index 0000000000..1a91ccc775 --- /dev/null +++ b/rust/pspp/src/spv/testdata/light1.expected @@ -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 index 0000000000000000000000000000000000000000..1095cede1b7d817259a17889f2f98e27f4685071 GIT binary patch literal 2187 zcmaKtdpHw(8^S!Hs%yw{(0(suDAF5yMEXG&;7fv`@XKv?|ZvprDdQ1AP@+E9M|&( z{2~}Y27vfm$AT#*10#uK-{62fxwmnNpzyox zxAm(0Gtf}@*(?{c<1UC|W7NY?i6qv=v_{b4(+=G2?7V6EzG-nmr`&nP+pgX1TUbU5 z<9;Z3O#42~bLE5*af|Tl>wZ>u!_c#@=fsON@ltMY@1eAmO@sn1GUV3G+r#nsGg^mT zixTF=@%{WFa5;v(KcYz;bWiy45qO4Ohv>u?0_}u*Tn(T+tT;J+H@Uo)*Sw%{-8wKC zS(m@V?(wZ7^dUhe^ItvO3ZJ^NkG3F|wn_7MO0D@lN)Fk$GVNaXU(y=?mfSLb%uAja z&T%o6++)aFzxIIDqP-t>lTM8)Zj9^JtJn%u^bJvwn(+gy~i-~8o92ClvE?kb- zUq730$4!{=iPPw)J2i_H6*OY7r_vh_>T*4Yrm-R^!5>Cb7D{7=S37jVqc_#N<{i~R z+j@~fFAh#SS^@>(EtgOssnr40)X`}smx=h(m9)8n9jQVJ4GJ0$9|o(tUhch3fHZWE zdr+PZ7o>bc{{deV{UT;Aj4=MN{6$>C=qPnW+4P9c|~ySX1q1x zRq5pQ?}GQwcUGV-9pEjk(^DW>Zbq)?wa#39w{_XtBxc5Lk)zmZ_3>0?$UN9;5@@-@ z=N3+uH|#rzzRA3^Q>%FAzzxmXX=++3o=$WqyEfpTDY{mqz(E}^1sABEd>2X&Gman* zqGRZ9?z*!I=DM(qNilze^egv48!Ola9D_ZO0OzEXJ+OOmw1!j5?V9x7I=HPkA7|`S zJ8U264AkvN*)WB7czyFboCc3(D?jk88{?=M4&Ok*f}H$w5!5@Ix5r-GP>p0hty7lB z$}?Z$Dc3G9HmnaYilRc@qY=_GDWmqu$}=F-40*NuJi$Ri z)GPgKihn(D8jkx)H#6jZys_~C{B0Dvaa(EPrS!W2i&6%tu~J>uE`yTTY=^}2p(04k z)M>Nr7TflkJqn1=!Huu<-R}1{?=zj_z+)~hoiOR*9XgfS*)wqh@C01K)hr5Ow~Tpq zx}<;Na5{z;6~RP)MfY(K4Mz3;@qOV(tKomrhrSFDref)2Xwxl_8Jied~CPejcAIj#(yo3B%UG z=>v@k+M4Ar< z9?h^y8?1!tIrJA$i4{bxwn}xrkkPnpIr#)+GibQ z-rJ+Y?|eSmF@X`E8j8OgfzwdY%o08Kj_$~mfX*G$iOa?(Cpn10$@JRNi617C;?jLk z8SVK>+IwXy%TB*~l2Hm0)u3cyqZMFQG3%bwreE&!dGB)F&1hysj`XqR!23ZV`|&ju zY4wEdPDM!V))udk#<1up0awc3cDPy7!oH`}VGIGW5OFYQdjx94ZYy}q<9ds7c1V^&-YBhJH>}h;DB$OI{*~Xg?fl>R$?0!)^*_t~I=~wAKQ*^n m`}beqpLKq1d9C%ILd*T{;oY#Z>({cRS5NP1%v;R^0R97LCbs