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