decodedrecord works
[pspp] / rust / src / format.rs
1 use std::{
2     fmt::{Display, Formatter, Result as FmtResult},
3     ops::RangeInclusive,
4 };
5
6 use thiserror::Error as ThisError;
7
8 use crate::{
9     dictionary::VarWidth,
10     raw::{self, VarType},
11 };
12
13 #[derive(ThisError, Debug)]
14 pub enum Error {
15     #[error("Unknown format type {value}.")]
16     UnknownFormat { value: u16 },
17
18     #[error("Output format {0} specifies width {}, but {} requires an even width.", .0.w, .0.format)]
19     OddWidthNotAllowed(UncheckedSpec),
20
21     #[error("Output format {0} specifies width {}, but {} requires a width between {} and {}.", .0.w, .0.format, .0.format.min_width(), .0.format.max_width())]
22     BadWidth(UncheckedSpec),
23
24     #[error("Output format {0} specifies decimal places, but {} format does not allow any decimals.", .0.format)]
25     DecimalsNotAllowedForFormat(UncheckedSpec),
26
27     #[error("Output format {0} specifies {} decimal places, but with a width of {}, {} does not allow any decimal places.", .0.d, .0.w, .0.format)]
28     DecimalsNotAllowedForWidth(UncheckedSpec),
29
30     #[error("Output format {spec} specifies {} decimal places but, with a width of {}, {} allows at most {max_d} decimal places.", .spec.d, .spec.w, .spec.format)]
31     TooManyDecimalsForWidth {
32         spec: UncheckedSpec,
33         max_d: Decimals,
34     },
35
36     #[error("String variable is not compatible with numeric format {0}.")]
37     UnnamedVariableNotCompatibleWithNumericFormat(Format),
38
39     #[error("Numeric variable is not compatible with string format {0}.")]
40     UnnamedVariableNotCompatibleWithStringFormat(Format),
41
42     #[error("String variable {variable} with width {width} is not compatible with format {bad_spec}.  Use format {good_spec} instead.")]
43     NamedStringVariableBadSpecWidth {
44         variable: String,
45         width: Width,
46         bad_spec: Spec,
47         good_spec: Spec,
48     },
49
50     #[error("String variable with width {width} is not compatible with format {bad_spec}.  Use format {good_spec} instead.")]
51     UnnamedStringVariableBadSpecWidth {
52         width: Width,
53         bad_spec: Spec,
54         good_spec: Spec,
55     },
56 }
57
58 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
59 pub enum Category {
60     // Numeric formats.
61     Basic,
62     Custom,
63     Legacy,
64     Binary,
65     Hex,
66     Date,
67     Time,
68     DateComponent,
69
70     // String formats.
71     String,
72 }
73
74 impl From<Format> for Category {
75     fn from(source: Format) -> Self {
76         match source {
77             Format::F | Format::Comma | Format::Dot | Format::Dollar | Format::Pct | Format::E => {
78                 Self::Basic
79             }
80             Format::CC(_) => Self::Custom,
81             Format::N | Format::Z => Self::Legacy,
82             Format::P | Format::PK | Format::IB | Format::PIB | Format::RB => Self::Binary,
83             Format::PIBHex | Format::RBHex => Self::Hex,
84             Format::Date
85             | Format::ADate
86             | Format::EDate
87             | Format::JDate
88             | Format::SDate
89             | Format::QYr
90             | Format::MoYr
91             | Format::WkYr
92             | Format::DateTime
93             | Format::YMDHMS => Self::Date,
94             Format::MTime | Format::Time | Format::DTime => Self::Time,
95             Format::WkDay | Format::Month => Self::DateComponent,
96             Format::A | Format::AHex => Self::String,
97         }
98     }
99 }
100
101 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
102 pub enum CC {
103     A,
104     B,
105     C,
106     D,
107     E,
108 }
109
110 impl Display for CC {
111     fn fmt(&self, f: &mut Formatter) -> FmtResult {
112         let s = match self {
113             CC::A => "A",
114             CC::B => "B",
115             CC::C => "C",
116             CC::D => "D",
117             CC::E => "E",
118         };
119         write!(f, "{}", s)
120     }
121 }
122
123 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
124 pub enum Format {
125     // Basic numeric formats.
126     F,
127     Comma,
128     Dot,
129     Dollar,
130     Pct,
131     E,
132
133     // Custom currency formats.
134     CC(CC),
135
136     // Legacy numeric formats.
137     N,
138     Z,
139
140     // Binary and hexadecimal formats.
141     P,
142     PK,
143     IB,
144     PIB,
145     PIBHex,
146     RB,
147     RBHex,
148
149     // Time and date formats.
150     Date,
151     ADate,
152     EDate,
153     JDate,
154     SDate,
155     QYr,
156     MoYr,
157     WkYr,
158     DateTime,
159     YMDHMS,
160     MTime,
161     Time,
162     DTime,
163
164     // Date component formats.
165     WkDay,
166     Month,
167
168     // String formats.
169     A,
170     AHex,
171 }
172
173 pub type Width = u16;
174 pub type SignedWidth = i16;
175
176 pub type Decimals = u8;
177
178 impl Format {
179     pub fn max_width(self) -> Width {
180         match self {
181             Self::P | Self::PK | Self::PIBHex | Self::RBHex => 16,
182             Self::IB | Self::PIB | Self::RB => 8,
183             Self::A => 32767,
184             Self::AHex => 32767 * 2,
185             _ => 40,
186         }
187     }
188
189     pub fn min_width(self) -> Width {
190         match self {
191             // Basic numeric formats.
192             Self::F => 1,
193             Self::Comma => 1,
194             Self::Dot => 1,
195             Self::Dollar => 2,
196             Self::Pct => 2,
197             Self::E => 6,
198
199             // Custom currency formats.
200             Self::CC(_) => 2,
201
202             // Legacy numeric formats.
203             Self::N => 1,
204             Self::Z => 1,
205
206             // Binary and hexadecimal formats.
207             Self::P => 1,
208             Self::PK => 1,
209             Self::IB => 1,
210             Self::PIB => 1,
211             Self::PIBHex => 2,
212             Self::RB => 2,
213             Self::RBHex => 4,
214
215             // Time and date formats.
216             Self::Date => 9,
217             Self::ADate => 8,
218             Self::EDate => 8,
219             Self::JDate => 5,
220             Self::SDate => 8,
221             Self::QYr => 6,
222             Self::MoYr => 6,
223             Self::WkYr => 8,
224             Self::DateTime => 17,
225             Self::YMDHMS => 16,
226             Self::MTime => 5,
227             Self::Time => 5,
228             Self::DTime => 8,
229
230             // Date component formats.
231             Self::WkDay => 2,
232             Self::Month => 3,
233
234             // String formats.
235             Self::A => 1,
236             Self::AHex => 2,
237         }
238     }
239
240     pub fn width_range(self) -> RangeInclusive<Width> {
241         self.min_width()..=self.max_width()
242     }
243
244     pub fn max_decimals(self, width: Width) -> Decimals {
245         let width = width.clamp(1, 40) as SignedWidth;
246         let max = match self {
247             Self::F | Self::Comma | Self::Dot | Self::CC(_) => width - 1,
248             Self::Dollar | Self::Pct => width - 2,
249             Self::E => width - 7,
250             Self::N | Self::Z => width,
251             Self::P => width * 2 - 1,
252             Self::PK => width * 2,
253             Self::IB | Self::PIB => max_digits_for_bytes(width as usize) as SignedWidth,
254             Self::PIBHex => 0,
255             Self::RB | Self::RBHex => 16,
256             Self::Date
257             | Self::ADate
258             | Self::EDate
259             | Self::JDate
260             | Self::SDate
261             | Self::QYr
262             | Self::MoYr
263             | Self::WkYr => 0,
264             Self::DateTime => width - 21,
265             Self::YMDHMS => width - 20,
266             Self::MTime => width - 6,
267             Self::Time => width - 9,
268             Self::DTime => width - 12,
269             Self::WkDay | Self::Month | Self::A | Self::AHex => 0,
270         };
271         max.clamp(0, 16) as Decimals
272     }
273
274     pub fn takes_decimals(self) -> bool {
275         self.max_decimals(Width::MAX) > 0
276     }
277
278     pub fn category(self) -> Category {
279         self.into()
280     }
281
282     pub fn width_step(self) -> Width {
283         if self.category() == Category::Hex || self == Self::AHex {
284             2
285         } else {
286             1
287         }
288     }
289
290     pub fn clamp_width(self, width: Width) -> Width {
291         let (min, max) = self.width_range().into_inner();
292         let width = width.clamp(min, max);
293         if self.width_step() == 2 {
294             width / 2 * 2
295         } else {
296             width
297         }
298     }
299
300     pub fn var_type(self) -> VarType {
301         match self {
302             Self::A | Self::AHex => VarType::String,
303             _ => VarType::Numeric,
304         }
305     }
306
307     /// Checks whether this format is valid for a variable with the given
308     /// `var_type`.
309     pub fn check_type_compatibility(self, var_type: VarType) -> Result<(), Error> {
310         let my_type = self.var_type();
311         match (my_type, var_type) {
312             (VarType::Numeric, VarType::String) => {
313                 Err(Error::UnnamedVariableNotCompatibleWithNumericFormat(self))
314             }
315             (VarType::String, VarType::Numeric) => {
316                 Err(Error::UnnamedVariableNotCompatibleWithStringFormat(self))
317             }
318             _ => Ok(()),
319         }
320     }
321 }
322
323 impl Display for Format {
324     fn fmt(&self, f: &mut Formatter) -> FmtResult {
325         let s = match self {
326             Self::F => "F",
327             Self::Comma => "COMMA",
328             Self::Dot => "DOT",
329             Self::Dollar => "DOLLAR",
330             Self::Pct => "PCT",
331             Self::E => "E",
332             Self::CC(cc) => return write!(f, "{}", cc),
333             Self::N => "N",
334             Self::Z => "Z",
335             Self::P => "P",
336             Self::PK => "PK",
337             Self::IB => "IB",
338             Self::PIB => "PIB",
339             Self::PIBHex => "PIBHEX",
340             Self::RB => "RB",
341             Self::RBHex => "RBHEX",
342             Self::Date => "DATE",
343             Self::ADate => "ADATE",
344             Self::EDate => "EDATE",
345             Self::JDate => "JDATE",
346             Self::SDate => "SDATE",
347             Self::QYr => "QYR",
348             Self::MoYr => "MOYR",
349             Self::WkYr => "WKYR",
350             Self::DateTime => "DATETIME",
351             Self::YMDHMS => "YMDHMS",
352             Self::MTime => "MTIME",
353             Self::Time => "TIME",
354             Self::DTime => "DTIME",
355             Self::WkDay => "WKDAY",
356             Self::Month => "MONTH",
357             Self::A => "A",
358             Self::AHex => "AHEX",
359         };
360         write!(f, "{}", s)
361     }
362 }
363
364 fn max_digits_for_bytes(bytes: usize) -> usize {
365     *[0, 3, 5, 8, 10, 13, 15, 17].get(bytes).unwrap_or(&20)
366 }
367
368 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
369 pub struct Spec {
370     format: Format,
371     w: Width,
372     d: Decimals,
373 }
374
375 impl Spec {
376     pub fn format(self) -> Format {
377         self.format
378     }
379     pub fn w(self) -> Width {
380         self.w
381     }
382     pub fn d(self) -> Decimals {
383         self.d
384     }
385
386     pub fn default_for_width(var_width: VarWidth) -> Self {
387         match var_width {
388             VarWidth::Numeric => Spec {
389                 format: Format::F,
390                 w: 8,
391                 d: 2,
392             },
393             VarWidth::String(w) => Spec {
394                 format: Format::A,
395                 w,
396                 d: 0,
397             },
398         }
399     }
400
401     pub fn fixed_from(source: &UncheckedSpec) -> Self {
402         let UncheckedSpec { format, w, d } = *source;
403         let (min, max) = format.width_range().into_inner();
404         let mut w = w.clamp(min, max);
405         if d <= format.max_decimals(Width::MAX) {
406             while d > format.max_decimals(w) {
407                 w += 1;
408                 assert!(w <= 40);
409             }
410         }
411         let d = d.clamp(0, format.max_decimals(w));
412         Self { format, w, d }
413     }
414
415     pub fn var_width(self) -> VarWidth {
416         match self.format {
417             Format::A => VarWidth::String(self.w),
418             Format::AHex => VarWidth::String(self.w / 2),
419             _ => VarWidth::Numeric,
420         }
421     }
422
423     pub fn var_type(self) -> VarType {
424         self.format.var_type()
425     }
426
427     /// Checks whether this format specification is valid for a variable with
428     /// width `var_width`.
429     pub fn check_width_compatibility(self, var_width: VarWidth) -> Result<Self, Error> {
430         // Verify that the format is right for the variable's type.
431         self.format.check_type_compatibility(var_width.into())?;
432
433         if let VarWidth::String(w) = var_width {
434             if var_width != self.var_width() {
435                 let bad_spec = self;
436                 let good_spec = if self.format == Format::A {
437                     Spec { w, ..self }
438                 } else {
439                     Spec { w: w * 2, ..self }
440                 };
441                 return Err(Error::UnnamedStringVariableBadSpecWidth {
442                     width: w,
443                     bad_spec,
444                     good_spec,
445                 });
446             }
447         }
448
449         Ok(self)
450     }
451 }
452
453 impl Display for Spec {
454     fn fmt(&self, f: &mut Formatter) -> FmtResult {
455         write!(f, "{}{}", self.format, self.w)?;
456         if self.format.takes_decimals() || self.d > 0 {
457             write!(f, ".{}", self.d)?;
458         }
459         Ok(())
460     }
461 }
462
463 impl TryFrom<UncheckedSpec> for Spec {
464     type Error = Error;
465
466     fn try_from(source: UncheckedSpec) -> Result<Self, Self::Error> {
467         let UncheckedSpec { format, w, d } = source;
468         let max_d = format.max_decimals(w);
469         if w % format.width_step() != 0 {
470             Err(Error::OddWidthNotAllowed(source))
471         } else if !format.width_range().contains(&w) {
472             Err(Error::BadWidth(source))
473         } else if d > max_d {
474             if format.takes_decimals() {
475                 Err(Error::DecimalsNotAllowedForFormat(source))
476             } else if max_d > 0 {
477                 Err(Error::TooManyDecimalsForWidth {
478                     spec: source,
479                     max_d,
480                 })
481             } else {
482                 Err(Error::DecimalsNotAllowedForWidth(source))
483             }
484         } else {
485             Ok(Spec { format, w, d })
486         }
487     }
488 }
489
490 impl TryFrom<u16> for Format {
491     type Error = Error;
492
493     fn try_from(source: u16) -> Result<Self, Self::Error> {
494         match source {
495             1 => Ok(Self::A),
496             2 => Ok(Self::AHex),
497             3 => Ok(Self::Comma),
498             4 => Ok(Self::Dollar),
499             5 => Ok(Self::F),
500             6 => Ok(Self::IB),
501             7 => Ok(Self::PIBHex),
502             8 => Ok(Self::P),
503             9 => Ok(Self::PIB),
504             10 => Ok(Self::PK),
505             11 => Ok(Self::RB),
506             12 => Ok(Self::RBHex),
507             15 => Ok(Self::Z),
508             16 => Ok(Self::N),
509             17 => Ok(Self::E),
510             20 => Ok(Self::Date),
511             21 => Ok(Self::Time),
512             22 => Ok(Self::DateTime),
513             23 => Ok(Self::ADate),
514             24 => Ok(Self::JDate),
515             25 => Ok(Self::DTime),
516             26 => Ok(Self::WkDay),
517             27 => Ok(Self::Month),
518             28 => Ok(Self::MoYr),
519             29 => Ok(Self::QYr),
520             30 => Ok(Self::WkYr),
521             31 => Ok(Self::Pct),
522             32 => Ok(Self::Dot),
523             33 => Ok(Self::CC(CC::A)),
524             34 => Ok(Self::CC(CC::B)),
525             35 => Ok(Self::CC(CC::C)),
526             36 => Ok(Self::CC(CC::D)),
527             37 => Ok(Self::CC(CC::E)),
528             38 => Ok(Self::EDate),
529             39 => Ok(Self::SDate),
530             40 => Ok(Self::MTime),
531             41 => Ok(Self::YMDHMS),
532             _ => Err(Error::UnknownFormat { value: source }),
533         }
534     }
535 }
536
537 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
538 pub struct UncheckedSpec {
539     pub format: Format,
540
541     pub w: Width,
542
543     pub d: Decimals,
544 }
545
546 impl TryFrom<raw::Spec> for UncheckedSpec {
547     type Error = Error;
548
549     fn try_from(raw: raw::Spec) -> Result<Self, Self::Error> {
550         let raw = raw.0;
551         let raw_format = (raw >> 16) as u16;
552         let format = raw_format.try_into()?;
553         let w = ((raw >> 8) & 0xff) as Width;
554         let d = (raw & 0xff) as Decimals;
555         Ok(Self { format, w, d })
556     }
557 }
558
559 impl Display for UncheckedSpec {
560     fn fmt(&self, f: &mut Formatter) -> FmtResult {
561         write!(f, "{}{}", self.format, self.w)?;
562         if self.format.takes_decimals() || self.d > 0 {
563             write!(f, ".{}", self.d)?;
564         }
565         Ok(())
566     }
567 }