legacy xml starts to work
authorBen Pfaff <blp@cs.stanford.edu>
Wed, 5 Nov 2025 15:50:43 +0000 (07:50 -0800)
committerBen Pfaff <blp@cs.stanford.edu>
Wed, 5 Nov 2025 15:50:43 +0000 (07:50 -0800)
rust/doc/src/invoking/pspp-show-spv.md
rust/pspp/src/output/spv.rs
rust/pspp/src/output/spv/legacy_bin.rs
rust/pspp/src/output/spv/legacy_xml.rs
rust/pspp/src/show_spv.rs

index a31ff548c3a8ec216b1972ca66978ae9a75e36f5..fafbbfa24d42ca215787c7d8bd27d44127d81985 100644 (file)
@@ -1,7 +1,7 @@
 # Inspecting SPSS Viewer Files
 
-The `pspp show-spv` command reads SPSS Viewer (SPV) files, which
-usually have `.sav` extension, and produces a report.  The basic
+The `pspp show-spv` command reads SPSS Viewer (SPV) files, whose names
+usually have an `.spv` extension, and produces a report.  The basic
 syntax is:
 
 ```
index b47911c6d230e2333b763a79536c806da419210f..f42c478dacd5f3178afaf47a60c369e9874f2d05 100644 (file)
@@ -443,16 +443,19 @@ impl Table {
                     Ok(result) => result,
                     Err(error) => panic!("{error:?}"),
                 };
-                visualization
-                    .decode(
-                        data,
-                        self.properties
-                            .as_ref()
-                            .map_or_else(Look::default, |properties| properties.clone().into()),
-                    )
-                    .unwrap()/*XXX*/;
-
-                Ok(PivotTable::new([]).into_item())
+                let pivot_table = visualization.decode(
+                    data,
+                    self.properties
+                        .as_ref()
+                        .map_or_else(Look::default, |properties| properties.clone().into()),
+                )?;
+
+                Ok(pivot_table.into_item().with_spv_info(
+                    SpvInfo::new(structure_member).with_members(SpvMembers::Legacy {
+                        xml: xml_member_name.clone(),
+                        binary: bin_member_name.clone(),
+                    }),
+                ))
             }
         }
     }
index f1e0e4634207eada99d51fcaed2f8986e778de4a..dd9a2ccb265e1b80e57a15e0a65d66fb60320e6a 100644 (file)
@@ -105,6 +105,7 @@ impl DataValue {
     }
 
     pub fn as_pivot_value(&self, format: Format) -> Value {
+        dbg!(format.type_());
         if format.type_().category() == Category::Date
             && let Some(s) = self.value.as_string()
             && let Ok(date_time) =
index fd91c341e42cfe55b8a85dc473c3cf0f363e86dd..2bfec0fbc3da1f35376e690535c05f156534e784 100644 (file)
@@ -367,8 +367,8 @@ impl Visualization {
             }
         }
 
-        fn decode_dimension(
-            variables: &[(&Series, usize)],
+        fn decode_dimension<'a>(
+            variables: &[(&'a Series, usize)],
             axes: &HashMap<usize, &Axis>,
             styles: &HashMap<&str, &Style>,
             a: Axis3,
@@ -376,7 +376,8 @@ impl Visualization {
             rotate_inner_column_labels: &mut bool,
             rotate_outer_row_labels: &mut bool,
             footnotes: &pivot::Footnotes,
-        ) -> Dimension {
+            dims: &mut Vec<Dim<'a>>,
+        ) {
             let base_level = variables[0].1;
             let show_label = if let Ok(a) = Axis2::try_from(a)
                 && let Some(axis) = axes.get(&(base_level + variables.len()))
@@ -419,26 +420,16 @@ impl Visualization {
                 .map(|(series, _level)| *series)
                 .collect::<Vec<_>>();
 
-            // Find the first row for each category, then drop missing
-            // categories and count what's left.
-            let max_cat = variables[0].max_category().unwrap()/*XXX*/;
-            let mut cat_rows = vec![None; max_cat + 1];
-            for (index, value) in variables[0].values.iter().enumerate() {
-                if let Some(row) = value.category() {
-                    cat_rows[row].get_or_insert(index);
-                }
-            }
-            let cat_rows = cat_rows.into_iter().flatten().collect::<Vec<_>>();
-
             // Make leaf categories.
-            let mut cats = Vec::with_capacity(cat_rows.len());
-            for (index, cat_row) in cat_rows.into_iter().enumerate() {
-                let dv = &variables[0].values[cat_row];
-                let name = variables[0].new_name(dv, footnotes);
-                cats.push((Category::from(Leaf::new(name)), index..index + 1));
-            }
-            if cats.is_empty() {
-                todo!()
+            let mut coordinate_to_index = HashMap::new();
+            let mut cats = Vec::new();
+            for (index, value) in variables[0].values.iter().enumerate() {
+                let Some(row) = value.category() else {
+                    continue;
+                };
+                coordinate_to_index.insert(row, index);
+                let name = variables[0].new_name(value, footnotes);
+                cats.push((Category::from(Leaf::new(name)), cats.len()..cats.len() + 1));
             }
 
             // Now group them, in one pass per grouping variable, innermost first.
@@ -473,7 +464,7 @@ impl Visualization {
                 cats = next_cats;
             }
 
-            Dimension::new(
+            let dimension = Dimension::new(
                 Group::new(
                     variables[0]
                         .label
@@ -482,7 +473,13 @@ impl Visualization {
                 )
                 .with_multiple(cats.into_iter().map(|(category, _range)| category))
                 .with_show_label(show_label),
-            )
+            );
+            dims.push(Dim {
+                axis: a,
+                dimension: Some(dimension),
+                coordinate: variables[0],
+                coordinate_to_index,
+            });
         }
 
         fn decode_dimensions<'a, 'b>(
@@ -496,8 +493,7 @@ impl Visualization {
             rotate_outer_row_labels: &mut bool,
             footnotes: &pivot::Footnotes,
             level_ofs: usize,
-            dimensions: &mut Vec<(Axis3, pivot::Dimension)>,
-            coordinates: &mut Vec<&'b Series>,
+            dims: &mut Vec<Dim<'b>>,
         ) {
             let variables = variables
                 .into_iter()
@@ -514,27 +510,6 @@ impl Visualization {
                 if let Some((var, level)) = var {
                     dim_vars.push((var, level));
                 } else if !dim_vars.is_empty() {
-                    coordinates.push(dim_vars[0].0);
-                    dimensions.push((
-                        a,
-                        decode_dimension(
-                            &dim_vars,
-                            axes,
-                            styles,
-                            a,
-                            look,
-                            rotate_inner_column_labels,
-                            rotate_outer_row_labels,
-                            footnotes,
-                        ),
-                    ));
-                    dim_vars.clear();
-                }
-            }
-            if !dim_vars.is_empty() {
-                coordinates.push(dim_vars[0].0);
-                dimensions.push((
-                    a,
                     decode_dimension(
                         &dim_vars,
                         axes,
@@ -544,9 +519,31 @@ impl Visualization {
                         rotate_inner_column_labels,
                         rotate_outer_row_labels,
                         footnotes,
-                    ),
-                ));
+                        dims,
+                    );
+                    dim_vars.clear();
+                }
             }
+            if !dim_vars.is_empty() {
+                decode_dimension(
+                    &dim_vars,
+                    axes,
+                    styles,
+                    a,
+                    look,
+                    rotate_inner_column_labels,
+                    rotate_outer_row_labels,
+                    footnotes,
+                    dims,
+                );
+            }
+        }
+
+        struct Dim<'a> {
+            axis: Axis3,
+            dimension: Option<pivot::Dimension>,
+            coordinate: &'a Series,
+            coordinate_to_index: HashMap<usize, usize>,
         }
 
         let mut rotate_inner_column_labels = false;
@@ -556,8 +553,7 @@ impl Visualization {
             .first()
             .map(|child| child.variables())
             .unwrap_or_default();
-        let mut dimensions = Vec::new();
-        let mut coordinates = Vec::new();
+        let mut dims = Vec::new();
         decode_dimensions(
             columns.into_iter().map(|vr| vr.reference.as_str()),
             &series,
@@ -569,8 +565,7 @@ impl Visualization {
             &mut rotate_outer_row_labels,
             &footnotes,
             1,
-            &mut dimensions,
-            &mut coordinates,
+            &mut dims,
         );
         let rows = cross
             .get(1)
@@ -587,8 +582,7 @@ impl Visualization {
             &mut rotate_outer_row_labels,
             &footnotes,
             1 + columns.len(),
-            &mut dimensions,
-            &mut coordinates,
+            &mut dims,
         );
 
         let mut level_ofs = columns.len() + rows.len() + 1;
@@ -604,16 +598,20 @@ impl Visualization {
                 &mut rotate_outer_row_labels,
                 &footnotes,
                 level_ofs,
-                &mut dimensions,
-                &mut coordinates,
+                &mut dims,
             );
             level_ofs += layers.len();
         }
 
+        let dimensions = dims
+            .iter_mut()
+            .map(|dim| (dim.axis, dim.dimension.take().unwrap()))
+            .collect::<Vec<_>>();
+
         let mut pivot_table = PivotTable::new(dimensions);
 
         let cell = series.get("cell").unwrap()/*XXX*/;
-        let mut coords = Vec::with_capacity(coordinates.len());
+        let mut coords = Vec::with_capacity(dims.len());
         let (cell_formats, format_map) = graph.interval.labeling.decode_format_map(&series);
         let cell_footnotes =
             graph
@@ -627,9 +625,14 @@ impl Visualization {
                 });
         for (i, cell) in cell.values.iter().enumerate() {
             coords.clear();
-            for series in &coordinates {
+            for dim in &dims {
                 // XXX indexing of values, and unwrap
-                coords.push(series.values[i].category().unwrap());
+                let coordinate = dim.coordinate.values[i].category().unwrap();
+                let index = match dim.coordinate_to_index.get(&coordinate) {
+                    Some(index) => *index,
+                    None => panic!("can't find {coordinate}"),
+                };
+                coords.push(index);
             }
 
             let format = if let Some(cell_formats) = &cell_formats {
index c2a0fd94b6384430df3464e1efa705b3f6c48f17..fa399f9067a5d37a5c605b7ee686dda2c0c03525 100644 (file)
@@ -42,7 +42,7 @@ pub struct ShowSpv {
     show_member_names: bool,
 }
 
-/// What to show in a system file.
+/// What to show in a viewer file.
 #[derive(Clone, Copy, Debug, PartialEq, ValueEnum)]
 enum Mode {
     /// List tables and other items.
@@ -54,6 +54,9 @@ enum Mode {
 
     /// Reads `.tlo` or `.stt` TableLook and outputs as `.stt` format.
     ConvertTableLook,
+
+    /// Prints contents.
+    View,
 }
 
 impl Mode {
@@ -62,6 +65,7 @@ impl Mode {
             Mode::Directory => "directory",
             Mode::GetTableLook => "get-table-look",
             Mode::ConvertTableLook => "convert-table-look",
+            Mode::View => "view",
         }
     }
 }
@@ -83,6 +87,14 @@ impl ShowSpv {
                 }
                 Ok(())
             }
+            Mode::View => {
+                let item = Item::from_spv_file(&self.input)?.0;
+                let item = self.criteria.apply(item);
+                for child in item.details.children() {
+                    println!("{child}");
+                }
+                Ok(())
+            }
             Mode::GetTableLook => todo!(),
             Mode::ConvertTableLook => todo!(),
         }