work
authorBen Pfaff <blp@cs.stanford.edu>
Sat, 11 Oct 2025 05:12:25 +0000 (22:12 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Sat, 11 Oct 2025 05:12:25 +0000 (22:12 -0700)
rust/pspp/src/message.rs
rust/pspp/src/output.rs
rust/pspp/src/show_spv.rs

index 28d0a9911ab8d4f7e9eba969798049ad0d0ef1bf..125de74687ac7ed5e1c457b8cd68ccbe56505dcf 100644 (file)
@@ -178,13 +178,13 @@ pub enum Category {
     Data,
 }
 
-#[derive(Serialize)]
+#[derive(Clone, Serialize)]
 pub struct Stack {
     location: Location,
     description: String,
 }
 
-#[derive(Debug, Default)]
+#[derive(Clone, Debug, Default)]
 pub struct Diagnostics(pub Vec<Diagnostic>);
 
 impl From<Diagnostic> for Diagnostics {
@@ -193,7 +193,7 @@ impl From<Diagnostic> for Diagnostics {
     }
 }
 
-#[derive(Serialize)]
+#[derive(Clone, Serialize)]
 pub struct Diagnostic {
     pub severity: Severity,
     pub category: Category,
index 1ea6c5c56119f82d0f4e9be942baa18ac6743dc9..563d5ef5fc70de507b06e1f2307594f257741d28 100644 (file)
@@ -18,6 +18,7 @@
 use std::{
     borrow::Cow,
     collections::BTreeMap,
+    mem::take,
     str::FromStr,
     sync::{Arc, OnceLock},
 };
@@ -45,7 +46,7 @@ mod spv;
 pub mod table;
 
 /// A single output item.
-#[derive(Debug, Serialize)]
+#[derive(Clone, Debug, Serialize)]
 pub struct Item {
     /// The localized label for the item that appears in the outline pane in the
     /// output viewer and in PDF outlines.  This is `None` if no label has been
@@ -121,7 +122,7 @@ where
     }
 }
 
-#[derive(Debug, Serialize)]
+#[derive(Clone, Debug, Serialize)]
 pub enum Details {
     Chart,
     Image,
@@ -343,7 +344,7 @@ impl ItemCursor {
 /// Information for output items that were read from an SPV file.
 ///
 /// This is for debugging and troubleshooting purposes.
-#[derive(Debug)]
+#[derive(Clone, Debug)]
 pub struct SpvInfo {
     /// True if there was an error reading the output item (e.g. because of
     /// corruption or because PSPP doesn't understand the format).
@@ -374,7 +375,7 @@ impl SpvInfo {
 }
 
 /// Identifies ZIP file members for one kind of output item in an SPV file.
-#[derive(Debug)]
+#[derive(Clone, Debug)]
 pub enum SpvMembers {
     /// Light detail members.
     Light(
@@ -464,7 +465,7 @@ impl Item {
     }
 }
 
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, PartialEq, Eq)]
 pub struct Selection {
     /// - `None`: Include all objects.
     /// - `Some(true)`: Include only visible objects.
@@ -515,10 +516,16 @@ impl Selection {
 
     pub fn parse_instances(s: &str) -> Result<Vec<isize>, anyhow::Error> {
         s.split(',')
-            .map(|s| match s.parse::<isize>() {
-                Ok(0) => Err(anyhow!("--instances values must be nonzero")),
-                Ok(n) => Ok(n),
-                Err(error) => Err(error.into()),
+            .map(|s| {
+                if s == "last" {
+                    Ok(-1)
+                } else {
+                    match s.parse::<isize>() {
+                        Ok(0) => Err(anyhow!("--instances values must be nonzero")),
+                        Ok(n) => Ok(n),
+                        Err(error) => Err(error.into()),
+                    }
+                }
             })
             .collect()
     }
@@ -566,9 +573,11 @@ impl Default for Selection {
     }
 }
 
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, PartialEq, Eq)]
 pub enum StringMatch {
+    /// Include any string that matches any of the patterns.
     Include(Vec<String>),
+    /// Include only the strings that do not match any of the patterns.
     Exclude(Vec<String>),
 }
 
@@ -611,10 +620,44 @@ impl FromStr for StringMatch {
     }
 }
 
-#[derive(Clone, Debug, Default)]
-pub struct Selections(pub Vec<Selection>);
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub struct Criteria(pub Vec<Selection>);
+
+impl Criteria {
+    /// Returns a new output item whose children are all the (direct and
+    /// indirect) children of `item` that meet the criteria.
+    fn apply(&self, mut item: Item) -> Item {
+        fn take_children(item: &mut Item) -> Vec<Arc<Item>> {
+            match &mut item.details {
+                Details::Group(items) => take(items),
+                _ => Vec::new(),
+            }
+        }
+        fn flatten_children(
+            children: Vec<Arc<Item>>,
+            depth: usize,
+            items: &mut Vec<Item>,
+            depths: &mut Vec<usize>,
+        ) {
+            for child in children {
+                flatten(Arc::unwrap_or_clone(child), depth, items, depths);
+            }
+        }
+        fn flatten(mut item: Item, depth: usize, items: &mut Vec<Item>, depths: &mut Vec<usize>) {
+            let children = take_children(&mut item);
+            items.push(item);
+            depths.push(depth);
+            flatten_children(children, depth + 1, items, depths);
+        }
+
+        let mut items = Vec::new();
+        let mut depths = Vec::new();
+        flatten_children(take_children(&mut item), 0, &mut items, &mut depths);
+        todo!()
+    }
+}
 
-impl FromArgMatches for Selections {
+impl FromArgMatches for Criteria {
     fn from_arg_matches(matches: &ArgMatches) -> Result<Self, clap::Error> {
         let mut this = Self::default();
         this.update_from_arg_matches(matches)?;
@@ -653,7 +696,6 @@ impl FromArgMatches for Selections {
             }
         }
 
-        println!("{:#?}", matches.ids());
         let mut values = BTreeMap::new();
         for id in matches.ids() {
             if matches.try_get_many::<clap::Id>(id.as_str()).is_ok() {
@@ -668,7 +710,7 @@ impl FromArgMatches for Selections {
                 continue;
             }
             match id.as_str() {
-                "or" => extract(matches, id, &mut values, |_: bool| Value::Or),
+                "_or" => extract(matches, id, &mut values, |_: bool| Value::Or),
                 "select" => extract(matches, id, &mut values, Value::Classes),
                 "commands" => extract(matches, id, &mut values, Value::Commands),
                 "subtypes" => extract(matches, id, &mut values, Value::Subtypes),
@@ -677,7 +719,7 @@ impl FromArgMatches for Selections {
                 "instances" => extract(matches, id, &mut values, Value::Instances),
                 "show-hidden" => extract(matches, id, &mut values, Value::ShowHidden),
                 "errors" => extract(matches, id, &mut values, Value::Errors),
-                _ => unreachable!(),
+                _ => unreachable!("{id}"),
             }
         }
 
@@ -688,7 +730,7 @@ impl FromArgMatches for Selections {
         let mut selection = Selection::default();
         for value in values.into_values() {
             match value {
-                Value::Or => self.0.push(std::mem::take(&mut selection)),
+                Value::Or => self.0.push(take(&mut selection)),
                 Value::Classes(classes) => selection.classes = classes,
                 Value::Commands(commands) => selection.commands = commands,
                 Value::Subtypes(subtypes) => selection.subtypes = subtypes,
@@ -704,7 +746,7 @@ impl FromArgMatches for Selections {
     }
 }
 
-impl Args for Selections {
+impl Args for Criteria {
     fn augment_args(cmd: clap::Command) -> clap::Command {
         SelectionArgs::augment_args(cmd.next_help_heading("Input selection options"))
     }
@@ -750,6 +792,7 @@ struct SelectionArgs {
 
     /// Include only XML and binary member names that match.  Without any member
     /// names, include all objects.
+    #[arg(long, required = false, action = ArgAction::Append)]
     pub members: Vec<String>,
 
     /// Separate two groups of selection options.
@@ -759,9 +802,10 @@ struct SelectionArgs {
 
 #[cfg(test)]
 mod tests {
+    use clap::Parser;
     use enumset::EnumSet;
 
-    use crate::output::{Class, Selection, StringMatch};
+    use crate::output::{Class, Criteria, Selection, StringMatch};
 
     #[test]
     fn parse_classes() {
@@ -790,6 +834,17 @@ mod tests {
         assert!(Selection::parse_nth_commands("0").is_err());
     }
 
+    #[test]
+    fn parse_instances() {
+        assert_eq!(Selection::parse_instances("1").unwrap(), vec![1]);
+        assert_eq!(
+            Selection::parse_instances("2,3,-2,-3").unwrap(),
+            vec![2, 3, -2, -3]
+        );
+        assert_eq!(Selection::parse_instances("last,1").unwrap(), vec![-1, 1]);
+        assert!(Selection::parse_instances("0").is_err());
+    }
+
     #[test]
     fn string_matches() {
         assert!(StringMatch::default().matches("xyzzy"));
@@ -810,4 +865,39 @@ mod tests {
         assert!(!m.matches("abc"));
         assert!(!m.matches("abcd"));
     }
+
+    #[test]
+    fn selections() {
+        #[derive(Parser, Debug, PartialEq, Eq)]
+        struct Command {
+            #[command(flatten)]
+            selections: Criteria,
+        }
+
+        let args = vec![
+            "myprog",
+            "--select=tables",
+            "--or",
+            "--commands=Frequencies,Descriptives",
+        ];
+        let matches = Command::parse_from(args);
+        assert_eq!(
+            matches,
+            Command {
+                selections: Criteria(vec![
+                    Selection {
+                        classes: EnumSet::only(Class::Tables),
+                        ..Selection::default()
+                    },
+                    Selection {
+                        commands: StringMatch::Include(vec![
+                            String::from("Frequencies"),
+                            String::from("Descriptives")
+                        ]),
+                        ..Selection::default()
+                    }
+                ])
+            }
+        );
+    }
 }
index 35ef9786ae1f8adafbea069253500542a6b6c1db..3a680c2dd2a47f310a72b883388788bf4532fd7c 100644 (file)
@@ -16,7 +16,7 @@
 
 use anyhow::Result;
 use clap::{Args, ValueEnum};
-use pspp::output::Selections;
+use pspp::output::Criteria;
 use std::{fmt::Display, path::PathBuf};
 
 /// Show information about SPSS viewer files (SPV files).
@@ -33,8 +33,9 @@ pub struct ShowSpv {
     #[arg(required = true)]
     input: PathBuf,
 
+    /// Input selection options.
     #[command(flatten)]
-    selection: Selections,
+    selection: Criteria,
 }
 
 /// What to show in a system file.