None => values.push(Value::empty()),
}
+ group.push("Documents");
+ values.push(Value::new_user_text(
+ self.documents
+ .iter()
+ .flat_map(|s| [s.as_str(), "\n"])
+ .collect::<String>(),
+ ));
+
(group, values)
}
}
}
impl Item {
- pub fn new(details: Details) -> Self {
+ pub fn new(details: impl Into<Details>) -> Self {
+ let details = details.into();
Self {
label: None,
command_name: details.command_name().cloned(),
show: true,
- details,
+ details: details,
}
}
}
}
+impl<T> From<T> for Item
+where
+ T: Into<Details>,
+{
+ fn from(value: T) -> Self {
+ Self::new(value)
+ }
+}
+
pub enum Details {
Chart,
Image,
Group(Vec<Arc<Item>>),
- Message(Diagnostic),
+ Message(Box<Diagnostic>),
PageBreak,
Table(Box<PivotTable>),
Text(Box<Text>),
}
}
+impl From<Diagnostic> for Details {
+ fn from(value: Diagnostic) -> Self {
+ Self::Message(Box::new(value))
+ }
+}
+
+impl From<Box<Diagnostic>> for Details {
+ fn from(value: Box<Diagnostic>) -> Self {
+ Self::Message(value)
+ }
+}
+
+impl From<PivotTable> for Details {
+ fn from(value: PivotTable) -> Self {
+ Self::Table(Box::new(value))
+ }
+}
+
+impl From<Box<PivotTable>> for Details {
+ fn from(value: Box<PivotTable>) -> Self {
+ Self::Table(value)
+ }
+}
+
+impl From<Text> for Details {
+ fn from(value: Text) -> Self {
+ Self::Text(Box::new(value))
+ }
+}
+
+impl From<Box<Text>> for Details {
+ fn from(value: Box<Text>) -> Self {
+ Self::Text(value)
+ }
+}
+
pub struct Text {
type_: TextType,
content: Value,
}
-impl From<&Diagnostic> for Text {
- fn from(value: &Diagnostic) -> Self {
- Text {
+impl Text {
+ pub fn new_log(s: impl Into<String>) -> Self {
+ Self {
type_: TextType::Log,
- content: Value::new_user_text(value.to_string()),
+ content: Value::new_user_text(s),
}
}
}
+impl From<&Diagnostic> for Text {
+ fn from(value: &Diagnostic) -> Self {
+ Self::new_log(value.to_string())
+ }
+}
+
pub enum TextType {
/// `TITLE` and `SUBTITLE` commands.
PageTitle,
-use std::{fs::File, path::Path, sync::Arc};
+use std::{fmt::Display, fs::File, path::Path, sync::Arc};
use enum_map::EnumMap;
}
#[track_caller]
-pub fn assert_rendering(name: &str, pivot_table: &PivotTable, expected: &str) {
- let actual = pivot_table.to_string();
- if actual != expected {
- eprintln!("Unexpected pivot table rendering:\n--- expected\n+++ actual");
+pub fn assert_lines_eq<E, A>(expected: &str, expected_name: E, actual: &str, actual_name: A)
+where
+ E: Display,
+ A: Display,
+{
+ if expected != actual {
+ eprintln!("Unexpected output:\n--- {expected_name}\n+++ {actual_name}");
for result in diff::lines(expected, &actual) {
- match result {
- diff::Result::Left(line) => eprintln!("-{line:?}"),
- diff::Result::Both(line, _) => eprintln!(" {line:?}"),
- diff::Result::Right(line) => eprintln!("+{line:?}"),
- }
+ let (prefix, line) = match result {
+ diff::Result::Left(line) => ('-', line),
+ diff::Result::Both(line, _) => (' ', line),
+ diff::Result::Right(line) => ('+', line),
+ };
+ let suffix = if line.trim_end().len() != line.len() {
+ "$"
+ } else {
+ ""
+ };
+ eprintln!("{prefix}{line}{suffix}");
}
panic!();
}
+}
+
+#[track_caller]
+pub fn assert_rendering(name: &str, pivot_table: &PivotTable, expected: &str) {
+ assert_lines_eq(
+ expected,
+ format!("{name} expected"),
+ &pivot_table.to_string(),
+ format!("{name} actual"),
+ );
let item = Arc::new(Item::new(Details::Table(Box::new(pivot_table.clone()))));
if let Some(dir) = std::env::var_os("PSPP_TEST_HTML_DIR") {
.unwrap();
}
super::Details::Message(diagnostic) => {
- self.write_text(item, &diagnostic.into(), structure)
+ self.write_text(item, &Text::from(diagnostic.as_ref()), structure)
}
super::Details::PageBreak => {
self.needs_page_break = true;
use std::{
borrow::Cow,
- fmt::Display,
+ fmt::{Display, Error as FmtError, Result as FmtResult, Write as FmtWrite},
fs::File,
- io::{BufWriter, Write},
+ io::{BufWriter, Write as IoWrite},
ops::{Index, Range},
sync::{Arc, LazyLock},
};
impl Display for DisplayPivotTable<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- for line in TextRenderer::default().render(self.pt) {
- writeln!(f, "{}", line)?;
- }
- Ok(())
+ TextRenderer::default().render_table(self.pt, f)
+ }
+}
+
+impl Display for Item {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ TextRenderer::default().render(self, f)
}
}
}
impl TextRenderer {
- fn render(&mut self, table: &PivotTable) -> Vec<TextLine> {
- let mut output = Vec::new();
+ fn render<W>(&mut self, item: &Item, writer: &mut W) -> FmtResult
+ where
+ W: FmtWrite,
+ {
+ match &item.details {
+ Details::Chart => todo!(),
+ Details::Image => todo!(),
+ Details::Group(children) => {
+ for (index, child) in children.iter().enumerate() {
+ if index > 0 {
+ writeln!(writer)?;
+ }
+ self.render(child, writer)?;
+ }
+ Ok(())
+ }
+ Details::Message(_diagnostic) => todo!(),
+ Details::PageBreak => Ok(()),
+ Details::Table(pivot_table) => self.render_table(&*pivot_table, writer),
+ Details::Text(_text) => todo!(),
+ }
+ }
+
+ fn render_table<W>(&mut self, table: &PivotTable, writer: &mut W) -> FmtResult
+ where
+ W: FmtWrite,
+ {
for (index, layer_indexes) in table.layers(true).enumerate() {
if index > 0 {
- output.push(TextLine::new());
+ writeln!(writer)?;
}
let mut pager = Pager::new(self, table, Some(layer_indexes.as_slice()));
while pager.has_next(self) {
pager.draw_next(self, usize::MAX);
- output.append(&mut self.lines);
+ for line in self.lines.drain(..) {
+ writeln!(writer, "{}", line)?;
+ }
}
}
- output
+ Ok(())
}
fn layout_cell(&self, text: &str, bb: Rect2) -> Coord2 {
}
fn write(&mut self, item: &Arc<Item>) {
- match &item.details {
- Details::Chart => todo!(),
- Details::Image => todo!(),
- Details::Group(_) => todo!(),
- Details::Message(_diagnostic) => todo!(),
- Details::PageBreak => (),
- Details::Table(pivot_table) => {
- for line in self.renderer.render(pivot_table) {
- writeln!(self.file, "{}", line.str()).unwrap();
- }
- }
- Details::Text(_text) => todo!(),
- }
+ let _ = self.renderer.render(item, &mut FmtAdapter(&mut self.file));
+ }
+}
+
+struct FmtAdapter<W>(W);
+
+impl<W> FmtWrite for FmtAdapter<W>
+where
+ W: IoWrite,
+{
+ fn write_str(&mut self, s: &str) -> FmtResult {
+ self.0.write_all(s.as_bytes()).map_err(|_| FmtError)
}
}
error::Error as StdError,
fmt::{Display, Formatter, Result as FmtResult},
iter::repeat_n,
+ path::{Path, PathBuf},
};
use crate::endian::{Endian, ToBytes};
#[derive(Debug)]
pub struct Error {
- pub file_name: Option<String>,
+ pub file_name: Option<PathBuf>,
pub line_number: Option<usize>,
pub token: Option<String>,
pub message: String,
impl Error {
fn new(
- file_name: Option<&str>,
+ file_name: Option<&Path>,
line_number: Option<usize>,
token: Option<&str>,
message: String,
) -> Error {
Error {
- file_name: file_name.map(String::from),
+ file_name: file_name.map(PathBuf::from),
line_number,
token: token.map(String::from),
message,
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
match (self.file_name.as_ref(), self.line_number) {
- (Some(ref file_name), Some(line_number)) => write!(f, "{file_name}:{line_number}: ")?,
- (Some(ref file_name), None) => write!(f, "{file_name}: ")?,
+ (Some(ref file_name), Some(line_number)) => {
+ write!(f, "{}:{line_number}: ", file_name.display())?
+ }
+ (Some(ref file_name), None) => write!(f, "{}: ", file_name.display())?,
(None, Some(line_number)) => write!(f, "line {line_number}: ")?,
(None, None) => (),
}
}
}
-pub fn sack(input: &str, input_file_name: Option<&str>, endian: Endian) -> Result<Vec<u8>> {
+pub fn sack(input: &str, input_file_name: Option<&Path>, endian: Endian) -> Result<Vec<u8>> {
let mut symbol_table = HashMap::new();
let output = _sack(input, input_file_name, endian, &mut symbol_table)?;
let output = if !symbol_table.is_empty() {
fn _sack(
input: &str,
- input_file_name: Option<&str>,
+ input_file_name: Option<&Path>,
endian: Endian,
symbol_table: &mut HashMap<String, Option<u32>>,
) -> Result<Vec<u8>> {
struct Lexer<'a> {
input: &'a str,
token: Option<(Token, &'a str)>,
- input_file_name: Option<&'a str>,
+ input_file_name: Option<&'a Path>,
line_number: usize,
endian: Endian,
}
}
impl<'a> Lexer<'a> {
- fn new(input: &'a str, input_file_name: Option<&'a str>, endian: Endian) -> Result<Lexer<'a>> {
+ fn new(input: &'a str, input_file_name: Option<&'a Path>, endian: Endian) -> Result<Lexer<'a>> {
let mut lexer = Lexer {
input,
token: None,
-use std::io::Cursor;
+use std::{io::Cursor, path::Path, sync::Arc};
use crate::{
endian::Endian,
- output::pivot::{test::assert_rendering, Axis3, Dimension, PivotTable},
+ output::{
+ pivot::{
+ test::{assert_lines_eq, assert_rendering},
+ Axis3, Dimension, PivotTable,
+ },
+ Details, Item, Text,
+ },
sys::{
cooked::{decode, Headers, Metadata},
raw::{encoding_from_headers, Decoder, Reader, Record},
#[test]
fn variable_labels_and_missing_values() {
- for endian in all::<Endian>() {
- let input = r#"
-# File header.
-"$FL2"; s60 "$(#) SPSS DATA FILE PSPP synthetic test file";
-2; # Layout code
-28; # Nominal case size
-0; # Not compressed
-0; # Not weighted
-1; # 1 case.
-100.0; # Bias.
-"05 Jan 11"; "20:53:52";
-"PSPP synthetic test file: "; i8 244; i8 245; i8 246; i8 248; s34 "";
-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";
-32; "Numeric variable 2's label ("; i8 249; i8 250; i8 251; ")";
-
-# Numeric variable, one missing value.
-2; 0; 0; 1; 0x050800 *2; s8 "NUM3";
-1.0;
-
-# Numeric variable, variable label and missing value.
-2; 0; 1; 1; 0x050800 *2; s8 "NUM4";
-30; "Another numeric variable label"; i8 0 * 2;
-1.0;
-
-# Numeric variable, two missing values.
-2; 0; 0; 2; 0x050800 *2; s8 "NUM5"; 1.0; 2.0;
-
-# Numeric variable, three missing values.
-2; 0; 0; 3; 0x050800 *2; s8 "NUM6"; 1.0; 2.0; 3.0;
-
-# Numeric variable, range of missing values.
-2; 0; 0; -2; 0x050800 *2; s8 "NUM7"; 1.0; 3.0;
-
-# Numeric variables, range of missing values plus discrete value.
-2; 0; 0; -3; 0x050800 *2; s8 "NUM8"; 1.0; 3.0; 5.0;
-2; 0; 0; -3; 0x050800 *2; s8 "NUM9"; 1.0; HIGHEST; -5.0;
-2; 0; 0; -3; 0x050800 *2; "NUM"; i8 192; i8 200; i8 204; i8 209; i8 210;
-LOWEST; 1.0; 5.0;
-
-# String variable, no label or missing values.
-2; 4; 0; 0; 0x010400 *2; s8 "STR1";
-
-# String variable, variable label.
-2; 4; 1; 0; 0x010400 *2; s8 "STR2";
-25; "String variable 2's label"; i8 0 * 3;
-
-# String variable, one missing value.
-2; 4; 0; 1; 0x010400 *2; s8 "STR3"; s8 "MISS";
-
-# String variable, variable label and missing value.
-2; 4; 1; 1; 0x010400 *2; s8 "STR4";
-29; "Another string variable label"; i8 0 * 3;
-s8 "OTHR";
-
-# String variable, two missing values.
-2; 4; 0; 2; 0x010400 *2; s8 "STR5"; s8 "MISS"; s8 "OTHR";
-
-# String variable, three missing values.
-2; 4; 0; 3; 0x010400 *2; s8 "STR6"; s8 "MISS"; s8 "OTHR"; s8 "MORE";
-
-# Long string variable, one missing value.
-# (This is not how SPSS represents missing values for long strings--it
-# uses a separate record as shown later below--but old versions of PSPP
-# did use this representation so we continue supporting it for backward
-# compatibility.
-2; 11; 0; 1; 0x010b00 *2; s8 "STR7"; "first8by";
-2; -1; 0; 0; 0; 0; s8 "";
-
-# Long string variables that will have missing values added with a
-# later record.
-2; 9; 0; 0; 0x010900 *2; s8 "STR8";
-2; -1; 0; 0; 0; 0; s8 "";
-2; 10; 0; 0; 0x010a00 *2; s8 "STR9";
-2; -1; 0; 0; 0; 0; s8 "";
-2; 11; 0; 0; 0x010b00 *2; s8 "STR10";
-2; -1; 0; 0; 0; 0; s8 "";
-
-# Long string variable, value label.
-2; 25; 1; 0; 0x011900 *2; s8 "STR11"; 14; "25-byte string"; i8 0 * 2;
-( 2; -1; 0; 0; 0; 0; s8 ""; ) * 2;
-# Variable label fields on continuation records have been spotted in system
-# files created by "SPSS Power Macintosh Release 6.1".
-2; -1; 1; 0; 0; 0; s8 ""; 20; "dummy variable label";
-
-# Machine integer info record.
-7; 3; 4; 8; 1; 2; 3; -1; 1; 1; ENDIAN; 1252;
-
-# Machine floating-point info record.
-7; 4; 8; 3; SYSMIS; HIGHEST; LOWEST;
-
-# Long string variable missing values record.
-7; 22; 1; COUNT (
-# One missing value for STR8.
-COUNT("STR8"); i8 1; 8; "abcdefgh";
-
-# Two missing values for STR9.
-COUNT("STR9"); i8 2; 8; "abcdefgh"; "01234567";
-
-# Three missing values for STR9.
-COUNT("STR10"); i8 3; 8; "abcdefgh"; "01234567"; "0 ";
-);
-
-# Character encoding record.
-7; 20; 1; 12; "windows-1252";
-
-# Dictionary termination record.
-999; 0;
-
-# Data.
-1.0; 2.0; 3.0; 4.0; 5.0; 6.0; 7.0; 8.0; 9.0; 10.0;
-s8 "abcd"; s8 "efgh"; s8 "ijkl"; s8 "mnop"; s8 "qrst"; s8 "uvwx";
-s16 "yzABCDEFGHI"; s16 "JKLMNOPQR"; s16 "STUVWXYZ01";
-s16 "23456789abc"; s32 "defghijklmnopqstuvwxyzABC";
-"#;
- 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 {
- product: "$(#) SPSS DATA FILE PSPP synthetic test file".into(),
- ..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(
- "variable_labels_and_missing_values",
- &pt,
- r#"╭────────────────────────────────┬────────┬────────────────────────────────┬─────────────────┬─────┬─────┬─────────┬────────────┬────────────┬──────────────────────╮
-│ │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 │ │
-│num3 │ 3│ │ │Input│ 8│Right │F8.0 │F8.0 │1 │
-│Another numeric variable label │ 4│Another numeric variable label │ │Input│ 8│Right │F8.0 │F8.0 │1 │
-│num5 │ 5│ │ │Input│ 8│Right │F8.0 │F8.0 │1; 2 │
-│num6 │ 6│ │ │Input│ 8│Right │F8.0 │F8.0 │1; 2; 3 │
-│num7 │ 7│ │ │Input│ 8│Right │F8.0 │F8.0 │1 THRU 3 │
-│num8 │ 8│ │ │Input│ 8│Right │F8.0 │F8.0 │1 THRU 3; 5 │
-│num9 │ 9│ │ │Input│ 8│Right │F8.0 │F8.0 │1 THRU HIGH; -5 │
-│numàèìñò │ 10│ │ │Input│ 8│Right │F8.0 │F8.0 │LOW THRU 1; 5 │
-│str1 │ 11│ │Nominal │Input│ 4│Left │A4 │A4 │ │
-│String variable 2's label │ 12│String variable 2's label │Nominal │Input│ 4│Left │A4 │A4 │ │
-│str3 │ 13│ │Nominal │Input│ 4│Left │A4 │A4 │"MISS" │
-│Another string variable label │ 14│Another string variable label │Nominal │Input│ 4│Left │A4 │A4 │"OTHR" │
-│str5 │ 15│ │Nominal │Input│ 4│Left │A4 │A4 │"MISS"; "OTHR" │
-│str6 │ 16│ │Nominal │Input│ 4│Left │A4 │A4 │"MISS"; "OTHR"; "MORE"│
-│str7 │ 17│ │Nominal │Input│ 11│Left │A11 │A11 │"first8by" │
-│str8 │ 18│ │Nominal │Input│ 9│Left │A9 │A9 │ │
-│str9 │ 19│ │Nominal │Input│ 10│Left │A10 │A10 │ │
-│str10 │ 20│ │Nominal │Input│ 11│Left │A11 │A11 │ │
-│25-byte string │ 21│25-byte string │Nominal │Input│ 25│Left │A25 │A25 │ │
-╰────────────────────────────────┴────────┴────────────────────────────────┴─────────────────┴─────┴─────┴─────────┴────────────┴────────────┴──────────────────────╯
-"#,
- );
- }
-}
-
-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_sysfile("variable_labels_and_missing_values");
}
#[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_sysfile("unspecified_number_of_variable_positions");
}
#[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.
-"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;
-
-# Machine integer info record (SPSS 13).
-7; 3; 4; 8; 13; 2; 3; -1; 1; 1; ENDIAN; 1252;
+ test_sysfile("wrong_variable_positions_but_v13");
+}
-# Character encoding record.
-7; 20; 1; 12; "windows-1252";
+#[test]
+fn value_labels() {
+ test_sysfile("value_labels");
+}
-# Dictionary termination record.
-999; 0;
+#[test]
+fn documents() {
+ test_sysfile("documents");
+}
-# Data.
-1.0; 2.0;
-"#;
- let sysfile = sack(input, None, endian).unwrap();
+fn test_sysfile(name: &str) {
+ let input_filename = Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join("src/sys/testdata")
+ .join(name)
+ .with_extension("sack");
+ let input = String::from_utf8(std::fs::read(&input_filename).unwrap()).unwrap();
+ let expected_filename = input_filename.with_extension("expected");
+ let expected = String::from_utf8(std::fs::read(&expected_filename).unwrap()).unwrap();
+ for endian in all::<Endian>() {
+ let sysfile = sack(&input, Some(&input_filename), 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 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_sysfile(
- &dictionary,
- &metadata,
- "\
- ╭──────────────────────┬────────────────────────╮
- │ Created │ 05-JAN-2011 20:53:52│
- ├──────────────────────┼────────────────────────┤
- │Writer Product │PSPP synthetic test file│
- │ Version │13.2.3 │
- ├──────────────────────┼────────────────────────┤
- │ Compression │None │
- │ Number of Cases│ 1│
- ╰──────────────────────┴────────────────────────╯
-
- ╭──────────────────────────┬────────┬──────────────────────────┬─────────────────┬─────┬─────┬─────────┬────────────┬────────────┬──────────────╮
- │ │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 │ │
- ╰──────────────────────────┴────────┴──────────────────────────┴─────────────────┴─────┴─────┴─────────┴────────────┴────────────┴──────────────╯
- ",
- );*/
-
- assert_eq!(
- metadata,
- Metadata {
- version: Some((13, 2, 3)),
- ..test_metadata(endian)
- }
- );
let (group, data) = metadata.to_pivot_rows();
let metadata_table = PivotTable::new([(Axis3::Y, Dimension::new(group))]).with_data(
data.into_iter()
.filter(|(_row, value)| !value.is_empty())
.map(|(row, value)| ([row], value)),
);
- assert_rendering(
- "wrong_variable_positions_but_v13_metadata",
- &metadata_table,
- "\
-╭──────────────────────┬────────────────────────╮
-│ Created │ 05-JAN-2011 20:53:52│
-├──────────────────────┼────────────────────────┤
-│Writer Product │PSPP synthetic test file│
-│ Version │13.2.3 │
-├──────────────────────┼────────────────────────┤
-│ Compression │None │
-│ Number of Cases│ 1│
-╰──────────────────────┴────────────────────────╯
-",
- );
- assert_eq!(
- dictionary.file_label.as_ref().map(|s| s.as_str()),
- Some("PSPP synthetic test file")
+ let (group, data) = dictionary.to_pivot_rows();
+ let dictionary_table = PivotTable::new([(Axis3::Y, Dimension::new(group))]).with_data(
+ data.into_iter()
+ .enumerate()
+ .filter(|(_row, value)| !value.is_empty())
+ .map(|(row, value)| ([row], value)),
);
- 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 │ │
-╰──────────────────────────┴────────┴──────────────────────────┴─────────────────┴─────┴─────┴─────────┴────────────┴────────────┴──────────────╯
-",
+ let mut output = Vec::new();
+ output.extend(
+ errors
+ .into_iter()
+ .map(|error| Arc::new(Item::from(Text::new_log(error.to_string())))),
);
- }
-}
-
-#[test]
-fn value_labels() {
- for endian in all::<Endian>() {
- let input = r#"
-# File header.
-"$FL2"; s60 "$(#) SPSS DATA FILE PSPP synthetic test file";
-2; # Layout code
-22; # Nominal case size
-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 variables.
-2; 0; 0; 0; 0x050800 *2; s8 "NUM1";
-2; 0; 0; 0; 0x050800 *2; s8 "NUM2";
-2; 0; 0; 0; 0x050800 *2; s8 "NUM3";
-2; 0; 0; 0; 0x050800 *2; s8 "NUM4";
-2; 0; 0; 0; 0x050800 *2; s8 "NUM5";
-
-# String variables.
-2; 1; 0; 0; 0x010100 *2; s8 "STR1"; # index 6
-2; 2; 0; 0; 0x010200 *2; s8 "STR2"; # index 7
-2; 3; 0; 0; 0x010300 *2; s8 "STR3"; # index 8
-2; 4; 0; 0; 0x010400 *2; s8 "STR4"; # index 9
-2; 4; 0; 0; 0x010400 *2; s8 "STR5"; # index 10
-2; 6; 0; 0; 0x010600 *2; s8 "STR6"; # index 11
-2; 7; 0; 0; 0x010700 *2; s8 "STR7"; # index 12
-2; 8; 0; 0; 0x010800 *2; s8 "STR8"; # index 13
-2; 9; 0; 0; 0x010900 *2; "STR9"; i8 230; s3 ""; # index 14
-2; -1; 0; 0; 0; 0; s8 "";
-2; 12; 0; 0; 0x010c00 *2; s8 "STR12"; # index 16
-2; -1; 0; 0; 0; 0; s8 "";
-2; 16; 0; 0; 0x011000 *2; s8 "STR16"; # index 18
-2; -1; 0; 0; 0; 0; s8 "";
-2; 17; 0; 0; 0x011100 *2; s8 "STR17"; # index 20
-( 2; -1; 0; 0; 0; 0; s8 ""; ) * 2;
-
-# One value label for NUM1.
-3; 1; 1.0; i8 17; i8 238; i8 228; i8 232; i8 237; s19 " (in Russian)"; 4; 1; 1;
-
-# Two value labels for NUM2, as a single pair of type 3 and type 4 records.
-3; 2; 1.0; i8 3; s7 "one"; 2.0; i8 3; s7 "two"; 4; 1; 2;
-
-# Two value labels for NUM3, as two pairs of type 3 and type 4 records.
-3; 1; 3.0; i8 5; s7 "three"; 4; 1; 3;
-3; 1; 4.0; i8 4; s7 "four"; 4; 1; 3;
-
-# Two common value labels for NUM4 and NUM5, plus two different ones for each.
-3; 1; 5.0; i8 4; s7 "five"; 4; 1; 4;
-3; 1; 6.0; i8 3; s7 "six"; 4; 1; 5;
-3; 2; 7.0; i8 5; s7 "seven"; 8.0; i8 5; s7 "eight"; 4; 2; 4; 5;
-3; 1; 9.0; i8 4; s7 "nine"; 4; 1; 4;
-3; 1; 10.0; i8 3; s7 "ten"; 4; 1; 5;
-
-# One value label for STR1.
-3; 1; s8 "a"; i8 19; s23 "value label for `a'"; 4; 1; 6;
-
-# Two value labels for STR2, as a single pair of type 3 and type 4 records.
-3; 2;
-s8 "bc"; i8 20; s23 "value label for `bc'";
-s8 "de"; i8 20; s23 "value label for `de'";
-4; 1; 7;
-
-# Two value labels for STR3, as two pairs of type 3 and type 4 records.
-3; 1; s8 "fgh"; i8 21; s23 "value label for `fgh'"; 4; 1; 8;
-3; 1; s8 "ijk"; i8 21; s23 "value label for `ijk'"; 4; 1; 8;
-
-# Two common value labels for STR4 and STR5, plus two different ones for each.
-3; 1; s8 "lmno"; i8 22; s23 "value label for `lmno'"; 4; 1; 9;
-3; 1; s8 "pqrs"; i8 22; s23 "value label for `pqrs'"; 4; 1; 10;
-3; 2;
-s8 "tuvw"; i8 22; s23 "value label for `tuvw'";
-s8 "xyzA"; i8 22; s23 "value label for `xyzA'";
-4; 2; 9; 10;
-3; 1; s8 "BCDE"; i8 22; s23 "value label for `BCDE'"; 4; 1; 9;
-3; 1; s8 "FGHI"; i8 22; s23 "value label for `FGHI'"; 4; 1; 10;
-
-# One value label for STR6, STR7, STR8.
-3; 1; s8 "JKLMNO"; i8 24; s31 "value label for `JKLMNO'"; 4; 1; 11;
-3; 1; s8 "JKLMNOP"; i8 25; s31 "value label for `JKLMNOP'"; 4; 1; 12;
-3; 1; s8 "JKLMNOPQ"; i8 26; s31 "value label for `JKLMNOPQ'"; 4; 1; 13;
-
-# Machine integer info record.
-7; 3; 4; 8; 1; 2; 3; -1; 1; 1; ENDIAN; 1251;
-
-# Character encoding record.
-7; 20; 1; 12; "windows-1251";
-
-7; 21; 1; COUNT (
-# One value label for STR9ж,
-COUNT("STR9"; i8 230); 9; 1; COUNT("RSTUVWXYZ"); COUNT("value label for `RSTUVWXYZ'");
-
-# Two value labels for STR12.
-COUNT("STR12"); 12; 2;
-COUNT("0123456789ab"); COUNT("value label for `0123456789ab'");
-COUNT("cdefghijklmn"); COUNT("value label for `cdefghijklmn'");
-
-# Three value labels for STR16.
-COUNT("STR16"); 16; 3;
-COUNT("opqrstuvwxyzABCD"); COUNT("value label for `opqrstuvwxyzABCD'");
-COUNT("EFGHIJKLMNOPQRST"); COUNT("value label for `EFGHIJKLMNOPQRST'");
-COUNT("UVWXYZ0123456789"); COUNT("value label for `UVWXYZ0123456789' with Cyrillic letters: `"; i8 244; i8 245; i8 246; "'");
-
-# One value label for STR17.
-COUNT("STR17"); 17; 1;
-COUNT("abcdefghijklmnopq"); COUNT("value label for `abcdefghijklmnopq'");
-);
-
-# Dictionary termination record.
-999; 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());
+ output.push(Arc::new(metadata_table.into()));
+ output.push(Arc::new(dictionary_table.into()));
+ output.push(Arc::new(
+ dictionary.output_variables().to_pivot_table().into(),
+ ));
+ if let Some(pt) = dictionary.output_value_labels().to_pivot_table() {
+ output.push(Arc::new(pt.into()));
}
+ let output = Item::new(Details::Group(output));
- 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, test_metadata(endian));
- assert_eq!(
- dictionary.file_label.as_ref().map(|s| s.as_str()),
- Some("PSPP synthetic test file")
- );
- assert_rendering(
- "value_labels_value_labels",
- &dictionary.output_value_labels().to_pivot_table().unwrap(),
- "\
-╭────────────────────────────────┬───────────────────────────────────────────────────────────────╮
-│Variable Value │ │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│num1 1 │один (in Russian) │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│num2 1 │one │
-│ 2 │two │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│num3 3 │three │
-│ 4 │four │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│num4 5 │five │
-│ 7 │seven │
-│ 8 │eight │
-│ 9 │nine │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│num5 6 │six │
-│ 7 │seven │
-│ 8 │eight │
-│ 10 │ten │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│str1 a │value label for `a' │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│str2 bc │value label for `bc' │
-│ de │value label for `de' │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│str3 fgh │value label for `fgh' │
-│ ijk │value label for `ijk' │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│str4 BCDE │value label for `BCDE' │
-│ lmno │value label for `lmno' │
-│ tuvw │value label for `tuvw' │
-│ xyzA │value label for `xyzA' │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│str5 FGHI │value label for `FGHI' │
-│ pqrs │value label for `pqrs' │
-│ tuvw │value label for `tuvw' │
-│ xyzA │value label for `xyzA' │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│str6 JKLMNO │value label for `JKLMNO' │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│str7 JKLMNOP │value label for `JKLMNOP' │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│str8 JKLMNOPQ │value label for `JKLMNOPQ' │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│str9ж RSTUVWXYZ │value label for `RSTUVWXYZ' │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│str12 0123456789ab │value label for `0123456789ab' │
-│ cdefghijklmn │value label for `cdefghijklmn' │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│str16 EFGHIJKLMNOPQRST │value label for `EFGHIJKLMNOPQRST' │
-│ UVWXYZ0123456789 │value label for `UVWXYZ0123456789' with Cyrillic letters: `фхц'│
-│ opqrstuvwxyzABCD │value label for `opqrstuvwxyzABCD' │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│str17 abcdefghijklmnopq│value label for `abcdefghijklmnopq' │
-╰────────────────────────────────┴───────────────────────────────────────────────────────────────╯
-",
+ assert_lines_eq(
+ &expected,
+ expected_filename.display(),
+ &output.to_string(),
+ "actual",
);
- 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 │ │
-│num2 │ 2│ │ │Input│ 8│Right │F8.0 │F8.0 │ │
-│num3 │ 3│ │ │Input│ 8│Right │F8.0 │F8.0 │ │
-│num4 │ 4│ │ │Input│ 8│Right │F8.0 │F8.0 │ │
-│num5 │ 5│ │ │Input│ 8│Right │F8.0 │F8.0 │ │
-│str1 │ 6│ │Nominal │Input│ 1│Left │A1 │A1 │ │
-│str2 │ 7│ │Nominal │Input│ 2│Left │A2 │A2 │ │
-│str3 │ 8│ │Nominal │Input│ 3│Left │A3 │A3 │ │
-│str4 │ 9│ │Nominal │Input│ 4│Left │A4 │A4 │ │
-│str5 │ 10│ │Nominal │Input│ 4│Left │A4 │A4 │ │
-│str6 │ 11│ │Nominal │Input│ 6│Left │A6 │A6 │ │
-│str7 │ 12│ │Nominal │Input│ 7│Left │A7 │A7 │ │
-│str8 │ 13│ │Nominal │Input│ 8│Left │A8 │A8 │ │
-│str9ж│ 14│ │Nominal │Input│ 9│Left │A9 │A9 │ │
-│str12│ 15│ │Nominal │Input│ 12│Left │A12 │A12 │ │
-│str16│ 16│ │Nominal │Input│ 16│Left │A16 │A16 │ │
-│str17│ 17│ │Nominal │Input│ 17│Left │A17 │A17 │ │
-╰─────┴────────┴─────┴─────────────────┴─────┴─────┴─────────┴────────────┴────────────┴──────────────╯
-");
}
}
--- /dev/null
+╭──────────────────────┬────────────────────────╮
+│ Created │ 01-JAN-2011 20:53:52│
+├──────────────────────┼────────────────────────┤
+│Writer Product │PSPP synthetic test file│
+│ Version │1.2.3 │
+├──────────────────────┼────────────────────────┤
+│ Compression │None │
+│ Number of Cases│ 1│
+╰──────────────────────┴────────────────────────╯
+
+╭─────────┬───────────────────────────────────────────────────────╮
+│Label │PSPP synthetic test file │
+│Variables│ 1│
+│Documents│First line of documents │
+│ │Second line of documents │
+│ │abbé appliqué attaché blasé café canapé cliché consommé│
+│ │Last line of documents │
+╰─────────┴───────────────────────────────────────────────────────╯
+
+╭────┬────────┬─────┬─────────────────┬─────┬─────┬─────────┬────────────┬────────────┬──────────────╮
+│ │Position│Label│Measurement Level│ Role│Width│Alignment│Print Format│Write Format│Missing Values│
+├────┼────────┼─────┼─────────────────┼─────┼─────┼─────────┼────────────┼────────────┼──────────────┤
+│num1│ 1│ │ │Input│ 8│Right │F8.0 │F8.0 │ │
+╰────┴────────┴─────┴─────────────────┴─────┴─────┴─────────┴────────────┴────────────┴──────────────╯
--- /dev/null
+# File header.
+"$FL2"; s60 "$(#) SPSS DATA FILE PSPP synthetic test file";
+2; # Layout code
+1; # Nominal case size
+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";
+
+# Machine integer info record.
+7; 3; 4; 8; 1; 2; 3; -1; 1; 1; ENDIAN; 1252;
+
+# Document record.
+6; 5;
+s80 "First line of documents";
+s80 "Second line of documents";
+"abb"; i8 233; " appliqu"; i8 233; " attach"; i8 233; " blas"; i8 233; " caf"; i8 233; " canap"; i8 233; " clich"; i8 233; " consomm"; i8 233;
+s25 "";
+s80 "";
+s80 "Last line of documents";
+
+# Character encoding record.
+7; 20; 1; 12; "windows-1252";
+
+# Dictionary termination record.
+999; 0;
+
+# Data.
+1.0;
--- /dev/null
+╭──────────────────────┬────────────────────────╮
+│ Created │ 05-JAN-2011 20:53:52│
+├──────────────────────┼────────────────────────┤
+│Writer Product │PSPP synthetic test file│
+├──────────────────────┼────────────────────────┤
+│ Compression │None │
+│ Number of Cases│ 1│
+╰──────────────────────┴────────────────────────╯
+
+╭─────────┬────────────────────────╮
+│Label │PSPP synthetic test file│
+│Variables│ 2│
+╰─────────┴────────────────────────╯
+
+╭──────────────────────────┬────────┬──────────────────────────┬─────────────────┬─────┬─────┬─────────┬────────────┬────────────┬──────────────╮
+│ │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 │ │
+╰──────────────────────────┴────────┴──────────────────────────┴─────────────────┴─────┴─────┴─────────┴────────────┴────────────┴──────────────╯
--- /dev/null
+# 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;
--- /dev/null
+╭──────────────────────┬────────────────────────╮
+│ Created │ 05-JAN-2011 20:53:52│
+├──────────────────────┼────────────────────────┤
+│Writer Product │PSPP synthetic test file│
+│ Version │1.2.3 │
+├──────────────────────┼────────────────────────┤
+│ Compression │None │
+│ Number of Cases│ 1│
+╰──────────────────────┴────────────────────────╯
+
+╭─────────┬────────────────────────╮
+│Label │PSPP synthetic test file│
+│Variables│ 17│
+╰─────────┴────────────────────────╯
+
+╭─────┬────────┬─────┬─────────────────┬─────┬─────┬─────────┬────────────┬────────────┬──────────────╮
+│ │Position│Label│Measurement Level│ Role│Width│Alignment│Print Format│Write Format│Missing Values│
+├─────┼────────┼─────┼─────────────────┼─────┼─────┼─────────┼────────────┼────────────┼──────────────┤
+│num1 │ 1│ │ │Input│ 8│Right │F8.0 │F8.0 │ │
+│num2 │ 2│ │ │Input│ 8│Right │F8.0 │F8.0 │ │
+│num3 │ 3│ │ │Input│ 8│Right │F8.0 │F8.0 │ │
+│num4 │ 4│ │ │Input│ 8│Right │F8.0 │F8.0 │ │
+│num5 │ 5│ │ │Input│ 8│Right │F8.0 │F8.0 │ │
+│str1 │ 6│ │Nominal │Input│ 1│Left │A1 │A1 │ │
+│str2 │ 7│ │Nominal │Input│ 2│Left │A2 │A2 │ │
+│str3 │ 8│ │Nominal │Input│ 3│Left │A3 │A3 │ │
+│str4 │ 9│ │Nominal │Input│ 4│Left │A4 │A4 │ │
+│str5 │ 10│ │Nominal │Input│ 4│Left │A4 │A4 │ │
+│str6 │ 11│ │Nominal │Input│ 6│Left │A6 │A6 │ │
+│str7 │ 12│ │Nominal │Input│ 7│Left │A7 │A7 │ │
+│str8 │ 13│ │Nominal │Input│ 8│Left │A8 │A8 │ │
+│str9ж│ 14│ │Nominal │Input│ 9│Left │A9 │A9 │ │
+│str12│ 15│ │Nominal │Input│ 12│Left │A12 │A12 │ │
+│str16│ 16│ │Nominal │Input│ 16│Left │A16 │A16 │ │
+│str17│ 17│ │Nominal │Input│ 17│Left │A17 │A17 │ │
+╰─────┴────────┴─────┴─────────────────┴─────┴─────┴─────────┴────────────┴────────────┴──────────────╯
+
+╭────────────────────────────────┬───────────────────────────────────────────────────────────────╮
+│Variable Value │ │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│num1 1 │один (in Russian) │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│num2 1 │one │
+│ 2 │two │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│num3 3 │three │
+│ 4 │four │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│num4 5 │five │
+│ 7 │seven │
+│ 8 │eight │
+│ 9 │nine │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│num5 6 │six │
+│ 7 │seven │
+│ 8 │eight │
+│ 10 │ten │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│str1 a │value label for `a' │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│str2 bc │value label for `bc' │
+│ de │value label for `de' │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│str3 fgh │value label for `fgh' │
+│ ijk │value label for `ijk' │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│str4 BCDE │value label for `BCDE' │
+│ lmno │value label for `lmno' │
+│ tuvw │value label for `tuvw' │
+│ xyzA │value label for `xyzA' │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│str5 FGHI │value label for `FGHI' │
+│ pqrs │value label for `pqrs' │
+│ tuvw │value label for `tuvw' │
+│ xyzA │value label for `xyzA' │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│str6 JKLMNO │value label for `JKLMNO' │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│str7 JKLMNOP │value label for `JKLMNOP' │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│str8 JKLMNOPQ │value label for `JKLMNOPQ' │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│str9ж RSTUVWXYZ │value label for `RSTUVWXYZ' │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│str12 0123456789ab │value label for `0123456789ab' │
+│ cdefghijklmn │value label for `cdefghijklmn' │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│str16 EFGHIJKLMNOPQRST │value label for `EFGHIJKLMNOPQRST' │
+│ UVWXYZ0123456789 │value label for `UVWXYZ0123456789' with Cyrillic letters: `фхц'│
+│ opqrstuvwxyzABCD │value label for `opqrstuvwxyzABCD' │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│str17 abcdefghijklmnopq│value label for `abcdefghijklmnopq' │
+╰────────────────────────────────┴───────────────────────────────────────────────────────────────╯
--- /dev/null
+# File header.
+"$FL2"; s60 "$(#) SPSS DATA FILE PSPP synthetic test file";
+2; # Layout code
+22; # Nominal case size
+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 variables.
+2; 0; 0; 0; 0x050800 *2; s8 "NUM1";
+2; 0; 0; 0; 0x050800 *2; s8 "NUM2";
+2; 0; 0; 0; 0x050800 *2; s8 "NUM3";
+2; 0; 0; 0; 0x050800 *2; s8 "NUM4";
+2; 0; 0; 0; 0x050800 *2; s8 "NUM5";
+
+# String variables.
+2; 1; 0; 0; 0x010100 *2; s8 "STR1"; # index 6
+2; 2; 0; 0; 0x010200 *2; s8 "STR2"; # index 7
+2; 3; 0; 0; 0x010300 *2; s8 "STR3"; # index 8
+2; 4; 0; 0; 0x010400 *2; s8 "STR4"; # index 9
+2; 4; 0; 0; 0x010400 *2; s8 "STR5"; # index 10
+2; 6; 0; 0; 0x010600 *2; s8 "STR6"; # index 11
+2; 7; 0; 0; 0x010700 *2; s8 "STR7"; # index 12
+2; 8; 0; 0; 0x010800 *2; s8 "STR8"; # index 13
+2; 9; 0; 0; 0x010900 *2; "STR9"; i8 230; s3 ""; # index 14
+2; -1; 0; 0; 0; 0; s8 "";
+2; 12; 0; 0; 0x010c00 *2; s8 "STR12"; # index 16
+2; -1; 0; 0; 0; 0; s8 "";
+2; 16; 0; 0; 0x011000 *2; s8 "STR16"; # index 18
+2; -1; 0; 0; 0; 0; s8 "";
+2; 17; 0; 0; 0x011100 *2; s8 "STR17"; # index 20
+( 2; -1; 0; 0; 0; 0; s8 ""; ) * 2;
+
+# One value label for NUM1.
+3; 1; 1.0; i8 17; i8 238; i8 228; i8 232; i8 237; s19 " (in Russian)"; 4; 1; 1;
+
+# Two value labels for NUM2, as a single pair of type 3 and type 4 records.
+3; 2; 1.0; i8 3; s7 "one"; 2.0; i8 3; s7 "two"; 4; 1; 2;
+
+# Two value labels for NUM3, as two pairs of type 3 and type 4 records.
+3; 1; 3.0; i8 5; s7 "three"; 4; 1; 3;
+3; 1; 4.0; i8 4; s7 "four"; 4; 1; 3;
+
+# Two common value labels for NUM4 and NUM5, plus two different ones for each.
+3; 1; 5.0; i8 4; s7 "five"; 4; 1; 4;
+3; 1; 6.0; i8 3; s7 "six"; 4; 1; 5;
+3; 2; 7.0; i8 5; s7 "seven"; 8.0; i8 5; s7 "eight"; 4; 2; 4; 5;
+3; 1; 9.0; i8 4; s7 "nine"; 4; 1; 4;
+3; 1; 10.0; i8 3; s7 "ten"; 4; 1; 5;
+
+# One value label for STR1.
+3; 1; s8 "a"; i8 19; s23 "value label for `a'"; 4; 1; 6;
+
+# Two value labels for STR2, as a single pair of type 3 and type 4 records.
+3; 2;
+s8 "bc"; i8 20; s23 "value label for `bc'";
+s8 "de"; i8 20; s23 "value label for `de'";
+4; 1; 7;
+
+# Two value labels for STR3, as two pairs of type 3 and type 4 records.
+3; 1; s8 "fgh"; i8 21; s23 "value label for `fgh'"; 4; 1; 8;
+3; 1; s8 "ijk"; i8 21; s23 "value label for `ijk'"; 4; 1; 8;
+
+# Two common value labels for STR4 and STR5, plus two different ones for each.
+3; 1; s8 "lmno"; i8 22; s23 "value label for `lmno'"; 4; 1; 9;
+3; 1; s8 "pqrs"; i8 22; s23 "value label for `pqrs'"; 4; 1; 10;
+3; 2;
+s8 "tuvw"; i8 22; s23 "value label for `tuvw'";
+s8 "xyzA"; i8 22; s23 "value label for `xyzA'";
+4; 2; 9; 10;
+3; 1; s8 "BCDE"; i8 22; s23 "value label for `BCDE'"; 4; 1; 9;
+3; 1; s8 "FGHI"; i8 22; s23 "value label for `FGHI'"; 4; 1; 10;
+
+# One value label for STR6, STR7, STR8.
+3; 1; s8 "JKLMNO"; i8 24; s31 "value label for `JKLMNO'"; 4; 1; 11;
+3; 1; s8 "JKLMNOP"; i8 25; s31 "value label for `JKLMNOP'"; 4; 1; 12;
+3; 1; s8 "JKLMNOPQ"; i8 26; s31 "value label for `JKLMNOPQ'"; 4; 1; 13;
+
+# Machine integer info record.
+7; 3; 4; 8; 1; 2; 3; -1; 1; 1; ENDIAN; 1251;
+
+# Character encoding record.
+7; 20; 1; 12; "windows-1251";
+
+7; 21; 1; COUNT (
+# One value label for STR9ж,
+COUNT("STR9"; i8 230); 9; 1; COUNT("RSTUVWXYZ"); COUNT("value label for `RSTUVWXYZ'");
+
+# Two value labels for STR12.
+COUNT("STR12"); 12; 2;
+COUNT("0123456789ab"); COUNT("value label for `0123456789ab'");
+COUNT("cdefghijklmn"); COUNT("value label for `cdefghijklmn'");
+
+# Three value labels for STR16.
+COUNT("STR16"); 16; 3;
+COUNT("opqrstuvwxyzABCD"); COUNT("value label for `opqrstuvwxyzABCD'");
+COUNT("EFGHIJKLMNOPQRST"); COUNT("value label for `EFGHIJKLMNOPQRST'");
+COUNT("UVWXYZ0123456789"); COUNT("value label for `UVWXYZ0123456789' with Cyrillic letters: `"; i8 244; i8 245; i8 246; "'");
+
+# One value label for STR17.
+COUNT("STR17"); 17; 1;
+COUNT("abcdefghijklmnopq"); COUNT("value label for `abcdefghijklmnopq'");
+);
+
+# Dictionary termination record.
+999; 0;
--- /dev/null
+╭──────────────────────┬────────────────────────╮
+│ Created │ 05-JAN-2011 20:53:52│
+├──────────────────────┼────────────────────────┤
+│Writer Product │PSPP synthetic test file│
+│ Version │1.2.3 │
+├──────────────────────┼────────────────────────┤
+│ Compression │None │
+│ Number of Cases│ 1│
+╰──────────────────────┴────────────────────────╯
+
+╭─────────┬──────────────────────────────╮
+│Label │PSPP synthetic test file: ôõöø│
+│Variables│ 21│
+╰─────────┴──────────────────────────────╯
+
+╭────────────────────────────────┬────────┬────────────────────────────────┬─────────────────┬─────┬─────┬─────────┬────────────┬────────────┬──────────────────────╮
+│ │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 │ │
+│num3 │ 3│ │ │Input│ 8│Right │F8.0 │F8.0 │1 │
+│Another numeric variable label │ 4│Another numeric variable label │ │Input│ 8│Right │F8.0 │F8.0 │1 │
+│num5 │ 5│ │ │Input│ 8│Right │F8.0 │F8.0 │1; 2 │
+│num6 │ 6│ │ │Input│ 8│Right │F8.0 │F8.0 │1; 2; 3 │
+│num7 │ 7│ │ │Input│ 8│Right │F8.0 │F8.0 │1 THRU 3 │
+│num8 │ 8│ │ │Input│ 8│Right │F8.0 │F8.0 │1 THRU 3; 5 │
+│num9 │ 9│ │ │Input│ 8│Right │F8.0 │F8.0 │1 THRU HIGH; -5 │
+│numàèìñò │ 10│ │ │Input│ 8│Right │F8.0 │F8.0 │LOW THRU 1; 5 │
+│str1 │ 11│ │Nominal │Input│ 4│Left │A4 │A4 │ │
+│String variable 2's label │ 12│String variable 2's label │Nominal │Input│ 4│Left │A4 │A4 │ │
+│str3 │ 13│ │Nominal │Input│ 4│Left │A4 │A4 │"MISS" │
+│Another string variable label │ 14│Another string variable label │Nominal │Input│ 4│Left │A4 │A4 │"OTHR" │
+│str5 │ 15│ │Nominal │Input│ 4│Left │A4 │A4 │"MISS"; "OTHR" │
+│str6 │ 16│ │Nominal │Input│ 4│Left │A4 │A4 │"MISS"; "OTHR"; "MORE"│
+│str7 │ 17│ │Nominal │Input│ 11│Left │A11 │A11 │"first8by" │
+│str8 │ 18│ │Nominal │Input│ 9│Left │A9 │A9 │ │
+│str9 │ 19│ │Nominal │Input│ 10│Left │A10 │A10 │ │
+│str10 │ 20│ │Nominal │Input│ 11│Left │A11 │A11 │ │
+│25-byte string │ 21│25-byte string │Nominal │Input│ 25│Left │A25 │A25 │ │
+╰────────────────────────────────┴────────┴────────────────────────────────┴─────────────────┴─────┴─────┴─────────┴────────────┴────────────┴──────────────────────╯
--- /dev/null
+# File header.
+"$FL2"; s60 "$(#) SPSS DATA FILE PSPP synthetic test file";
+2; # Layout code
+28; # Nominal case size
+0; # Not compressed
+0; # Not weighted
+1; # 1 case.
+100.0; # Bias.
+"05 Jan 11"; "20:53:52";
+"PSPP synthetic test file: "; i8 244; i8 245; i8 246; i8 248; s34 "";
+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";
+32; "Numeric variable 2's label ("; i8 249; i8 250; i8 251; ")";
+
+# Numeric variable, one missing value.
+2; 0; 0; 1; 0x050800 *2; s8 "NUM3";
+1.0;
+
+# Numeric variable, variable label and missing value.
+2; 0; 1; 1; 0x050800 *2; s8 "NUM4";
+30; "Another numeric variable label"; i8 0 * 2;
+1.0;
+
+# Numeric variable, two missing values.
+2; 0; 0; 2; 0x050800 *2; s8 "NUM5"; 1.0; 2.0;
+
+# Numeric variable, three missing values.
+2; 0; 0; 3; 0x050800 *2; s8 "NUM6"; 1.0; 2.0; 3.0;
+
+# Numeric variable, range of missing values.
+2; 0; 0; -2; 0x050800 *2; s8 "NUM7"; 1.0; 3.0;
+
+# Numeric variables, range of missing values plus discrete value.
+2; 0; 0; -3; 0x050800 *2; s8 "NUM8"; 1.0; 3.0; 5.0;
+2; 0; 0; -3; 0x050800 *2; s8 "NUM9"; 1.0; HIGHEST; -5.0;
+2; 0; 0; -3; 0x050800 *2; "NUM"; i8 192; i8 200; i8 204; i8 209; i8 210;
+LOWEST; 1.0; 5.0;
+
+# String variable, no label or missing values.
+2; 4; 0; 0; 0x010400 *2; s8 "STR1";
+
+# String variable, variable label.
+2; 4; 1; 0; 0x010400 *2; s8 "STR2";
+25; "String variable 2's label"; i8 0 * 3;
+
+# String variable, one missing value.
+2; 4; 0; 1; 0x010400 *2; s8 "STR3"; s8 "MISS";
+
+# String variable, variable label and missing value.
+2; 4; 1; 1; 0x010400 *2; s8 "STR4";
+29; "Another string variable label"; i8 0 * 3;
+s8 "OTHR";
+
+# String variable, two missing values.
+2; 4; 0; 2; 0x010400 *2; s8 "STR5"; s8 "MISS"; s8 "OTHR";
+
+# String variable, three missing values.
+2; 4; 0; 3; 0x010400 *2; s8 "STR6"; s8 "MISS"; s8 "OTHR"; s8 "MORE";
+
+# Long string variable, one missing value.
+# (This is not how SPSS represents missing values for long strings--it
+# uses a separate record as shown later below--but old versions of PSPP
+# did use this representation so we continue supporting it for backward
+# compatibility.
+2; 11; 0; 1; 0x010b00 *2; s8 "STR7"; "first8by";
+2; -1; 0; 0; 0; 0; s8 "";
+
+# Long string variables that will have missing values added with a
+# later record.
+2; 9; 0; 0; 0x010900 *2; s8 "STR8";
+2; -1; 0; 0; 0; 0; s8 "";
+2; 10; 0; 0; 0x010a00 *2; s8 "STR9";
+2; -1; 0; 0; 0; 0; s8 "";
+2; 11; 0; 0; 0x010b00 *2; s8 "STR10";
+2; -1; 0; 0; 0; 0; s8 "";
+
+# Long string variable, value label.
+2; 25; 1; 0; 0x011900 *2; s8 "STR11"; 14; "25-byte string"; i8 0 * 2;
+( 2; -1; 0; 0; 0; 0; s8 ""; ) * 2;
+# Variable label fields on continuation records have been spotted in system
+# files created by "SPSS Power Macintosh Release 6.1".
+2; -1; 1; 0; 0; 0; s8 ""; 20; "dummy variable label";
+
+# Machine integer info record.
+7; 3; 4; 8; 1; 2; 3; -1; 1; 1; ENDIAN; 1252;
+
+# Machine floating-point info record.
+7; 4; 8; 3; SYSMIS; HIGHEST; LOWEST;
+
+# Long string variable missing values record.
+7; 22; 1; COUNT (
+# One missing value for STR8.
+COUNT("STR8"); i8 1; 8; "abcdefgh";
+
+# Two missing values for STR9.
+COUNT("STR9"); i8 2; 8; "abcdefgh"; "01234567";
+
+# Three missing values for STR9.
+COUNT("STR10"); i8 3; 8; "abcdefgh"; "01234567"; "0 ";
+);
+
+# Character encoding record.
+7; 20; 1; 12; "windows-1252";
+
+# Dictionary termination record.
+999; 0;
+
+# Data.
+1.0; 2.0; 3.0; 4.0; 5.0; 6.0; 7.0; 8.0; 9.0; 10.0;
+s8 "abcd"; s8 "efgh"; s8 "ijkl"; s8 "mnop"; s8 "qrst"; s8 "uvwx";
+s16 "yzABCDEFGHI"; s16 "JKLMNOPQR"; s16 "STUVWXYZ01";
+s16 "23456789abc"; s32 "defghijklmnopqstuvwxyzABC";
--- /dev/null
+╭──────────────────────┬────────────────────────╮
+│ Created │ 05-JAN-2011 20:53:52│
+├──────────────────────┼────────────────────────┤
+│Writer Product │PSPP synthetic test file│
+│ Version │13.2.3 │
+├──────────────────────┼────────────────────────┤
+│ Compression │None │
+│ Number of Cases│ 1│
+╰──────────────────────┴────────────────────────╯
+
+╭─────────┬────────────────────────╮
+│Label │PSPP synthetic test file│
+│Variables│ 2│
+╰─────────┴────────────────────────╯
+
+╭──────────────────────────┬────────┬──────────────────────────┬─────────────────┬─────┬─────┬─────────┬────────────┬────────────┬──────────────╮
+│ │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 │ │
+╰──────────────────────────┴────────┴──────────────────────────┴─────────────────┴─────┴─────┴─────────┴────────────┴────────────┴──────────────╯
--- /dev/null
+# 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;
+
+# 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 input = read_to_string(&input_file_name)
.map_err(|err| anyhow!("{input_file_str}: read failed ({err})"))?;
- let output = sack(&input, Some(&input_file_str), endian)?;
+ let output = sack(&input, Some(&input_file_name), endian)?;
let output_file_str = output_file_name.to_string_lossy();
std::fs::write(&output_file_name, output)