2 fmt::{Display, Formatter, Result as FmtResult},
6 use thiserror::Error as ThisError;
13 #[derive(ThisError, Debug)]
15 #[error("Unknown format type {value}")]
16 UnknownFormat { value: u16 },
18 #[error("Output format {0} specifies width {}, but {} requires an even width.", .0.w, .0.format)]
19 OddWidthNotAllowed(UncheckedSpec),
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),
24 #[error("Output format {0} specifies decimal places, but {} format does not allow any decimals.", .0.format)]
25 DecimalsNotAllowedForFormat(UncheckedSpec),
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),
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 {
36 #[error("String variable is not compatible with numeric format {0}.")]
37 UnnamedVariableNotCompatibleWithNumericFormat(Format),
39 #[error("Numeric variable is not compatible with string format {0}.")]
40 UnnamedVariableNotCompatibleWithStringFormat(Format),
42 #[error("String variable {variable} is not compatible with numeric format {format}.")]
43 NamedVariableNotCompatibleWithNumericFormat { variable: String, format: Format },
45 #[error("Numeric variable {variable} is not compatible with string format {format}.")]
46 NamedVariableNotCompatibleWithStringFormat { variable: String, format: Format },
48 #[error("String variable {variable} with width {width} is not compatible with format {bad_spec}. Use format {good_spec} instead.")]
49 NamedStringVariableBadSpecWidth {
56 #[error("String variable with width {width} is not compatible with format {bad_spec}. Use format {good_spec} instead.")]
57 UnnamedStringVariableBadSpecWidth {
64 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
80 impl From<Format> for Category {
81 fn from(source: Format) -> Self {
83 Format::F | Format::Comma | Format::Dot | Format::Dollar | Format::Pct | Format::E => {
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,
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,
107 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
116 impl Display for CC {
117 fn fmt(&self, f: &mut Formatter) -> FmtResult {
129 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
131 // Basic numeric formats.
139 // Custom currency formats.
142 // Legacy numeric formats.
146 // Binary and hexadecimal formats.
155 // Time and date formats.
170 // Date component formats.
179 pub type Width = u16;
180 pub type SignedWidth = i16;
182 pub type Decimals = u8;
185 pub fn max_width(self) -> Width {
187 Self::P | Self::PK | Self::PIBHex | Self::RBHex => 16,
188 Self::IB | Self::PIB | Self::RB => 8,
190 Self::AHex => 32767 * 2,
195 pub fn min_width(self) -> Width {
197 // Basic numeric formats.
205 // Custom currency formats.
208 // Legacy numeric formats.
212 // Binary and hexadecimal formats.
221 // Time and date formats.
230 Self::DateTime => 17,
236 // Date component formats.
246 pub fn width_range(self) -> RangeInclusive<Width> {
247 self.min_width()..=self.max_width()
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,
261 Self::RB | Self::RBHex => 16,
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,
277 max.clamp(0, 16) as Decimals
280 pub fn takes_decimals(self) -> bool {
281 self.max_decimals(Width::MAX) > 0
284 pub fn category(self) -> Category {
288 pub fn width_step(self) -> Width {
289 if self.category() == Category::Hex || self == Self::AHex {
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 {
306 pub fn var_type(self) -> VarType {
308 Self::A | Self::AHex => VarType::String,
309 _ => VarType::Numeric,
313 /// Checks whether this format is valid for a variable with the given
315 pub fn check_type_compatibility(
317 variable: Option<&str>,
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(),
329 Err(Error::UnnamedVariableNotCompatibleWithNumericFormat(self))
332 (VarType::String, VarType::Numeric) => {
333 if let Some(variable) = variable {
334 Err(Error::NamedVariableNotCompatibleWithStringFormat {
335 variable: variable.into(),
339 Err(Error::UnnamedVariableNotCompatibleWithStringFormat(self))
347 impl Display for Format {
348 fn fmt(&self, f: &mut Formatter) -> FmtResult {
351 Self::Comma => "COMMA",
353 Self::Dollar => "DOLLAR",
356 Self::CC(cc) => return write!(f, "{}", cc),
363 Self::PIBHex => "PIBHEX",
365 Self::RBHex => "RBHEX",
366 Self::Date => "DATE",
367 Self::ADate => "ADATE",
368 Self::EDate => "EDATE",
369 Self::JDate => "JDATE",
370 Self::SDate => "SDATE",
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",
382 Self::AHex => "AHEX",
388 fn max_digits_for_bytes(bytes: usize) -> usize {
389 *[0, 3, 5, 8, 10, 13, 15, 17].get(bytes).unwrap_or(&20)
392 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
400 pub fn format(self) -> Format {
403 pub fn w(self) -> Width {
406 pub fn d(self) -> Decimals {
410 pub fn default_for_width(var_width: VarWidth) -> Self {
412 VarWidth::Numeric => Spec {
417 VarWidth::String(w) => Spec {
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) {
435 let d = d.clamp(0, format.max_decimals(w));
436 Self { format, w, d }
439 pub fn var_width(self) -> VarWidth {
441 Format::A => VarWidth::String(self.w),
442 Format::AHex => VarWidth::String(self.w / 2),
443 _ => VarWidth::Numeric,
447 pub fn var_type(self) -> VarType {
448 self.format.var_type()
451 /// Checks whether this format specification is valid for a variable with
452 /// width `var_width`.
453 pub fn check_width_compatibility(
455 variable: Option<&str>,
457 ) -> Result<Self, Error> {
458 // Verify that the format is right for the variable's type.
460 .check_type_compatibility(variable, var_width.into())?;
462 if let VarWidth::String(w) = var_width {
463 if var_width != self.var_width() {
465 let good_spec = if self.format == Format::A {
468 Spec { w: w * 2, ..self }
470 if let Some(variable) = variable {
471 return Err(Error::NamedStringVariableBadSpecWidth {
472 variable: variable.into(),
478 return Err(Error::UnnamedStringVariableBadSpecWidth {
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)?;
501 impl TryFrom<UncheckedSpec> for Spec {
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 {
520 Err(Error::DecimalsNotAllowedForWidth(source))
523 Ok(Spec { format, w, d })
528 impl TryFrom<u16> for Format {
531 fn try_from(source: u16) -> Result<Self, Self::Error> {
535 3 => Ok(Self::Comma),
536 4 => Ok(Self::Dollar),
539 7 => Ok(Self::PIBHex),
544 12 => Ok(Self::RBHex),
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),
558 30 => Ok(Self::WkYr),
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 }),
575 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
576 pub struct UncheckedSpec {
584 impl TryFrom<raw::Spec> for UncheckedSpec {
587 fn try_from(raw: raw::Spec) -> Result<Self, Self::Error> {
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 })
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)?;