work
[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;
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 type Width = u16;
179 type SignedWidth = i16;
180
181 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 fixed_from(source: &UncheckedSpec) -> Self {
408         let UncheckedSpec { format, w, d } = *source;
409         let (min, max) = format.width_range().into_inner();
410         let mut w = w.clamp(min, max);
411         if d <= format.max_decimals(Width::MAX) {
412             while d > format.max_decimals(w) {
413                 w += 1;
414                 assert!(w <= 40);
415             }
416         }
417         let d = d.clamp(0, format.max_decimals(w));
418         Self { format, w, d }
419     }
420
421     pub fn var_width(self) -> Width {
422         match self.format {
423             Format::A => self.w,
424             Format::AHex => self.w / 2,
425             _ => 0,
426         }
427     }
428
429     pub fn var_type(self) -> VarType {
430         self.format.var_type()
431     }
432
433     pub fn check_width_compatibility(self, variable: Option<&str>, w: Width) -> Result<(), Error> {
434         self.format.check_type_compatibility(variable, self.var_type())?;
435         let expected_width = self.var_width();
436         if w != expected_width {
437             let bad_spec = self;
438             let good_spec = if self.format == Format::A {
439                 Spec { w, ..self }
440             } else {
441                 Spec { w: w * 2, ..self }
442             };
443             if let Some(variable) = variable {
444                 Err(Error::NamedStringVariableBadSpecWidth {
445                     variable: variable.into(),
446                     width: w,
447                     bad_spec,
448                     good_spec,
449                 })
450             } else {
451                 Err(Error::UnnamedStringVariableBadSpecWidth {
452                     width: w,
453                     bad_spec,
454                     good_spec,
455                 })
456             }
457         } else {
458             Ok(())
459         }
460     }
461 }
462
463 impl Display for Spec {
464     fn fmt(&self, f: &mut Formatter) -> FmtResult {
465         write!(f, "{}{}", self.format, self.w)?;
466         if self.format.takes_decimals() || self.d > 0 {
467             write!(f, ".{}", self.d)?;
468         }
469         Ok(())
470     }
471 }
472
473 impl TryFrom<UncheckedSpec> for Spec {
474     type Error = Error;
475
476     fn try_from(source: UncheckedSpec) -> Result<Self, Self::Error> {
477         let UncheckedSpec { format, w, d } = source;
478         let max_d = format.max_decimals(w);
479         if w % format.width_step() != 0 {
480             Err(Error::OddWidthNotAllowed(source))
481         } else if !format.width_range().contains(&w) {
482             Err(Error::BadWidth(source))
483         } else if d > max_d {
484             if format.takes_decimals() {
485                 Err(Error::DecimalsNotAllowedForFormat(source))
486             } else if max_d > 0 {
487                 Err(Error::TooManyDecimalsForWidth {
488                     spec: source,
489                     max_d,
490                 })
491             } else {
492                 Err(Error::DecimalsNotAllowedForWidth(source))
493             }
494         } else {
495             Ok(Spec { format, w, d })
496         }
497     }
498 }
499
500 impl TryFrom<u16> for Format {
501     type Error = Error;
502
503     fn try_from(source: u16) -> Result<Self, Self::Error> {
504         match source {
505             1 => Ok(Self::A),
506             2 => Ok(Self::AHex),
507             3 => Ok(Self::Comma),
508             4 => Ok(Self::Dollar),
509             5 => Ok(Self::F),
510             6 => Ok(Self::IB),
511             7 => Ok(Self::PIBHex),
512             8 => Ok(Self::P),
513             9 => Ok(Self::PIB),
514             10 => Ok(Self::PK),
515             11 => Ok(Self::RB),
516             12 => Ok(Self::RBHex),
517             15 => Ok(Self::Z),
518             16 => Ok(Self::N),
519             17 => Ok(Self::E),
520             20 => Ok(Self::Date),
521             21 => Ok(Self::Time),
522             22 => Ok(Self::DateTime),
523             23 => Ok(Self::ADate),
524             24 => Ok(Self::JDate),
525             25 => Ok(Self::DTime),
526             26 => Ok(Self::WkDay),
527             27 => Ok(Self::Month),
528             28 => Ok(Self::MoYr),
529             29 => Ok(Self::QYr),
530             30 => Ok(Self::WkYr),
531             31 => Ok(Self::Pct),
532             32 => Ok(Self::Dot),
533             33 => Ok(Self::CC(CC::A)),
534             34 => Ok(Self::CC(CC::B)),
535             35 => Ok(Self::CC(CC::C)),
536             36 => Ok(Self::CC(CC::D)),
537             37 => Ok(Self::CC(CC::E)),
538             38 => Ok(Self::EDate),
539             39 => Ok(Self::SDate),
540             40 => Ok(Self::MTime),
541             41 => Ok(Self::YMDHMS),
542             _ => Err(Error::UnknownFormat { value: source }),
543         }
544     }
545 }
546
547 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
548 pub struct UncheckedSpec {
549     pub format: Format,
550
551     pub w: Width,
552
553     pub d: Decimals,
554 }
555
556 impl TryFrom<u32> for UncheckedSpec {
557     type Error = Error;
558
559     fn try_from(source: u32) -> Result<Self, Self::Error> {
560         let raw_format = (source >> 16) as u16;
561         let format = raw_format.try_into()?;
562         let w = ((source >> 8) & 0xff) as Width;
563         let d = (source & 0xff) as Decimals;
564         Ok(Self { format, w, d })
565     }
566 }
567
568 impl Display for UncheckedSpec {
569     fn fmt(&self, f: &mut Formatter) -> FmtResult {
570         write!(f, "{}{}", self.format, self.w)?;
571         if self.format.takes_decimals() || self.d > 0 {
572             write!(f, ".{}", self.d)?;
573         }
574         Ok(())
575     }
576 }