Add show-legacy-series command.
authorBen Pfaff <blp@cs.stanford.edu>
Sat, 3 Jan 2026 00:49:18 +0000 (16:49 -0800)
committerBen Pfaff <blp@cs.stanford.edu>
Sat, 3 Jan 2026 00:49:18 +0000 (16:49 -0800)
rust/pspp/src/cli/show_spv.rs
rust/pspp/src/spv/read.rs
rust/pspp/src/spv/read/legacy_xml.rs
rust/pspp/src/spv/read/structure.rs

index cdc46a8ff404c24f372445af8c9978d10fe41271..0a2cfdcf0e0b1c529b119149d2f51b05d1e7aa57 100644 (file)
@@ -25,13 +25,13 @@ use pspp::{
     spv::{
         SpvArchive,
         legacy_bin::LegacyBin,
-        read::{ReadSeek, structure::OutlineItem},
+        read::{ReadSeek, legacy_xml::Visualization, structure::OutlineItem},
     },
 };
 use std::{
     collections::HashMap,
     fmt::Display,
-    io::{Cursor, Read},
+    io::{BufReader, Cursor, Read},
     path::PathBuf,
     sync::Arc,
 };
@@ -83,8 +83,17 @@ enum Mode {
     ConvertTableLook,
 
     /// Print data values in legacy tables.
+    ///
+    /// Data values come from `_tableData.bin` members inside the SPV files.
+    /// They do not require reading the corresponding `_table.xml` files.
     LegacyData,
 
+    /// Print data series in legacy tables.
+    ///
+    /// The series come from `_tableData.bin` members inside the SPV files, as
+    /// transformed by instructions in their paired `_table.xml` files.
+    LegacySeries,
+
     /// Prints contents.
     View,
 }
@@ -96,6 +105,7 @@ impl Mode {
             Mode::GetTableLook => "get-table-look",
             Mode::ConvertTableLook => "convert-table-look",
             Mode::LegacyData => "legacy-data",
+            Mode::LegacySeries => "legacy-series",
             Mode::View => "view",
         }
     }
@@ -113,6 +123,7 @@ impl ShowSpv {
             Mode::Directory => self.directory(),
             Mode::View => self.view(),
             Mode::LegacyData => self.legacy_data(),
+            Mode::LegacySeries => self.legacy_series(),
             Mode::GetTableLook => todo!(),
             Mode::ConvertTableLook => todo!(),
         }
@@ -205,6 +216,76 @@ impl ShowSpv {
         }
         Ok(())
     }
+
+    fn legacy_series(self) -> Result<()> {
+        let (mut archive, items) = self.read_outline()?;
+        for item in items {
+            for item in item.iter_in_order() {
+                if let Some(spv_info) = item.spv_info()
+                    && let Some(members) = &spv_info.members
+                    && let SpvMembers::LegacyTable { xml, binary } = &members
+                {
+                    /// Read and decode binary file.
+                    let mut bin_member = archive.0.by_name(&binary)?;
+                    let mut bin_data = Vec::with_capacity(bin_member.size() as usize);
+                    bin_member.read_to_end(&mut bin_data)?;
+                    let mut cursor = Cursor::new(bin_data);
+                    let legacy_bin = LegacyBin::read(&mut cursor).map_err(|e| {
+                        e.with_message(format!(
+                            "While parsing {binary:?} as legacy binary SPV member"
+                        ))
+                    })?;
+                    let data = legacy_bin.decode(&mut |w| eprintln!("{w}"));
+                    drop(bin_member);
+
+                    /// Read decode series in XML file.
+                    let member = BufReader::new(archive.0.by_name(&xml)?);
+                    let visualization: Visualization = match serde_path_to_error::deserialize(
+                        &mut quick_xml::de::Deserializer::from_reader(member),
+                    ) {
+                        Ok(result) => result,
+                        Err(error) => panic!("{error:?}"),
+                    };
+                    let series = visualization.decode_series(data, &mut |w| {
+                        eprintln!("{w}");
+                    });
+
+                    let n_values = series
+                        .values()
+                        .map(|map| map.values.len())
+                        .max()
+                        .unwrap_or(0);
+                    let index = Dimension::new(
+                        Group::new("Index")
+                            .with_multiple(Leaf::numbers(0..n_values))
+                            .with_label_shown(),
+                    );
+                    let variables = Dimension::new(Group::new("Series").with_multiple(
+                        series.values().map(|series| {
+                            series
+                                .name
+                                .replace("categories", "\ncategories")
+                                .replace("labels", "\nlabels")
+                                .replace("group", "\ngroup")
+                                .replace("Label", "\nLabel")
+                        }),
+                    ));
+                    let mut pivot_table =
+                        PivotTable::new([(Axis3::Y, index), (Axis3::X, variables)]);
+                    for (series_index, series) in series.values().enumerate() {
+                        for (value_index, data_value) in series.values.iter().enumerate() {
+                            pivot_table.insert(
+                                [value_index, series_index],
+                                Value::new_datum(&data_value.value),
+                            );
+                        }
+                    }
+                    println!("{pivot_table}");
+                }
+            }
+        }
+        Ok(())
+    }
 }
 
 fn print_item_directory<T>(item: &T, level: usize, show_member_names: bool)
index 8cb5987df4cfd8faec1aa00de52857a6339e236f..78902bf9e5e606d0d79376a93733c30bfb34af9f 100644 (file)
@@ -41,7 +41,7 @@ use crate::{
 mod css;
 pub mod html;
 pub mod legacy_bin;
-mod legacy_xml;
+pub mod legacy_xml;
 mod light;
 pub mod structure;
 #[cfg(test)]
index d4af8374adc4d0d80eb1b0e86a76cfff4a467ee6..b953dcd4029fb03782c9cbb47e70fcd53b7213cc 100644 (file)
@@ -647,7 +647,7 @@ impl Visualization {
 
     pub fn decode(
         &self,
-        data: IndexMap<String, IndexMap<String, Vec<DataValue>>>,
+        binary_data: IndexMap<String, IndexMap<String, Vec<DataValue>>>,
         look: Look,
         warn: &mut dyn FnMut(LegacyXmlWarning),
     ) -> Result<PivotTable, super::Error> {
@@ -692,7 +692,7 @@ impl Visualization {
         };
         let caption = LabelFrame::decode_label(caption_labels, &footnotes);
 
-        let series = self.decode_series(data, warn);
+        let series = self.decode_series(binary_data, warn);
         let (mut dims, current_layer) = self.decode_dimensions(graph, &series, &footnotes);
 
         let cell_footnotes = graph
@@ -742,9 +742,9 @@ impl Visualization {
 }
 
 pub struct Series {
-    name: String,
+    pub name: String,
     label: Option<String>,
-    values: Vec<DataValue>,
+    pub values: Vec<DataValue>,
     map: Map,
     affixes: Vec<Affix>,
     coordinate_to_index: RefCell<BTreeMap<usize, CategoryLocator>>,
index 0c204914bbca50f8e993f0d68443c0bc8710398c..a2ad34871a4aad0eef5f5780eadb4ab04211ad7c 100644 (file)
@@ -330,7 +330,7 @@ impl Table {
             let pivot_table =
                 visualization.decode(data, *self.look.unwrap_or_default(), &mut |w| {
                     warn(Warning {
-                        member: bin_member_name.clone(),
+                        member: xml_member_name.clone(),
                         details: WarningDetails::LegacyXmlWarning(w),
                     })
                 })?;