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