work on tests
authorBen Pfaff <blp@cs.stanford.edu>
Fri, 23 May 2025 15:29:39 +0000 (08:29 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Fri, 23 May 2025 15:29:39 +0000 (08:29 -0700)
rust/pspp/src/dictionary.rs
rust/pspp/src/sys/cooked.rs
rust/pspp/src/sys/test.rs

index 9653f057780eb6738d71e083ab938ad45e02b5be..99f91e3dbb4ef2e87ce5bf998052ea0f99f60717 100644 (file)
@@ -678,34 +678,6 @@ impl<'a> OutputValueLabels<'a> {
         }
         Some(pt)
     }
-
-    fn get_field_value(index: usize, variable: &Variable, field: VariableField) -> Option<Value> {
-        match field {
-            VariableField::Position => Some(Value::new_integer(Some(index as f64 + 1.0))),
-            VariableField::Label => variable.label().map(|label| Value::new_user_text(label)),
-            VariableField::Measure => variable
-                .measure
-                .map(|measure| Value::new_text(measure.as_str())),
-            VariableField::Role => Some(Value::new_text(variable.role.as_str())),
-            VariableField::Width => Some(Value::new_integer(Some(variable.display_width as f64))),
-            VariableField::Alignment => Some(Value::new_text(variable.alignment.as_str())),
-            VariableField::PrintFormat => {
-                Some(Value::new_user_text(variable.print_format.to_string()))
-            }
-            VariableField::WriteFormat => {
-                Some(Value::new_user_text(variable.write_format.to_string()))
-            }
-            VariableField::MissingValues if !variable.missing_values.is_empty() => {
-                Some(Value::new_user_text(
-                    variable
-                        .missing_values
-                        .display(variable.encoding)
-                        .to_string(),
-                ))
-            }
-            VariableField::MissingValues => None,
-        }
-    }
 }
 
 #[derive(Copy, Clone, Debug, Enum)]
index 139a191682a368019c15c13f18dafec0ebd0b125..17548623e630d75721da161b7c5b58b3b528f8b8 100644 (file)
@@ -3,12 +3,13 @@ use std::{cell::RefCell, collections::HashMap, ops::Range, rc::Rc};
 
 use crate::{
     dictionary::{
-        Dictionary, InvalidRole, MultipleResponseSet, MultipleResponseType, Datum, VarWidth,
+        Datum, Dictionary, InvalidRole, MultipleResponseSet, MultipleResponseType, VarWidth,
         Variable, VariableSet,
     },
     endian::Endian,
     format::{Error as FormatError, Format, UncheckedFormat},
     identifier::{ByIdentifier, Error as IdError, Identifier},
+    output::pivot::{Group, Value},
     sys::{
         encoding::Error as EncodingError,
         raw::{
@@ -377,6 +378,50 @@ pub struct Metadata {
 }
 
 impl Metadata {
+    fn to_pivot_rows(&self) -> (Group, Vec<Value>) {
+        let mut group = Group::new("File Metadata");
+        let mut values = Vec::new();
+
+        group.push("Created");
+        // XXX use datetime format
+        values.push(Value::new_text(
+            self.creation.format("%e %b %y %H:%M:%S").to_string(),
+        ));
+
+        group.push("Product");
+        if let Some(product_ext) = &self.product_ext {
+            values.push(Value::new_text(format!(
+                "{} ({})",
+                self.product, product_ext
+            )));
+        } else {
+            values.push(Value::new_text(&self.product));
+        };
+
+        if let Some(version) = &self.version {
+            group.push("Product Version");
+            values.push(Value::new_text(format!(
+                "{}.{}.{}",
+                version.0, version.1, version.2
+            )));
+        }
+
+        group.push("Compression");
+        values.push(Value::new_text(match self.compression {
+            Some(Compression::Simple) => "SAV",
+            Some(Compression::ZLib) => "ZSAV",
+            None => "None",
+        }));
+
+        group.push("Number of Cases");
+        values.push(match self.n_cases {
+            Some(n_cases) => Value::new_integer(Some(n_cases as f64)),
+            None => Value::new_text("Unknown"),
+        });
+
+        (group, values)
+    }
+
     fn decode(headers: &Headers, mut warn: impl FnMut(Error)) -> Self {
         let header = &headers.header;
         let creation_date = NaiveDate::parse_from_str(&header.creation_date, "%e %b %y")
@@ -600,7 +645,11 @@ pub fn decode(
 
         for dict_index in dict_indexes {
             let variable = dictionary.variables.get_index_mut2(dict_index).unwrap();
-            for ValueLabel { datum: value, label } in record.labels.iter().cloned() {
+            for ValueLabel {
+                datum: value,
+                label,
+            } in record.labels.iter().cloned()
+            {
                 let value = value.decode(variable.width);
                 variable.value_labels.insert(value, label);
             }
index a25e2370ddb4ceb647b11bfbdebf99cd6c2b78fb..76e4f476ea2d2e5950bd542e92832e4f0d664b7c 100644 (file)
@@ -153,15 +153,8 @@ s16 "23456789abc"; s32 "defghijklmnopqstuvwxyzABC";
         assert_eq!(
             metadata,
             Metadata {
-                creation: NaiveDate::from_ymd_opt(2011, 1, 5)
-                    .unwrap()
-                    .and_time(NaiveTime::from_hms_opt(20, 53, 52).unwrap()),
-                endian,
-                compression: None,
-                n_cases: Some(1),
                 product: "$(#) SPSS DATA FILE PSPP synthetic test file".into(),
-                product_ext: None,
-                version: Some((1, 2, 3)),
+                ..test_metadata(endian)
             }
         );
         assert_eq!(
@@ -203,6 +196,163 @@ s16 "23456789abc"; s32 "defghijklmnopqstuvwxyzABC";
     }
 }
 
+fn test_metadata(endian: Endian) -> Metadata {
+    Metadata {
+        creation: NaiveDate::from_ymd_opt(2011, 1, 5)
+            .unwrap()
+            .and_time(NaiveTime::from_hms_opt(20, 53, 52).unwrap()),
+        endian,
+        compression: None,
+        n_cases: Some(1),
+        product: "$(#) SPSS DATA FILE PSPP synthetic test file".into(),
+        product_ext: None,
+        version: Some((1, 2, 3)),
+    }
+}
+
+#[test]
+fn unspecified_number_of_variable_positions() {
+    for endian in all::<Endian>() {
+        let input = r#"
+# File header.
+"$FL2"; s60 "$(#) SPSS DATA FILE PSPP synthetic test file";
+2; # Layout code
+-1; # Nominal case size (unspecified)
+0; # Not compressed
+0; # Not weighted
+1; # 1 case.
+100.0; # Bias.
+"05 Jan 11"; "20:53:52"; s64 "PSPP synthetic test file";
+i8 0 *3;
+
+# Numeric variable, no label or missing values.
+2; 0; 0; 0; 0x050800 *2; s8 "NUM1";
+
+# Numeric variable, variable label.
+2; 0; 1; 0; 0x050800 *2; s8 "NUM2";
+26; "Numeric variable 2's label"; i8 0 *2;
+
+# Character encoding record.
+7; 20; 1; 12; "windows-1252";
+
+# Dictionary termination record.
+999; 0;
+
+# Data.
+1.0; 2.0;
+"#;
+        let sysfile = sack(input, None, endian).unwrap();
+        let cursor = Cursor::new(sysfile);
+        let reader = Reader::new(cursor, |warning| println!("{warning}")).unwrap();
+        let headers: Vec<Record> = reader.collect::<Result<Vec<_>, _>>().unwrap();
+        let encoding = encoding_from_headers(&headers, &|e| eprintln!("{e}")).unwrap();
+        let decoder = Decoder::new(encoding, |e| eprintln!("{e}"));
+        let mut decoded_records = Vec::new();
+        for header in headers {
+            decoded_records.push(header.decode(&decoder).unwrap());
+        }
+
+        let mut errors = Vec::new();
+        let headers = Headers::new(decoded_records, &mut |e| errors.push(e)).unwrap();
+        let (dictionary, metadata) = decode(headers, encoding, |e| errors.push(e)).unwrap();
+        assert_eq!(errors, vec![]);
+        assert_eq!(
+            metadata,
+            Metadata {
+                version: None,
+                ..test_metadata(endian)
+            }
+        );
+        assert_eq!(
+            dictionary.file_label.as_ref().map(|s| s.as_str()),
+            Some("PSPP synthetic test file")
+        );
+        assert!(dictionary.output_value_labels().to_pivot_table().is_none());
+        let pt = dictionary.output_variables().to_pivot_table();
+        assert_rendering("value_labels_dictionary", &pt, "\
+╭──────────────────────────┬────────┬──────────────────────────┬─────────────────┬─────┬─────┬─────────┬────────────┬────────────┬──────────────╮
+│                          │Position│           Label          │Measurement Level│ Role│Width│Alignment│Print Format│Write Format│Missing Values│
+├──────────────────────────┼────────┼──────────────────────────┼─────────────────┼─────┼─────┼─────────┼────────────┼────────────┼──────────────┤
+│num1                      │       1│                          │                 │Input│    8│Right    │F8.0        │F8.0        │              │
+│Numeric variable 2's label│       2│Numeric variable 2's label│                 │Input│    8│Right    │F8.0        │F8.0        │              │
+╰──────────────────────────┴────────┴──────────────────────────┴─────────────────┴─────┴─────┴─────────┴────────────┴────────────┴──────────────╯
+");
+    }
+}
+
+#[test]
+fn wrong_variable_positions_but_v13() {
+    for endian in all::<Endian>() {
+        let input = r#"
+# File header.
+"$FL2"; s60 "$(#) SPSS DATA FILE PSPP synthetic test file";
+2; # Layout code
+-1; # Nominal case size (unspecified)
+0; # Not compressed
+0; # Not weighted
+1; # 1 case.
+100.0; # Bias.
+"01 Jan 11"; "20:53:52"; s64 "PSPP synthetic test file";
+i8 0 *3;
+
+# Numeric variable, no label or missing values.
+2; 0; 0; 0; 0x050800 *2; s8 "NUM1";
+
+# Numeric variable, variable label.
+2; 0; 1; 0; 0x050800 *2; s8 "NUM2";
+26; "Numeric variable 2's label"; i8 0 *2;
+
+# Machine integer info record (SPSS 13).
+7; 3; 4; 8; 13; 2; 3; -1; 1; 1; ENDIAN; 1252;
+
+# Character encoding record.
+7; 20; 1; 12; "windows-1252";
+
+# Dictionary termination record.
+999; 0;
+
+# Data.
+1.0; 2.0;
+"#;
+        let sysfile = sack(input, None, endian).unwrap();
+        let cursor = Cursor::new(sysfile);
+        let reader = Reader::new(cursor, |warning| println!("{warning}")).unwrap();
+        let headers: Vec<Record> = reader.collect::<Result<Vec<_>, _>>().unwrap();
+        let encoding = encoding_from_headers(&headers, &|e| eprintln!("{e}")).unwrap();
+        let decoder = Decoder::new(encoding, |e| eprintln!("{e}"));
+        let mut decoded_records = Vec::new();
+        for header in headers {
+            decoded_records.push(header.decode(&decoder).unwrap());
+        }
+
+        let mut errors = Vec::new();
+        let headers = Headers::new(decoded_records, &mut |e| errors.push(e)).unwrap();
+        let (dictionary, metadata) = decode(headers, encoding, |e| errors.push(e)).unwrap();
+        assert_eq!(errors, vec![]);
+        assert_eq!(
+            metadata,
+            Metadata {
+                version: None,
+                ..test_metadata(endian)
+            }
+        );
+        assert_eq!(
+            dictionary.file_label.as_ref().map(|s| s.as_str()),
+            Some("PSPP synthetic test file")
+        );
+        assert!(dictionary.output_value_labels().to_pivot_table().is_none());
+        let pt = dictionary.output_variables().to_pivot_table();
+        assert_rendering("value_labels_dictionary", &pt, "\
+╭──────────────────────────┬────────┬──────────────────────────┬─────────────────┬─────┬─────┬─────────┬────────────┬────────────┬──────────────╮
+│                          │Position│           Label          │Measurement Level│ Role│Width│Alignment│Print Format│Write Format│Missing Values│
+├──────────────────────────┼────────┼──────────────────────────┼─────────────────┼─────┼─────┼─────────┼────────────┼────────────┼──────────────┤
+│num1                      │       1│                          │                 │Input│    8│Right    │F8.0        │F8.0        │              │
+│Numeric variable 2's label│       2│Numeric variable 2's label│                 │Input│    8│Right    │F8.0        │F8.0        │              │
+╰──────────────────────────┴────────┴──────────────────────────┴─────────────────┴─────┴─────┴─────────┴────────────┴────────────┴──────────────╯
+");
+    }
+}
+
 #[test]
 fn value_labels() {
     for endian in all::<Endian>() {
@@ -332,28 +482,14 @@ COUNT("abcdefghijklmnopq"); COUNT("value label for `abcdefghijklmnopq'");
         let headers = Headers::new(decoded_records, &mut |e| errors.push(e)).unwrap();
         let (dictionary, metadata) = decode(headers, encoding, |e| errors.push(e)).unwrap();
         assert_eq!(errors, vec![]);
-        assert_eq!(
-            metadata,
-            Metadata {
-                creation: NaiveDate::from_ymd_opt(2011, 1, 5)
-                    .unwrap()
-                    .and_time(NaiveTime::from_hms_opt(20, 53, 52).unwrap()),
-                endian,
-                compression: None,
-                n_cases: Some(1),
-                product: "$(#) SPSS DATA FILE PSPP synthetic test file".into(),
-                product_ext: None,
-                version: Some((1, 2, 3)),
-            }
-        );
+        assert_eq!(metadata, test_metadata(endian));
         assert_eq!(
             dictionary.file_label.as_ref().map(|s| s.as_str()),
             Some("PSPP synthetic test file")
         );
-        let pt = dictionary.output_value_labels().to_pivot_table().unwrap();
         assert_rendering(
             "value_labels_value_labels",
-            &pt,
+            &dictionary.output_value_labels().to_pivot_table().unwrap(),
             "\
 ╭────────────────────────────────┬───────────────────────────────────────────────────────────────╮
 │Variable Value                  │                                                               │