work
[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, UncheckedSpec, Width},
8     identifier::{Error as IdError, Identifier},
9     {endian::Endian, CategoryLabels, Compression}, raw,
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.clone())
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)
50                 .unwrap();
51             if self.take_name(&name) {
52                 return name;
53             }
54             assert!(self.n_generated_names < usize::MAX);
55         }
56     }
57     fn decode_string<'a>(&self, input: &'a [u8], warn: &impl Fn(Error)) -> Cow<'a, str> {
58         let (output, malformed) = self.encoding.decode_without_bom_handling(input);
59         if malformed {
60             warn(Error::TBD);
61         }
62         output
63     }
64 }
65
66 pub trait Decode: Sized {
67     type Input;
68     fn decode(decoder: &Decoder, input: &Self::Input, warn: impl Fn(Error)) -> Result<Self, Error>;
69 }
70
71 #[derive(Clone)]
72 pub struct Header {
73     pub eye_catcher: String,
74     pub weight_index: Option<usize>,
75     pub n_cases: Option<u64>,
76     pub creation: NaiveDateTime,
77     pub file_label: String,
78 }
79
80 impl Decode for Header {
81     type Input = crate::raw::Header;
82
83     fn decode(decoder: &Decoder, input: &Self::Input, warn: impl Fn(Error)) -> Result<Self, Error> {
84         let eye_catcher = decoder.decode_string(&input.eye_catcher.0, &warn);
85         let file_label = decoder.decode_string(&input.file_label.0, &warn);
86         let creation_date = decoder.decode_string(&input.creation_date.0, &warn);
87         let creation_date = NaiveDate::parse_from_str(&creation_date, "%v").unwrap_or_else(|_| {
88             warn(Error::InvalidCreationDate {
89                 creation_date: creation_date.into(),
90             });
91             Default::default()
92         });
93         let creation_time = decoder.decode_string(&input.creation_time.0, &warn);
94         let creation_time =
95             NaiveTime::parse_from_str(&creation_time, "%H:%M:%S").unwrap_or_else(|_| {
96                 warn(Error::InvalidCreationTime {
97                     creation_time: creation_time.into(),
98                 });
99                 Default::default()
100             });
101         Ok(Header {
102             eye_catcher: eye_catcher.into(),
103             weight_index: input.weight_index.map(|n| n as usize),
104             n_cases: input.n_cases.map(|n| n as u64),
105             creation: NaiveDateTime::new(creation_date, creation_time),
106             file_label: file_label.into(),
107         })
108     }
109 }
110
111 pub struct Variable {
112     pub width: Width,
113     pub name: Identifier,
114     pub print_format: Spec,
115     pub write_format: Spec,
116     //pub missing_values: MissingValues,
117     pub label: Option<String>,
118 }
119
120 fn decode_format(raw: raw::Spec, name: &str, width: Width) -> Spec {
121     UncheckedSpec::try_from(raw)
122         .and_then(Spec::try_from)
123         .and_then(|x| x.check_width_compatibility(Some(name), width))
124         .unwrap_or_else(|_warning| {
125             /*warn(warning);*/
126             Spec::default_for_width(width)
127         })
128 }
129
130 fn decode_var(
131     decoder: &mut Decoder,
132     input: &crate::raw::Variable,
133     warn: impl Fn(Error),
134 ) -> Result<Option<Variable>, Error> {
135     match input.width {
136         0..=255 => (),
137         -1 => return Ok(None),
138         _ => {
139             return Err(Error::BadVariableWidth {
140                 offset: input.offset,
141                 width: input.width,
142             })
143         }
144     };
145     let width = input.width as Width;
146     let name = decoder.decode_string(&input.name.0, &warn);
147     let name = match Identifier::new(&name, decoder.encoding) {
148         Ok(name) => {
149             if !decoder.take_name(&name) {
150                 decoder.generate_name()
151             } else {
152                 name
153             }
154         }
155         Err(error) => {
156             warn(error.into());
157             decoder.generate_name()
158         }
159     };
160     let print_format = decode_format(input.print_format, &name.0, width);
161     let write_format = decode_format(input.write_format, &name.0, width);
162     let label = input.label.as_ref().map(|label| decoder.decode_string(&label.0, &warn).into());
163     Ok(Some(Variable { width, name, print_format, write_format, label }))
164 }
165
166 #[derive(Clone)]
167 pub struct Document(Vec<String>);
168
169 impl Decode for Document {
170     type Input = crate::raw::Document;
171
172     fn decode(decoder: &Decoder, input: &Self::Input, warn: impl Fn(Error)) -> Result<Self, Error> {
173         Ok(Document(
174             input
175                 .lines
176                 .iter()
177                 .map(|s| decoder.decode_string(&s.0, &warn).into())
178                 .collect(),
179         ))
180     }
181 }
182
183 pub use crate::raw::FloatInfo;
184 pub use crate::raw::IntegerInfo;
185
186 #[derive(Clone, Debug)]
187 pub enum MultipleResponseType {
188     MultipleDichotomy {
189         value: String,
190         labels: CategoryLabels,
191     },
192     MultipleCategory,
193 }
194 #[derive(Clone, Debug)]
195 pub struct MultipleResponseSet {
196     pub name: String,
197     pub label: String,
198     pub mr_type: MultipleResponseType,
199     pub vars: Vec<String>,
200 }
201
202 #[derive(Clone, Debug)]
203 pub struct MultipleResponseRecord(Vec<MultipleResponseSet>);
204
205 #[derive(Clone, Debug)]
206 pub struct ProductInfo(String);
207
208 pub enum Measure {
209     Nominal,
210     Ordinal,
211     Scale,
212 }
213
214 pub enum Alignment {
215     Left,
216     Right,
217     Center,
218 }
219
220 pub struct VarDisplay {
221     pub measure: Option<Measure>,
222     pub width: u32,
223     pub align: Option<Alignment>,
224 }
225
226 pub struct VarDisplayRecord(pub Vec<VarDisplay>);