0012a51dcc3dc0f1c412c6dd30556e7681ce6f9d
[pspp] / rust / src / cooked.rs
1 use std::{borrow::Cow, collections::HashSet};
2
3 use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
4 use encoding_rs::Encoding;
5
6 use crate::{
7     format::Spec,
8     identifier::{Identifier, Error as IdError},
9     {endian::Endian, CategoryLabels, Compression},
10 };
11 use thiserror::Error as ThisError;
12
13 #[derive(ThisError, Debug)]
14 pub enum Error {
15     #[error("Variable record at offset {offset:#x} specifies width {width} not in valid range [-1,255).")]
16     BadVariableWidth { offset: u64, width: i32 },
17
18     #[error("This file has corrupted metadata written by a buggy version of PSPP.  To ensure that other software can read it correctly, save a new copy of the file.")]
19     BadLongMissingValueFormat,
20
21     #[error("File creation date {creation_date} is not in the expected format \"DD MMM YY\" format.  Using 01 Jan 1970.")]
22     InvalidCreationDate { creation_date: String },
23
24     #[error("File creation time {creation_time} is not in the expected format \"HH:MM:SS\" format.  Using midnight.")]
25     InvalidCreationTime { creation_time: String },
26
27     #[error("Invalid variable name: {0}")]
28     BadIdentifier(#[from] IdError),
29
30     #[error("Details TBD")]
31     TBD,
32 }
33
34 pub struct Decoder {
35     pub compression: Option<Compression>,
36     pub endian: Endian,
37     pub encoding: &'static Encoding,
38     pub var_names: HashSet<Identifier>,
39     n_generated_names: usize,
40 }
41
42 impl Decoder {
43     fn take_name(&mut self, id: Identifier) -> bool {
44         self.var_names.insert(id)
45     }
46     fn generate_name(&mut self) -> Identifier {
47         loop {
48             self.n_generated_names += 1;
49             let name = Identifier::new(&format!("VAR{:03}", self.n_generated_names), self.encoding).unwrap();
50             if self.take_name(name.clone()) {
51                 return name;
52             }
53             assert!(self.n_generated_names < usize::MAX);
54         }
55     }
56     fn decode_string<'a>(&self, input: &'a [u8], warn: &impl Fn(Error)) -> Cow<'a, str> {
57         let (output, malformed) = self.encoding.decode_without_bom_handling(input);
58         if malformed {
59             warn(Error::TBD);
60         }
61         output
62     }
63 }
64
65 pub trait Decode: Sized {
66     type Input;
67     fn decode(decoder: &Decoder, input: &Self::Input, warn: impl Fn(Error)) -> Result<Self, Error>;
68 }
69
70 #[derive(Clone)]
71 pub struct Header {
72     pub eye_catcher: String,
73     pub weight_index: Option<usize>,
74     pub n_cases: Option<u64>,
75     pub creation: NaiveDateTime,
76     pub file_label: String,
77 }
78
79 impl Decode for Header {
80     type Input = crate::raw::Header;
81
82     fn decode(decoder: &Decoder, input: &Self::Input, warn: impl Fn(Error)) -> Result<Self, Error> {
83         let eye_catcher = decoder.decode_string(&input.eye_catcher, &warn);
84         let file_label = decoder.decode_string(&input.file_label, &warn);
85         let creation_date = decoder.decode_string(&input.creation_date, &warn);
86         let creation_date = NaiveDate::parse_from_str(&creation_date, "%v").unwrap_or_else(|_| {
87             warn(Error::InvalidCreationDate {
88                 creation_date: creation_date.into(),
89             });
90             Default::default()
91         });
92         let creation_time = decoder.decode_string(&input.creation_time, &warn);
93         let creation_time =
94             NaiveTime::parse_from_str(&creation_time, "%H:%M:%S").unwrap_or_else(|_| {
95                 warn(Error::InvalidCreationTime {
96                     creation_time: creation_time.into(),
97                 });
98                 Default::default()
99             });
100         Ok(Header {
101             eye_catcher: eye_catcher.into(),
102             weight_index: input.weight_index.map(|n| n as usize),
103             n_cases: input.n_cases.map(|n| n as u64),
104             creation: NaiveDateTime::new(creation_date, creation_time),
105             file_label: file_label.into(),
106         })
107     }
108 }
109
110 pub struct Variable {
111     pub width: i32,
112     pub name: Identifier,
113     pub print_format: Spec,
114     pub write_format: Spec,
115 }
116
117 fn decode_var(
118     decoder: &mut Decoder,
119     input: &crate::raw::Variable,
120     warn: impl Fn(Error),
121 ) -> Result<Option<Variable>, Error> {
122     match input.width {
123         0..=255 => (),
124         -1 => return Ok(None),
125         _ => {
126             return Err(Error::BadVariableWidth {
127                 offset: input.offset,
128                 width: input.width,
129             })
130         }
131     };
132     let name = decoder.decode_string(&input.name, &warn);
133     let name = match Identifier::new(&name, decoder.encoding) {
134         Ok(name) => {
135             if !decoder.take_name(name) {
136                 decoder.generate_name()
137             } else {
138                 name
139             }
140         }
141         Err(error) => {
142             warn(error.into());
143             decoder.generate_name()
144         }
145     };
146 }
147
148 #[derive(Clone)]
149 pub struct Document(Vec<String>);
150
151 impl Decode for Document {
152     type Input = crate::raw::Document;
153
154     fn decode(decoder: &Decoder, input: &Self::Input, warn: impl Fn(Error)) -> Result<Self, Error> {
155         Ok(Document(
156             input
157                 .lines
158                 .iter()
159                 .map(|s| decoder.decode_string(s, &warn).into())
160                 .collect(),
161         ))
162     }
163 }
164
165 pub use crate::raw::FloatInfo;
166 pub use crate::raw::IntegerInfo;
167
168 #[derive(Clone, Debug)]
169 pub enum MultipleResponseType {
170     MultipleDichotomy {
171         value: String,
172         labels: CategoryLabels,
173     },
174     MultipleCategory,
175 }
176 #[derive(Clone, Debug)]
177 pub struct MultipleResponseSet {
178     pub name: String,
179     pub label: String,
180     pub mr_type: MultipleResponseType,
181     pub vars: Vec<String>,
182 }
183
184 #[derive(Clone, Debug)]
185 pub struct MultipleResponseRecord(Vec<MultipleResponseSet>);
186
187 #[derive(Clone, Debug)]
188 pub struct ProductInfo(String);
189
190 pub enum Measure {
191     Nominal,
192     Ordinal,
193     Scale,
194 }
195
196 pub enum Alignment {
197     Left,
198     Right,
199     Center,
200 }
201
202 pub struct VarDisplay {
203     pub measure: Option<Measure>,
204     pub width: u32,
205     pub align: Option<Alignment>,
206 }
207
208 pub struct VarDisplayRecord(pub Vec<VarDisplay>);