From a9777a89cf27984a3d8637c43e1a338050f7704f Mon Sep 17 00:00:00 2001 From: Ben Pfaff Date: Sat, 24 Oct 2026 11:15:43 -0700 Subject: [PATCH] work on variable sets --- rust/pspp/src/spv/read/graph.rs | 34 ++++--- rust/pspp/src/spv/read/graph/var_set.rs | 119 +++++++++++++++++++++--- 2 files changed, 128 insertions(+), 25 deletions(-) diff --git a/rust/pspp/src/spv/read/graph.rs b/rust/pspp/src/spv/read/graph.rs index 7efed85ba8..82bdcb1f61 100644 --- a/rust/pspp/src/spv/read/graph.rs +++ b/rust/pspp/src/spv/read/graph.rs @@ -1203,29 +1203,32 @@ impl Series { categorical: bool, formatter: Formatter, ) -> Self { - let unique_values = if categorical { - values - .iter() - .unique_by(|sv| sv.value.clone()) - .sorted_by(|a, b| a.value.cmp(&b.value)) - .cloned() - .collect() - } else { - Vec::new() - }; Self { name, _label: None, _short_label: None, _weight: false, _key: false, + unique_values: Self::unique_values(&values, categorical), categorical, values, - unique_values, formatter, } } + fn unique_values(values: &[SeriesValue], categorical: bool) -> Vec { + if categorical { + values + .iter() + .unique_by(|sv| sv.value.clone()) + .sorted_by(|a, b| a.value.cmp(&b.value)) + .cloned() + .collect() + } else { + Vec::new() + } + } + fn new_empty() -> Self { Self::new( String::from("empty"), @@ -1241,6 +1244,15 @@ impl Series { ..self } } + + fn subset(mut self, indexes: impl IntoIterator) -> Self { + self.values = indexes + .into_iter() + .map(|index| self.values[index].clone()) + .collect_vec(); + self.unique_values = Self::unique_values(&self.values, self.categorical); + self + } } #[derive(Deserialize, Debug)] diff --git a/rust/pspp/src/spv/read/graph/var_set.rs b/rust/pspp/src/spv/read/graph/var_set.rs index 1e520bea2a..8d3555a972 100644 --- a/rust/pspp/src/spv/read/graph/var_set.rs +++ b/rust/pspp/src/spv/read/graph/var_set.rs @@ -356,7 +356,7 @@ impl DomainComponent { } } -#[derive(Debug)] +#[derive(Clone, Debug)] pub enum Domain { Empty, Component(DomainComponent), @@ -388,17 +388,20 @@ impl Domain { Self::Component(DomainComponent::for_variable(series)) } - pub fn cross(a: Domain, b: Domain) -> Self { - Self::Cross(Box::new([a, b])) + pub fn cross(self, other: Domain) -> Self { + Self::Cross(Box::new([self, other])) } - pub fn nest(a: Domain, b: Domain, data: Vec) -> Self { + pub fn nest(self, other: Domain, data: Vec) -> Self { Self::Nest { - domains: Box::new([a, b]), + domains: Box::new([self, other]), data, } } + pub fn is_empty(&self) -> bool { + self.len() == 0 + } /// The number of domain components in this domain. pub fn len(&self) -> usize { match self { @@ -438,6 +441,7 @@ impl Domain { pub struct VarSet { data: Vec, domain: Domain, + case_mapping: CaseMapping, } impl VarSet { @@ -451,6 +455,7 @@ impl VarSet { Self { data: vec![Series::new_empty()], domain: Domain::new_unity(), + case_mapping: CaseMapping::new_empty(), } } @@ -458,6 +463,7 @@ impl VarSet { Self { data: vec![series.clone()], domain: Domain::for_variable(series), + case_mapping: CaseMapping::new_indexed(series.values.len()), } } @@ -471,28 +477,51 @@ impl VarSet { } pub fn cross(self, other: Self) -> Self { - if self.domain.len() == 0 { + if self.domain.is_empty() { other - } else if other.domain.len() == 0 { + } else if other.domain.is_empty() { self - } else { + } else if self.len() == other.len() + && self.case_mapping == other.case_mapping + && self.case_mapping.is_all_unique() + { Self { data: chain(self.data, other.data).collect(), - domain: Domain::cross(self.domain, other.domain), + domain: self.domain.cross(other.domain), + case_mapping: self.case_mapping, + } + } else { + let [a, b] = self.case_mapping.cross(&other.case_mapping); + Self { + data: chain( + self.data + .into_iter() + .map(|series| series.subset(a.iter().copied())), + other + .data + .into_iter() + .map(|series| series.subset(b.iter().copied())), + ) + .collect(), + case_mapping: self.case_mapping.subset(a), + domain: self.domain.cross(other.domain), } } } pub fn nest(self, other: Self) -> Self { - if self.domain.len() == 0 { + if self.domain.is_empty() { other - } else if other.domain.len() == 0 { + } else if other.domain.is_empty() { self } else { - let data = chain(self.data, other.data).collect_vec(); + let self_domain = self.domain.clone(); + let other_domain = other.domain.clone(); + let crossed = self.cross(other); Self { - domain: Domain::nest(self.domain, other.domain, data.clone()), - data, + data: crossed.data.clone(), + domain: self_domain.nest(other_domain, crossed.data), + case_mapping: crossed.case_mapping, } } } @@ -524,6 +553,68 @@ struct DimensionInfo { is_categorical: bool, } +// `PartialEq` might need to be implemented manually if we introduce a field for +// caching the reverse mapping. +#[derive(Debug, PartialEq, Eq)] +struct CaseMapping { + map: Vec, +} + +impl CaseMapping { + pub fn new_empty() -> Self { + Self { map: Vec::new() } + } + pub fn new_indexed(n: usize) -> Self { + Self { + map: (0..n).collect(), + } + } + + pub fn new_map(map: Vec) -> Self { + Self { map } + } + + pub fn len(&self) -> usize { + self.map.len() + } + pub fn all_cases(&self) -> &[usize] { + self.map.as_slice() + } + pub fn get(&self, index: usize) -> usize { + self.map[index] + } + + /// True if each input index maps to a different case. + pub fn is_all_unique(&self) -> bool { + self.map.iter().all_unique() + } + + pub fn cross(&self, other: &Self) -> [Vec; 2] { + let mut other_cases = Vec::new(); + for (i, &j) in other.all_cases().into_iter().enumerate() { + if j >= other_cases.len() { + other_cases.resize(j + 1, Vec::new()); + } + other_cases[j].push(i); + } + let mut result = [Vec::new(), Vec::new()]; + for (i, &j) in self.all_cases().iter().enumerate() { + if let Some(cases) = other_cases.get(j) { + for &k in cases { + result[0].push(i); + result[1].push(k); + } + } + } + result + } + + pub fn subset(mut self, indexes: impl IntoIterator) -> Self { + self.map = indexes.into_iter().map(|index| self.map[index]).collect(); + self + } +} + #[cfg(test)] mod tests { use std::str::FromStr; -- 2.47.3