]> pintos-os.org Git - pspp/commitdiff
work on variable sets
authorBen Pfaff <blp@cs.stanford.edu>
Sat, 24 Oct 2026 18:15:43 +0000 (11:15 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Sat, 24 Oct 2026 18:15:43 +0000 (11:15 -0700)
rust/pspp/src/spv/read/graph.rs
rust/pspp/src/spv/read/graph/var_set.rs

index 7efed85ba8d1991360eec3272374f0d71f6dd6e9..82bdcb1f61bb1d2cbf2433536e8b107b95aca840 100644 (file)
@@ -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<SeriesValue> {
+        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<Item = usize>) -> 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)]
index 1e520bea2a747c4edbc5b28782d13c7b7f0ca926..8d3555a9723340ab96148ebe4140fb738bcb86d9 100644 (file)
@@ -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<Series>) -> Self {
+    pub fn nest(self, other: Domain, data: Vec<Series>) -> 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<Series>,
     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<usize>,
+}
+
+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<usize>) -> 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<usize>; 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<Item = usize>) -> Self {
+        self.map = indexes.into_iter().map(|index| self.map[index]).collect();
+        self
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use std::str::FromStr;