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::{
}
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")
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);
}
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!(
}
}
+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>() {
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 │ │