work
authorBen Pfaff <blp@cs.stanford.edu>
Tue, 20 May 2025 20:07:31 +0000 (13:07 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Tue, 20 May 2025 20:07:31 +0000 (13:07 -0700)
18 files changed:
rust/pspp/src/dictionary.rs
rust/pspp/src/endian.rs
rust/pspp/src/format/display.rs [deleted file]
rust/pspp/src/format/display/mod.rs [new file with mode: 0644]
rust/pspp/src/format/display/test.rs [new file with mode: 0644]
rust/pspp/src/format/mod.rs
rust/pspp/src/identifier.rs
rust/pspp/src/output/pivot/look_xml.rs
rust/pspp/src/output/pivot/mod.rs
rust/pspp/src/output/pivot/output.rs
rust/pspp/src/output/pivot/test.rs
rust/pspp/src/output/pivot/tlo.rs
rust/pspp/src/output/render.rs
rust/pspp/src/output/spv.rs
rust/pspp/src/output/text.rs
rust/pspp/src/sys/cooked.rs
rust/pspp/src/sys/raw.rs
rust/pspp/src/sys/test.rs

index bbf97749f834413f50c9773b388cc9b19c948936..cbbde90349e636923ec88ad7edb7d28cf3d9a858 100644 (file)
@@ -7,6 +7,7 @@ use std::{
     fmt::{Debug, Formatter, Result as FmtResult},
     hash::Hash,
     ops::{Bound, RangeBounds, RangeInclusive},
+    str::FromStr,
 };
 
 use encoding_rs::Encoding;
@@ -322,8 +323,17 @@ pub struct Dictionary {
     pub encoding: &'static Encoding,
 }
 
-#[derive(Debug)]
-pub struct DuplicateVariableName;
+#[derive(Debug, ThisError)]
+pub enum AddVarError {
+    #[error("Duplicate variable name {0}.")]
+    DuplicateVariableName(Identifier),
+
+    #[error("Variable encoding {} does not match dictionary encoding {}.", var_encoding.name(), dict_encoding.name())]
+    WrongEncoding {
+        var_encoding: &'static Encoding,
+        dict_encoding: &'static Encoding,
+    },
+}
 
 impl Dictionary {
     /// Creates a new, empty dictionary with the specified `encoding`.
@@ -357,15 +367,24 @@ impl Dictionary {
             .collect()
     }
 
-    /// Adds `variable` at the end of the dictionary and returns its index.  The
-    /// operation fails if the dictionary already contains a variable with the
-    /// same name (or a variant with different case).
-    pub fn add_var(&mut self, variable: Variable) -> Result<DictIndex, DuplicateVariableName> {
-        let (index, inserted) = self.variables.insert_full(ByIdentifier::new(variable));
-        if inserted {
-            Ok(index)
+    /// Adds `variable` at the end of the dictionary and returns its index.
+    ///
+    /// The operation fails if the dictionary already contains a variable with
+    /// the same name (or a variant with different case), or if `variable`'s
+    /// encoding differs from the dictionary's
+    pub fn add_var(&mut self, variable: Variable) -> Result<DictIndex, AddVarError> {
+        if variable.encoding != self.encoding {
+            Err(AddVarError::WrongEncoding {
+                var_encoding: variable.encoding,
+                dict_encoding: self.encoding,
+            })
         } else {
-            Err(DuplicateVariableName)
+            match self.variables.insert_full(ByIdentifier::new(variable)) {
+                (index, true) => Ok(index),
+                (index, false) => Err(AddVarError::DuplicateVariableName(
+                    self.variables[index].name.clone(),
+                )),
+            }
         }
     }
 
@@ -520,17 +539,21 @@ impl Dictionary {
         assert!(self.try_rename_var(index, new_name));
     }
 
-    pub fn display_variables(&self) -> DisplayVariables {
-        DisplayVariables::new(self)
+    pub fn output_variables(&self) -> OutputVariables {
+        OutputVariables::new(self)
+    }
+
+    pub fn output_value_labels(&self) -> OutputValueLabels {
+        OutputValueLabels::new(self)
     }
 }
 
-pub struct DisplayVariables<'a> {
+pub struct OutputVariables<'a> {
     dictionary: &'a Dictionary,
     fields: EnumMap<VariableField, bool>,
 }
 
-impl<'a> DisplayVariables<'a> {
+impl<'a> OutputVariables<'a> {
     fn new(dictionary: &'a Dictionary) -> Self {
         Self {
             dictionary,
@@ -557,12 +580,11 @@ impl<'a> DisplayVariables<'a> {
         let mut pt = PivotTable::new(vec![
             (Axis3::Y, Dimension::new(names)),
             (Axis3::X, Dimension::new(attributes)),
-        ]);
+        ])
+        .with_show_empty();
         for (var_index, variable) in self.dictionary.variables.iter().enumerate() {
             for (field, field_index) in &columns {
-                if let Some(value) =
-                    Self::get_field_value(var_index, variable, *field, self.dictionary.encoding)
-                {
+                if let Some(value) = Self::get_field_value(var_index, variable, *field) {
                     pt.insert(&[var_index, *field_index], value);
                 }
             }
@@ -575,7 +597,6 @@ impl<'a> DisplayVariables<'a> {
         index: usize,
         variable: &Variable,
         field: VariableField,
-        encoding: &'static Encoding,
     ) -> Option<PivotValue> {
         match field {
             VariableField::Position => Some(PivotValue::new_integer(Some(index as f64 + 1.0))),
@@ -585,9 +606,7 @@ impl<'a> DisplayVariables<'a> {
             VariableField::Measure => variable
                 .measure
                 .map(|measure| PivotValue::new_text(measure.as_str())),
-            VariableField::Role => variable
-                .role
-                .map(|role| PivotValue::new_text(role.as_str())),
+            VariableField::Role => Some(PivotValue::new_text(variable.role.as_str())),
             VariableField::Width => {
                 Some(PivotValue::new_integer(Some(variable.display_width as f64)))
             }
@@ -598,9 +617,86 @@ impl<'a> DisplayVariables<'a> {
             VariableField::WriteFormat => {
                 Some(PivotValue::new_user_text(variable.write_format.to_string()))
             }
-            VariableField::MissingValues => Some(PivotValue::new_user_text(
-                variable.missing_values.display(encoding).to_string(),
-            )),
+            VariableField::MissingValues if !variable.missing_values.is_empty() => {
+                Some(PivotValue::new_user_text(
+                    variable
+                        .missing_values
+                        .display(variable.encoding)
+                        .to_string(),
+                ))
+            }
+            VariableField::MissingValues => None,
+        }
+    }
+}
+
+pub struct OutputValueLabels<'a> {
+    dictionary: &'a Dictionary,
+}
+
+impl<'a> OutputValueLabels<'a> {
+    fn new(dictionary: &'a Dictionary) -> Self {
+        Self { dictionary }
+    }
+    fn any_value_labels(&self) -> bool {
+        self.dictionary
+            .variables
+            .iter()
+            .any(|variable| !variable.value_labels.is_empty())
+    }
+    pub fn to_pivot_table(&self) -> Option<PivotTable> {
+        if !self.any_value_labels() {
+            return None;
+        }
+
+        let mut values = Group::new("Variable Value").with_label_shown();
+        for variable in &self.dictionary.variables {
+            let mut group = Group::new(&**variable);
+            let mut values = variable.value_labels.iter().collect::<Vec<_>>();
+            values.sort();
+            for (value, label) in values {
+                let value = PivotValue::new_variable(variable);
+                //group.push();
+                todo!()
+            }
+        }
+
+        todo!()
+    }
+
+    fn get_field_value(
+        index: usize,
+        variable: &Variable,
+        field: VariableField,
+    ) -> Option<PivotValue> {
+        match field {
+            VariableField::Position => Some(PivotValue::new_integer(Some(index as f64 + 1.0))),
+            VariableField::Label => variable
+                .label()
+                .map(|label| PivotValue::new_user_text(label)),
+            VariableField::Measure => variable
+                .measure
+                .map(|measure| PivotValue::new_text(measure.as_str())),
+            VariableField::Role => Some(PivotValue::new_text(variable.role.as_str())),
+            VariableField::Width => {
+                Some(PivotValue::new_integer(Some(variable.display_width as f64)))
+            }
+            VariableField::Alignment => Some(PivotValue::new_text(variable.alignment.as_str())),
+            VariableField::PrintFormat => {
+                Some(PivotValue::new_user_text(variable.print_format.to_string()))
+            }
+            VariableField::WriteFormat => {
+                Some(PivotValue::new_user_text(variable.write_format.to_string()))
+            }
+            VariableField::MissingValues if !variable.missing_values.is_empty() => {
+                Some(PivotValue::new_user_text(
+                    variable
+                        .missing_values
+                        .display(variable.encoding)
+                        .to_string(),
+                ))
+            }
+            VariableField::MissingValues => None,
         }
     }
 }
@@ -654,54 +750,56 @@ pub enum Role {
     Input,
     Target,
     Both,
+    None,
     Partition,
     Split,
 }
 
 impl Role {
-    /// Convert `input` to [Role].
-    ///
-    /// This can't be `FromStr<Option<Role>` because defining traits on `Option`
-    /// is not allowed.
-    fn try_from_str(input: &str) -> Result<Option<Role>, InvalidRole> {
+    fn as_str(&self) -> &'static str {
+        match self {
+            Role::Input => "Input",
+            Role::Target => "Target",
+            Role::Both => "Both",
+            Role::None => "None",
+            Role::Partition => "Partition",
+            Role::Split => "Split",
+        }
+    }
+}
+
+impl FromStr for Role {
+    type Err = InvalidRole;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
         for (string, value) in [
-            ("input", Some(Role::Input)),
-            ("target", Some(Role::Target)),
-            ("both", Some(Role::Both)),
-            ("partition", Some(Role::Partition)),
-            ("split", Some(Role::Split)),
-            ("none", None),
+            ("input", Role::Input),
+            ("target", Role::Target),
+            ("both", Role::Both),
+            ("none", Role::None),
+            ("partition", Role::Partition),
+            ("split", Role::Split),
         ] {
-            if string.eq_ignore_ascii_case(input) {
+            if string.eq_ignore_ascii_case(s) {
                 return Ok(value);
             }
         }
-        Err(InvalidRole::UnknownRole(input.into()))
+        Err(InvalidRole::UnknownRole(s.into()))
     }
+}
 
-    /// Convert `integer` to [Role].
-    ///
-    /// This can't be `TryFrom<Option<Role>>` because defining traits on
-    /// `Option>` is not allowed.
-    fn try_from_integer(integer: i32) -> Result<Option<Role>, InvalidRole> {
-        match integer {
-            0 => Ok(Some(Role::Input)),
-            1 => Ok(Some(Role::Target)),
-            2 => Ok(Some(Role::Both)),
-            4 => Ok(Some(Role::Partition)),
-            5 => Ok(Some(Role::Split)),
-            3 => Ok(None),
-            _ => Err(InvalidRole::UnknownRole(integer.to_string())),
-        }
-    }
+impl TryFrom<i32> for Role {
+    type Error = InvalidRole;
 
-    fn as_str(&self) -> &'static str {
-        match self {
-            Role::Input => "Input",
-            Role::Target => "Target",
-            Role::Both => "Both",
-            Role::Partition => "Partition",
-            Role::Split => "Split",
+    fn try_from(value: i32) -> Result<Self, Self::Error> {
+        match value {
+            0 => Ok(Role::Input),
+            1 => Ok(Role::Target),
+            2 => Ok(Role::Both),
+            3 => Ok(Role::None),
+            4 => Ok(Role::Partition),
+            5 => Ok(Role::Split),
+            _ => Err(InvalidRole::UnknownRole(value.to_string())),
         }
     }
 }
@@ -739,8 +837,8 @@ impl TryFrom<&Attributes> for Option<Role> {
         let role = Identifier::new("$@Role").unwrap();
         value.0.get(&role).map_or(Ok(None), |attribute| {
             if let Ok([string]) = <&[String; 1]>::try_from(attribute.as_slice()) {
-                match string.parse() {
-                    Ok(integer) => Role::try_from_integer(integer),
+                match string.parse::<i32>() {
+                    Ok(integer) => Ok(Some(Role::try_from(integer)?)),
                     Err(_) => Err(InvalidRole::UnknownRole(string.clone())),
                 }
             } else {
@@ -787,7 +885,7 @@ pub struct Variable {
     pub measure: Option<Measure>,
 
     /// Role in data analysis.
-    pub role: Option<Role>,
+    pub role: Role,
 
     /// Width of data column in GUI.
     pub display_width: u32,
@@ -804,10 +902,16 @@ pub struct Variable {
 
     /// Variable attributes.
     pub attributes: Attributes,
+
+    /// Encoding for [Value]s inside this variable.
+    ///
+    /// The variables in a [Dictionary] must all use the same encoding as the
+    /// dictionary.
+    pub encoding: &'static Encoding,
 }
 
 impl Variable {
-    pub fn new(name: Identifier, width: VarWidth) -> Self {
+    pub fn new(name: Identifier, width: VarWidth, encoding: &'static Encoding) -> Self {
         let var_type = VarType::from(width);
         let leave = name.class().must_leave();
         Self {
@@ -819,12 +923,13 @@ impl Variable {
             value_labels: HashMap::new(),
             label: None,
             measure: Measure::default_for_type(var_type),
-            role: None,
+            role: Role::default(),
             display_width: width.default_display_width(),
             alignment: Alignment::default_for_type(var_type),
             leave,
             short_names: Vec::new(),
             attributes: Attributes::new(),
+            encoding,
         }
     }
 
index dc94b6d32e6927dd966e0c09f7968ef54096c57c..5b68a47d274ede5cde565535129a6ad2779d24a0 100644 (file)
@@ -1,3 +1,4 @@
+use enum_iterator::Sequence;
 use smallvec::SmallVec;
 
 /// The endianness for integer and floating-point numbers in SPSS system files.
@@ -5,7 +6,7 @@ use smallvec::SmallVec;
 /// SPSS system files can declare IBM 370 and DEC VAX floating-point
 /// representations, but no file that uses either of these has ever been found
 /// in the wild, so this code does not handle them.
-#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Sequence)]
 pub enum Endian {
     /// Big-endian: MSB at lowest address.
     #[cfg_attr(target_endian = "big", default)]
diff --git a/rust/pspp/src/format/display.rs b/rust/pspp/src/format/display.rs
deleted file mode 100644 (file)
index f82a14c..0000000
+++ /dev/null
@@ -1,2841 +0,0 @@
-use std::{
-    cmp::min,
-    fmt::{Display, Error as FmtError, Formatter, Result as FmtResult, Write as _},
-    io::{Error as IoError, Write as IoWrite},
-    str::from_utf8_unchecked,
-};
-
-use chrono::{Datelike, NaiveDate};
-use encoding_rs::{Encoding, UTF_8};
-use libm::frexp;
-use smallstr::SmallString;
-use smallvec::{Array, SmallVec};
-
-use crate::{
-    calendar::{calendar_offset_to_gregorian, day_of_year, month_name, short_month_name},
-    dictionary::Value,
-    endian::ToBytes,
-    format::{Category, DateTemplate, Decimal, Format, NumberStyle, Settings, TemplateItem, Type},
-    settings::{EndianSettings, Settings as PsppSettings},
-};
-
-pub struct DisplayValue<'a, 'b> {
-    format: Format,
-    settings: &'b Settings,
-    endian: EndianSettings,
-    value: &'a Value,
-    encoding: &'static Encoding,
-}
-
-impl Value {
-    /// Returns an object that implements [Display] for printing this `Value` as
-    /// `format`.  `encoding` specifies this `Value`'s encoding (therefore, it
-    /// is used only if this is a `Value::String`).
-    ///
-    /// [Display]: std::fmt::Display
-    pub fn display(&self, format: Format, encoding: &'static Encoding) -> DisplayValue {
-        DisplayValue::new(format, self, encoding)
-    }
-
-    pub fn display_plain(&self, encoding: &'static Encoding) -> DisplayValuePlain {
-        DisplayValuePlain {
-            value: self,
-            encoding,
-            quote_strings: true,
-        }
-    }
-}
-
-pub struct DisplayValuePlain<'a> {
-    value: &'a Value,
-    encoding: &'static Encoding,
-    quote_strings: bool,
-}
-
-impl DisplayValuePlain<'_> {
-    pub fn without_quotes(self) -> Self {
-        Self {
-            quote_strings: false,
-            ..self
-        }
-    }
-}
-
-impl Display for DisplayValuePlain<'_> {
-    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
-        match self.value {
-            Value::Number(None) => write!(f, "SYSMIS"),
-            Value::Number(Some(number)) if number.abs() < 0.0005 || number.abs() > 1e15 => {
-                write!(f, "{number:.}")
-            }
-            Value::Number(Some(number)) => write!(f, "{number:.e}"),
-            Value::String(string) => {
-                if self.quote_strings {
-                    write!(f, "\"{}\"", string.display(self.encoding))
-                } else {
-                    string.display(self.encoding).fmt(f)
-                }
-            }
-        }
-    }
-}
-
-impl Display for DisplayValue<'_, '_> {
-    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
-        let number = match self.value {
-            Value::Number(number) => *number,
-            Value::String(string) => {
-                if self.format.type_() == Type::AHex {
-                    for byte in &string.0 {
-                        write!(f, "{byte:02x}")?;
-                    }
-                } else {
-                    write!(
-                        f,
-                        "{}",
-                        self.encoding.decode_without_bom_handling(&string.0).0
-                    )?;
-                }
-                return Ok(());
-            }
-        };
-
-        let Some(number) = number else {
-            return self.missing(f);
-        };
-
-        match self.format.type_() {
-            Type::F
-            | Type::Comma
-            | Type::Dot
-            | Type::Dollar
-            | Type::Pct
-            | Type::E
-            | Type::CC(_) => self.number(f, number),
-            Type::N => self.n(f, number),
-            Type::Z => self.z(f, number),
-
-            Type::P | Type::PK | Type::IB | Type::PIB | Type::RB => self.fmt_binary(f),
-
-            Type::PIBHex => self.pibhex(f, number),
-            Type::RBHex => self.rbhex(f, number),
-            Type::Date
-            | Type::ADate
-            | Type::EDate
-            | Type::JDate
-            | Type::SDate
-            | Type::QYr
-            | Type::MoYr
-            | Type::WkYr
-            | Type::DateTime
-            | Type::YmdHms
-            | Type::MTime
-            | Type::Time
-            | Type::DTime
-            | Type::WkDay => self.date(f, number),
-            Type::Month => self.month(f, number),
-            Type::A | Type::AHex => unreachable!(),
-        }
-    }
-}
-
-impl<'a, 'b> DisplayValue<'a, 'b> {
-    pub fn new(format: Format, value: &'a Value, encoding: &'static Encoding) -> Self {
-        let settings = PsppSettings::global();
-        Self {
-            format,
-            value,
-            encoding,
-            settings: &settings.formats,
-            endian: settings.endian,
-        }
-    }
-    pub fn with_settings(self, settings: &'b Settings) -> Self {
-        Self { settings, ..self }
-    }
-    pub fn with_endian(self, endian: EndianSettings) -> Self {
-        Self { endian, ..self }
-    }
-    fn fmt_binary(&self, f: &mut Formatter) -> FmtResult {
-        let output = self.to_binary().unwrap();
-        for b in output {
-            f.write_char(b as char)?;
-        }
-        Ok(())
-    }
-    fn number(&self, f: &mut Formatter<'_>, number: f64) -> FmtResult {
-        if number.is_finite() {
-            let style = self.settings.number_style(self.format.type_);
-            if self.format.type_ != Type::E && number.abs() < 1.5 * power10(self.format.w()) {
-                let rounder = Rounder::new(style, number, self.format.d);
-                if self.decimal(f, &rounder, style, true)?
-                    || self.scientific(f, number, style, true)?
-                    || self.decimal(f, &rounder, style, false)?
-                {
-                    return Ok(());
-                }
-            }
-
-            if !self.scientific(f, number, style, false)? {
-                self.overflow(f)?;
-            }
-            Ok(())
-        } else {
-            self.infinite(f, number)
-        }
-    }
-
-    fn infinite(&self, f: &mut Formatter<'_>, number: f64) -> FmtResult {
-        if self.format.w >= 3 {
-            let s = if number.is_nan() {
-                "NaN"
-            } else if number.is_infinite() {
-                if number.is_sign_positive() {
-                    "+Infinity"
-                } else {
-                    "-Infinity"
-                }
-            } else {
-                "Unknown"
-            };
-            let w = self.format.w();
-            write!(f, "{s:>0$.*}", w)
-        } else {
-            self.overflow(f)
-        }
-    }
-
-    fn missing(&self, f: &mut Formatter<'_>) -> FmtResult {
-        match self.format.type_ {
-            Type::P | Type::PK | Type::IB | Type::PIB | Type::RB => return self.fmt_binary(f),
-            Type::RBHex => return self.rbhex(f, -f64::MAX),
-            _ => (),
-        }
-
-        let w = self.format.w() as isize;
-        let d = self.format.d() as isize;
-        let dot_position = match self.format.type_ {
-            Type::N => w - 1,
-            Type::Pct => w - d - 2,
-            Type::E => w - d - 5,
-            _ => w - d - 1,
-        };
-        let dot_position = dot_position.max(0) as u16;
-
-        for i in 0..self.format.w {
-            if i == dot_position {
-                write!(f, ".")?;
-            } else {
-                write!(f, " ")?;
-            }
-        }
-        Ok(())
-    }
-
-    fn overflow(&self, f: &mut Formatter<'_>) -> FmtResult {
-        for _ in 0..self.format.w {
-            write!(f, "*")?;
-        }
-        Ok(())
-    }
-
-    fn decimal(
-        &self,
-        f: &mut Formatter<'_>,
-        rounder: &Rounder,
-        style: &NumberStyle,
-        require_affixes: bool,
-    ) -> Result<bool, FmtError> {
-        for decimals in (0..=self.format.d).rev() {
-            // Make sure there's room for the number's magnitude, plus the
-            // negative suffix, plus (if negative) the negative prefix.
-            let RounderWidth {
-                mut width,
-                integer_digits,
-                negative,
-            } = rounder.width(decimals as usize);
-            width += style.neg_suffix.width;
-            if negative {
-                width += style.neg_prefix.width;
-            }
-            if width > self.format.w() {
-                continue;
-            }
-
-            // If there's room for the prefix and suffix, allocate
-            // space.  If the affixes are required, but there's no
-            // space, give up.
-            let add_affixes = allocate_space(style.affix_width(), self.format.w(), &mut width);
-            if !add_affixes && require_affixes {
-                continue;
-            }
-
-            // Check whether we should include grouping characters.  We need
-            // room for a complete set or we don't insert any at all.  We don't
-            // include grouping characters if decimal places were requested but
-            // they were all dropped.
-            let grouping = style.grouping.filter(|_| {
-                integer_digits > 3
-                    && (self.format.d == 0 || decimals > 0)
-                    && allocate_space((integer_digits - 1) / 3, self.format.w(), &mut width)
-            });
-
-            // Assemble number.
-            let magnitude = rounder.format(decimals as usize);
-            let mut output = SmallString::<[u8; 40]>::new();
-            for _ in width..self.format.w() {
-                output.push(' ');
-            }
-            if negative {
-                output.push_str(&style.neg_prefix.s);
-            }
-            if add_affixes {
-                output.push_str(&style.prefix.s);
-            }
-            if let Some(grouping) = grouping {
-                for (i, digit) in magnitude[..integer_digits].bytes().enumerate() {
-                    if i > 0 && (integer_digits - i) % 3 == 0 {
-                        output.push(grouping.into());
-                    }
-                    output.push(digit as char);
-                }
-            } else {
-                output.push_str(&magnitude[..integer_digits]);
-            }
-            if decimals > 0 {
-                output.push(style.decimal.into());
-                let s = &magnitude[integer_digits + 1..];
-                output.push_str(&s[..decimals as usize]);
-            }
-            if add_affixes {
-                output.push_str(&style.suffix.s);
-            }
-            if negative {
-                output.push_str(&style.neg_suffix.s);
-            } else {
-                for _ in 0..style.neg_suffix.width {
-                    output.push(' ');
-                }
-            }
-
-            debug_assert!(output.len() >= self.format.w());
-            debug_assert!(output.len() <= self.format.w() + style.extra_bytes);
-            f.write_str(&output)?;
-            return Ok(true);
-        }
-        Ok(false)
-    }
-
-    fn scientific(
-        &self,
-        f: &mut Formatter<'_>,
-        number: f64,
-        style: &NumberStyle,
-        require_affixes: bool,
-    ) -> Result<bool, FmtError> {
-        // Allocate minimum required space.
-        let mut width = 6 + style.neg_suffix.width;
-        if number < 0.0 {
-            width += style.neg_prefix.width;
-        }
-        if width > self.format.w() {
-            return Ok(false);
-        }
-
-        // Check for room for prefix and suffix.
-        let add_affixes = allocate_space(style.affix_width(), self.format.w(), &mut width);
-        if require_affixes && !add_affixes {
-            return Ok(false);
-        }
-
-        // Figure out number of characters we can use for the fraction, if any.
-        // (If that turns out to be `1`, then we'll output a decimal point
-        // without any digits following.)
-        let mut fraction_width = min(self.format.d as usize + 1, self.format.w() - width).min(16);
-        if self.format.type_ != Type::E && fraction_width == 1 {
-            fraction_width = 0;
-        }
-        width += fraction_width;
-
-        let mut output = SmallString::<[u8; 40]>::new();
-        for _ in width..self.format.w() {
-            output.push(' ');
-        }
-        if number < 0.0 {
-            output.push_str(&style.neg_prefix.s);
-        }
-        if add_affixes {
-            output.push_str(&style.prefix.s);
-        }
-        write!(
-            &mut output,
-            "{:.*E}",
-            fraction_width.saturating_sub(1),
-            number.abs()
-        )
-        .unwrap();
-        if fraction_width == 1 {
-            // Insert `.` before the `E`, to get a value like "1.E+000".
-            output.insert(output.find('E').unwrap(), '.');
-        }
-
-        // Rust always uses `.` as the decimal point. Translate to `,` if
-        // necessary.
-        if style.decimal == Decimal::Comma {
-            fix_decimal_point(&mut output);
-        }
-
-        // Make exponent have exactly three digits, plus sign.
-        let e = output.as_bytes().iter().position(|c| *c == b'E').unwrap();
-        let exponent: isize = output[e + 1..].parse().unwrap();
-        if exponent.abs() > 999 {
-            return Ok(false);
-        }
-        output.truncate(e + 1);
-        write!(&mut output, "{exponent:+04}").unwrap();
-
-        // Add suffixes.
-        if add_affixes {
-            output.push_str(&style.suffix.s);
-        }
-        if number.is_sign_negative() {
-            output.push_str(&style.neg_suffix.s);
-        } else {
-            for _ in 0..style.neg_suffix.width {
-                output.push(' ');
-            }
-        }
-
-        println!(
-            "{} for {number} width={width} fraction_width={fraction_width}: {output:?}",
-            self.format
-        );
-        debug_assert!(output.len() >= self.format.w());
-        debug_assert!(output.len() <= self.format.w() + style.extra_bytes);
-        f.write_str(&output)?;
-        Ok(true)
-    }
-
-    fn n(&self, f: &mut Formatter<'_>, number: f64) -> FmtResult {
-        if number < 0.0 {
-            return self.missing(f);
-        }
-
-        let legacy = LegacyFormat::new(number, self.format.d());
-        let w = self.format.w();
-        let len = legacy.len();
-        if len > w {
-            self.overflow(f)
-        } else {
-            write!(f, "{}{legacy}", Zeros(w.saturating_sub(len)))
-        }
-    }
-
-    fn z(&self, f: &mut Formatter<'_>, number: f64) -> FmtResult {
-        let legacy = LegacyFormat::new(number, self.format.d());
-        let w = self.format.w();
-        let len = legacy.len();
-        if len > w {
-            self.overflow(f)
-        } else {
-            let mut s = SmallString::<[u8; 40]>::new();
-            write!(&mut s, "{legacy}")?;
-            if number < 0.0 {
-                if let Some(last) = s.pop() {
-                    let last = last.to_digit(10).unwrap();
-                    s.push(b"}JKLMNOPQR"[last as usize] as char);
-                }
-            }
-            write!(f, "{}{s}", Zeros(w.saturating_sub(len)))
-        }
-    }
-
-    fn pibhex(&self, f: &mut Formatter<'_>, number: f64) -> FmtResult {
-        if number < 0.0 {
-            self.overflow(f)
-        } else {
-            let number = number.round();
-            if number >= power256(self.format.w / 2) {
-                self.overflow(f)
-            } else {
-                let binary = integer_to_binary(number as u64, self.format.w / 2);
-                output_hex(f, &binary)
-            }
-        }
-    }
-
-    fn rbhex(&self, f: &mut Formatter<'_>, number: f64) -> FmtResult {
-        let rb = self.rb(Some(number), self.format.w() / 2);
-        output_hex(f, &rb)
-    }
-
-    fn date(&self, f: &mut Formatter<'_>, number: f64) -> FmtResult {
-        const MINUTE: f64 = 60.0;
-        const HOUR: f64 = 60.0 * 60.0;
-        const DAY: f64 = 60.0 * 60.0 * 24.0;
-
-        let (date, mut time) = match self.format.type_.category() {
-            Category::Date => {
-                if number < 0.0 {
-                    return self.missing(f);
-                }
-                let Some(date) = calendar_offset_to_gregorian(number / DAY) else {
-                    return self.missing(f);
-                };
-                (date, number % DAY)
-            }
-            Category::Time => (NaiveDate::MIN, number),
-            _ => unreachable!(),
-        };
-
-        let mut output = SmallString::<[u8; 40]>::new();
-        for TemplateItem { c, n } in DateTemplate::for_format(self.format).unwrap() {
-            match c {
-                'd' if n < 3 => write!(&mut output, "{:02}", date.day()).unwrap(),
-                'd' => write!(&mut output, "{:03}", day_of_year(date).unwrap_or(1)).unwrap(),
-                'm' if n < 3 => write!(&mut output, "{:02}", date.month()).unwrap(),
-                'm' => write!(&mut output, "{}", short_month_name(date.month()).unwrap()).unwrap(),
-                'y' if n >= 4 => {
-                    let year = date.year();
-                    if year <= 9999 {
-                        write!(&mut output, "{year:04}").unwrap();
-                    } else if self.format.type_ == Type::DateTime
-                        || self.format.type_ == Type::YmdHms
-                    {
-                        write!(&mut output, "****").unwrap();
-                    } else {
-                        return self.overflow(f);
-                    }
-                }
-                'y' => {
-                    let epoch = self.settings.epoch.0;
-                    let offset = date.year() - epoch;
-                    if !(0..=99).contains(&offset) {
-                        return self.overflow(f);
-                    }
-                    write!(&mut output, "{:02}", date.year().abs() % 100).unwrap();
-                }
-                'q' => write!(&mut output, "{}", date.month0() / 3 + 1).unwrap(),
-                'w' => write!(
-                    &mut output,
-                    "{:2}",
-                    (day_of_year(date).unwrap_or(1) - 1) / 7 + 1
-                )
-                .unwrap(),
-                'D' => {
-                    if time < 0.0 {
-                        output.push('-');
-                    }
-                    time = time.abs();
-                    write!(&mut output, "{:1$.0}", (time / DAY).floor(), n).unwrap();
-                    time %= DAY;
-                }
-                'H' => {
-                    if time < 0.0 {
-                        output.push('-');
-                    }
-                    time = time.abs();
-                    write!(&mut output, "{:01$.0}", (time / HOUR).floor(), n).unwrap();
-                    time %= HOUR;
-                }
-                'M' => {
-                    if time < 0.0 {
-                        output.push('-');
-                    }
-                    time = time.abs();
-                    write!(&mut output, "{:02.0}", (time / MINUTE).floor()).unwrap();
-                    time %= MINUTE;
-
-                    let excess_width = self.format.w() as isize - output.len() as isize;
-                    if excess_width < 0 || (self.format.type_ == Type::MTime && excess_width < 3) {
-                        return self.overflow(f);
-                    }
-                    if excess_width == 3
-                        || excess_width == 4
-                        || (excess_width >= 5 && self.format.d == 0)
-                    {
-                        write!(&mut output, ":{:02.0}", time.floor()).unwrap();
-                    } else if excess_width >= 5 {
-                        let d = min(self.format.d(), excess_width as usize - 4);
-                        let w = d + 3;
-                        write!(&mut output, ":{:02$.*}", d, time, w).unwrap();
-                        if self.settings.decimal == Decimal::Comma {
-                            fix_decimal_point(&mut output);
-                        }
-                    }
-                    break;
-                }
-                c if n == 1 => output.push(c),
-                _ => unreachable!(),
-            }
-        }
-        write!(f, "{:>1$}", &output, self.format.w())
-    }
-
-    fn month(&self, f: &mut Formatter<'_>, number: f64) -> FmtResult {
-        if let Some(month) = month_name(number as u32) {
-            write!(f, "{month:.*}", self.format.w())
-        } else {
-            self.missing(f)
-        }
-    }
-
-    /// Writes this object to `w`. Writes binary formats ([Type::P],
-    /// [Type::PIB], and so on) as binary values, and writes other output
-    /// formats in the given `encoding`.
-    ///
-    /// If `dv` is a [DisplayValue], the difference between `write!(f, "{}",
-    /// dv)` and `dv.write(f, encoding)` is:
-    ///
-    /// * `write!` always outputs UTF-8. Binary formats are encoded as the
-    ///   Unicode characters corresponding to their bytes.
-    ///
-    /// * `dv.write` outputs the desired `encoding`. Binary formats are not
-    ///   encoded in `encoding` (and thus they might be invalid for the
-    ///   encoding).
-    pub fn write<W>(&self, mut w: W, encoding: &'static Encoding) -> Result<(), IoError>
-    where
-        W: IoWrite,
-    {
-        match self.to_binary() {
-            Some(binary) => w.write_all(&binary),
-            None if encoding == UTF_8 => {
-                write!(&mut w, "{}", self)
-            }
-            None => {
-                let mut temp = SmallString::<[u8; 64]>::new();
-                write!(&mut temp, "{}", self).unwrap();
-                w.write_all(&encoding.encode(&temp).0)
-            }
-        }
-    }
-
-    fn to_binary(&self) -> Option<SmallVec<[u8; 16]>> {
-        let number = self.value.as_number()?;
-        match self.format.type_() {
-            Type::P => Some(self.p(number)),
-            Type::PK => Some(self.pk(number)),
-            Type::IB => Some(self.ib(number)),
-            Type::PIB => Some(self.pib(number)),
-            Type::RB => Some(self.rb(number, self.format.w())),
-            _ => None,
-        }
-    }
-
-    fn bcd(&self, number: Option<f64>, digits: usize) -> (bool, SmallVec<[u8; 16]>) {
-        let legacy = LegacyFormat::new(number.unwrap_or_default(), self.format.d());
-        let len = legacy.len();
-
-        let mut output = SmallVec::new();
-        if len > digits {
-            output.resize(digits.div_ceil(2), 0);
-            (false, output)
-        } else {
-            let mut decimal = SmallString::<[u8; 16]>::new();
-            write!(
-                &mut decimal,
-                "{}{legacy}",
-                Zeros(digits.saturating_sub(len))
-            )
-            .unwrap();
-
-            let mut src = decimal.bytes();
-            for _ in 0..digits / 2 {
-                let d0 = src.next().unwrap() - b'0';
-                let d1 = src.next().unwrap() - b'0';
-                output.push((d0 << 4) + d1);
-            }
-            if digits % 2 != 0 {
-                let d = src.next().unwrap() - b'0';
-                output.push(d << 4);
-            }
-            (true, output)
-        }
-    }
-
-    fn p(&self, number: Option<f64>) -> SmallVec<[u8; 16]> {
-        let (valid, mut output) = self.bcd(number, self.format.w() * 2 - 1);
-        if valid && number.is_some_and(|number| number < 0.0) {
-            *output.last_mut().unwrap() |= 0xd;
-        } else {
-            *output.last_mut().unwrap() |= 0xf;
-        }
-        output
-    }
-
-    fn pk(&self, number: Option<f64>) -> SmallVec<[u8; 16]> {
-        let number = match number {
-            Some(number) if number < 0.0 => None,
-            other => other,
-        };
-        let (_valid, output) = self.bcd(number, self.format.w() * 2);
-        output
-    }
-
-    fn ib(&self, number: Option<f64>) -> SmallVec<[u8; 16]> {
-        let number = number.map_or(0.0, |number| (number * power10(self.format.d())).round());
-        let number = if number >= power256(self.format.w) / 2.0 - 1.0
-            || number < -power256(self.format.w) / 2.0
-        {
-            0.0
-        } else {
-            number
-        };
-        let integer = number.abs() as u64;
-        let integer = if number < 0.0 {
-            (-(integer as i64)) as u64
-        } else {
-            integer
-        };
-        self.endian.output.to_smallvec(integer, self.format.w())
-    }
-
-    fn pib(&self, number: Option<f64>) -> SmallVec<[u8; 16]> {
-        let number = number.map_or(0.0, |number| (number * power10(self.format.d())).round());
-        let number = if number >= power256(self.format.w) || number < 0.0 {
-            0.0
-        } else {
-            number
-        };
-        let integer = number.abs() as u64;
-        self.endian.output.to_smallvec(integer, self.format.w())
-    }
-
-    fn rb(&self, number: Option<f64>, w: usize) -> SmallVec<[u8; 16]> {
-        let number = number.unwrap_or(-f64::MAX);
-        let bytes: [u8; 8] = self.endian.output.to_bytes(number);
-        let mut vec = SmallVec::new();
-        vec.extend_from_slice(&bytes);
-        vec.resize(w, 0);
-        vec
-    }
-}
-
-struct LegacyFormat {
-    s: SmallVec<[u8; 40]>,
-    trailing_zeros: usize,
-}
-
-impl LegacyFormat {
-    fn new(number: f64, d: usize) -> Self {
-        let mut s = SmallVec::<[u8; 40]>::new();
-        write!(&mut s, "{:E}", number.abs()).unwrap();
-        debug_assert!(s.is_ascii());
-
-        // Parse exponent.
-        //
-        // Add 1 because of the transformation we will do just below, and `d` so
-        // that we just need to round to the nearest integer.
-        let e_index = s.iter().position(|c| *c == b'E').unwrap();
-        let mut exponent = unsafe { from_utf8_unchecked(&s[e_index + 1..]) }
-            .parse::<i32>()
-            .unwrap()
-            + 1
-            + d as i32;
-
-        // Transform `1.234E56` into `1234`.
-        if e_index == 1 {
-            // No decimals, e.g. `1E4` or `0E0`.
-            s.truncate(1)
-        } else {
-            s.remove(1);
-            s.truncate(e_index - 1);
-        };
-        debug_assert!(s.iter().all(|c| c.is_ascii_digit()));
-
-        if exponent >= 0 && exponent < s.len() as i32 {
-            // The first `exponent` digits are before the decimal point.  We
-            // need to round off there.
-            let exp = exponent as usize;
-
-            fn round_up(digits: &mut [u8], position: usize) -> bool {
-                for index in (0..position).rev() {
-                    match digits[index] {
-                        b'0'..=b'8' => {
-                            digits[index] += 1;
-                            return true;
-                        }
-                        b'9' => {
-                            digits[index] = b'0';
-                        }
-                        _ => unreachable!(),
-                    }
-                }
-                false
-            }
-
-            if s[exp] >= b'5' && !round_up(&mut s, exp) {
-                s.clear();
-                s.push(b'1');
-                exponent += 1;
-            }
-        }
-
-        let exponent = exponent.max(0) as usize;
-        s.truncate(exponent);
-        s.resize(exponent, b'0');
-        let trailing_zeros = exponent.saturating_sub(s.len());
-        Self { s, trailing_zeros }
-    }
-    fn s(&self) -> &str {
-        unsafe { from_utf8_unchecked(&self.s) }
-    }
-    fn len(&self) -> usize {
-        self.s.len() + self.trailing_zeros
-    }
-}
-
-impl Display for LegacyFormat {
-    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
-        write!(f, "{}{}", self.s(), Zeros(self.trailing_zeros))
-    }
-}
-
-struct Zeros(usize);
-
-impl Display for Zeros {
-    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
-        let mut n = self.0;
-        while n > 0 {
-            static ZEROS: &str = "0000000000000000000000000000000000000000";
-            let chunk = n.min(ZEROS.len());
-            f.write_str(&ZEROS[..chunk])?;
-            n -= chunk;
-        }
-        Ok(())
-    }
-}
-
-fn integer_to_binary(number: u64, width: u16) -> SmallVec<[u8; 8]> {
-    let bytes = (number << ((8 - width) * 8)).to_be_bytes();
-    SmallVec::from_slice(&bytes[..width as usize])
-}
-
-fn output_hex(f: &mut Formatter<'_>, bytes: &[u8]) -> FmtResult {
-    for byte in bytes {
-        write!(f, "{byte:02X}")?;
-    }
-    Ok(())
-}
-
-fn allocate_space(want: usize, capacity: usize, used: &mut usize) -> bool {
-    if *used + want <= capacity {
-        *used += want;
-        true
-    } else {
-        false
-    }
-}
-
-/// A representation of a number that can be quickly rounded to any desired
-/// number of decimal places (up to a specified maximum).
-#[derive(Debug)]
-struct Rounder {
-    /// Magnitude of number with excess precision.
-    string: SmallString<[u8; 40]>,
-
-    /// Number of digits before decimal point.
-    integer_digits: usize,
-
-    /// Number of `9`s or `.`s at start of string.
-    leading_nines: usize,
-
-    /// Number of `0`s or `.`s at start of string.
-    leading_zeros: usize,
-
-    /// Is the number negative?
-    negative: bool,
-}
-
-impl Rounder {
-    fn new(style: &NumberStyle, number: f64, max_decimals: u8) -> Self {
-        debug_assert!(number.abs() < 1e41);
-        debug_assert!((0..=16).contains(&max_decimals));
-
-        let mut string = SmallString::new();
-        if max_decimals == 0 {
-            // Fast path.  No rounding needed.
-            //
-            // We append `.00` to the integer representation because
-            // [Self::round_up] assumes that fractional digits are present.
-            write!(&mut string, "{:.0}.00", number.round().abs()).unwrap()
-        } else {
-            // Slow path.
-            //
-            // This is more difficult than it really should be because we have
-            // to make sure that numbers that are exactly halfway between two
-            // representations are always rounded away from zero.  This is not
-            // what format! normally does (usually it rounds to even), so we
-            // have to fake it as best we can, by formatting with extra
-            // precision and then doing the rounding ourselves.
-            //
-            // We take up to two rounds to format numbers.  In the first round,
-            // we obtain 2 digits of precision beyond those requested by the
-            // user.  If those digits are exactly "50", then in a second round
-            // we format with as many digits as are significant in a "double".
-            //
-            // It might be better to directly implement our own floating-point
-            // formatting routine instead of relying on the system's sprintf
-            // implementation.  But the classic Steele and White paper on
-            // printing floating-point numbers does not hint how to do what we
-            // want, and it's not obvious how to change their algorithms to do
-            // so.  It would also be a lot of work.
-            write!(
-                &mut string,
-                "{:.*}",
-                max_decimals as usize + 2,
-                number.abs()
-            )
-            .unwrap();
-            if string.ends_with("50") {
-                let (_sig, binary_exponent) = frexp(number);
-                let decimal_exponent = binary_exponent * 3 / 10;
-                let format_decimals = (f64::DIGITS as i32 + 1) - decimal_exponent;
-                if format_decimals > max_decimals as i32 + 2 {
-                    string.clear();
-                    write!(&mut string, "{:.*}", format_decimals as usize, number.abs()).unwrap();
-                }
-            }
-        };
-
-        if !style.leading_zero && string.starts_with("0") {
-            string.remove(0);
-        }
-        let leading_zeros = string
-            .bytes()
-            .take_while(|c| *c == b'0' || *c == b'.')
-            .count();
-        let leading_nines = string
-            .bytes()
-            .take_while(|c| *c == b'9' || *c == b'.')
-            .count();
-        let integer_digits = string.bytes().take_while(u8::is_ascii_digit).count();
-        let negative = number.is_sign_negative();
-        Self {
-            string,
-            integer_digits,
-            leading_nines,
-            leading_zeros,
-            negative,
-        }
-    }
-
-    /// Returns a [RounderWdith] for formatting the magnitude to `decimals`
-    /// decimal places. `decimals` must be in `0..=16`.
-    fn width(&self, decimals: usize) -> RounderWidth {
-        // Calculate base measures.
-        let mut width = self.integer_digits;
-        if decimals > 0 {
-            width += decimals + 1;
-        }
-        let mut integer_digits = self.integer_digits;
-        let mut negative = self.negative;
-
-        // Rounding can cause adjustments.
-        if self.should_round_up(decimals) {
-            // Rounding up leading `9s` adds a new digit (a `1`).
-            if self.leading_nines >= width {
-                width += 1;
-                integer_digits += 1;
-            }
-        } else {
-            // Rounding down.
-            if self.leading_zeros >= width {
-                // All digits that remain after rounding are zeros.  Therefore
-                // we drop the negative sign.
-                negative = false;
-                if self.integer_digits == 0 && decimals == 0 {
-                    // No digits at all are left.  We need to display
-                    // at least a single digit (a zero).
-                    debug_assert_eq!(width, 0);
-                    width += 1;
-                    integer_digits = 1;
-                }
-            }
-        }
-        RounderWidth {
-            width,
-            integer_digits,
-            negative,
-        }
-    }
-
-    /// Returns true if the number should be rounded up when chopped off at
-    /// `decimals` decimal places, false if it should be rounded down.
-    fn should_round_up(&self, decimals: usize) -> bool {
-        let digit = self.string.as_bytes()[self.integer_digits + decimals + 1];
-        debug_assert!(digit.is_ascii_digit());
-        digit >= b'5'
-    }
-
-    /// Formats the number, rounding to `decimals` decimal places.  Exactly as
-    /// many characters as indicated by [Self::width(decimals)] are written.
-    fn format(&self, decimals: usize) -> SmallString<[u8; 40]> {
-        let mut output = SmallString::new();
-        let mut base_width = self.integer_digits;
-        if decimals > 0 {
-            base_width += decimals + 1;
-        }
-
-        if self.should_round_up(decimals) {
-            if self.leading_nines < base_width {
-                // Rounding up.  This is the common case where rounding up
-                // doesn't add an extra digit.
-                output.push_str(&self.string[..base_width]);
-
-                // SAFETY: This loop only changes ASCII characters to other
-                // ASCII characters.
-                unsafe {
-                    for c in output.as_bytes_mut().iter_mut().rev() {
-                        match *c {
-                            b'9' => *c = b'0',
-                            b'0'..=b'8' => {
-                                *c += 1;
-                                break;
-                            }
-                            b'.' => (),
-                            _ => unreachable!(),
-                        }
-                    }
-                }
-            } else {
-                // Rounding up leading 9s causes the result to be a 1 followed
-                // by a number of 0s, plus a decimal point.
-                output.push('1');
-                for _ in 0..self.integer_digits {
-                    output.push('0');
-                }
-                if decimals > 0 {
-                    output.push('.');
-                    for _ in 0..decimals {
-                        output.push('0');
-                    }
-                }
-                debug_assert_eq!(output.len(), base_width + 1);
-            }
-        } else {
-            // Rounding down.
-            if self.integer_digits != 0 || decimals != 0 {
-                // Common case: just copy the digits.
-                output.push_str(&self.string);
-            } else {
-                // No digits remain.  The output is just a zero.
-                output.push('0');
-            }
-        }
-        output
-    }
-}
-
-struct RounderWidth {
-    /// Number of characters required to format the number to a specified number
-    /// of decimal places.  This includes integer digits and a decimal point and
-    /// fractional digits, if any, but it does not include any negative prefix
-    /// or suffix or other affixes.
-    width: usize,
-
-    /// Number of digits before the decimal point, between 0 and 40.
-    integer_digits: usize,
-
-    /// True if the number is negative and its rounded representation would
-    /// include at least one nonzero digit.
-    negative: bool,
-}
-
-/// Returns `10^x`.
-fn power10(x: usize) -> f64 {
-    const POWERS: [f64; 41] = [
-        1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16,
-        1e17, 1e18, 1e19, 1e20, 1e21, 1e22, 1e23, 1e24, 1e25, 1e26, 1e27, 1e28, 1e29, 1e30, 1e31,
-        1e32, 1e33, 1e34, 1e35, 1e36, 1e37, 1e38, 1e39, 1e40,
-    ];
-    POWERS
-        .get(x)
-        .copied()
-        .unwrap_or_else(|| 10.0_f64.powi(x as i32))
-}
-
-/// Returns `256^x`.
-fn power256(x: u16) -> f64 {
-    const POWERS: [f64; 9] = [
-        1.0,
-        256.0,
-        65536.0,
-        16777216.0,
-        4294967296.0,
-        1099511627776.0,
-        281474976710656.0,
-        72057594037927936.0,
-        18446744073709551616.0,
-    ];
-    POWERS
-        .get(x as usize)
-        .copied()
-        .unwrap_or_else(|| 256.0_f64.powi(x as i32))
-}
-
-fn fix_decimal_point<A>(s: &mut SmallString<A>)
-where
-    A: Array<Item = u8>,
-{
-    // SAFETY: This only changes only one ASCII character (`.`) to
-    // another ASCII character (`,`).
-    unsafe {
-        if let Some(dot) = s.as_bytes_mut().iter_mut().find(|c| **c == b'.') {
-            *dot = b',';
-        }
-    }
-}
-
-#[cfg(test)]
-mod test {
-    use std::{fmt::Write, fs::File, io::BufRead, path::Path};
-
-    use binrw::io::BufReader;
-    use encoding_rs::UTF_8;
-    use itertools::Itertools;
-    use smallstr::SmallString;
-    use smallvec::SmallVec;
-
-    use crate::{
-        dictionary::Value,
-        endian::Endian,
-        format::{AbstractFormat, Epoch, Format, Settings, Type, UncheckedFormat, CC},
-        lex::{
-            scan::StringScanner,
-            segment::Syntax,
-            token::{Punct, Token},
-        },
-        settings::EndianSettings,
-    };
-
-    fn test(name: &str) {
-        let filename = Path::new(env!("CARGO_MANIFEST_DIR"))
-            .join("src/format/testdata/display")
-            .join(name);
-        let input = BufReader::new(File::open(&filename).unwrap());
-        let settings = Settings::default()
-            .with_cc(CC::A, ",,,".parse().unwrap())
-            .with_cc(CC::B, "-,[[[,]]],-".parse().unwrap())
-            .with_cc(CC::C, "((,[,],))".parse().unwrap())
-            .with_cc(CC::D, ",XXX,,-".parse().unwrap())
-            .with_cc(CC::E, ",,YYY,-".parse().unwrap());
-        let endian = EndianSettings::new(Endian::Big);
-        let mut value = Some(0.0);
-        let mut value_name = String::new();
-        for (line, line_number) in input.lines().map(|r| r.unwrap()).zip(1..) {
-            let line = line.trim();
-            let tokens = StringScanner::new(line, Syntax::Interactive, true)
-                .unwrapped()
-                .collect::<Vec<_>>();
-            match &tokens[0] {
-                Token::Number(number) => {
-                    value = if let Some(Token::Punct(Punct::Exp)) = tokens.get(1) {
-                        assert_eq!(tokens.len(), 3);
-                        let exponent = tokens[2].as_number().unwrap();
-                        Some(number.powf(exponent))
-                    } else {
-                        assert_eq!(tokens.len(), 1);
-                        Some(*number)
-                    };
-                    value_name = String::from(line);
-                }
-                Token::End => {
-                    value = None;
-                    value_name = String::from(line);
-                }
-                Token::Id(id) => {
-                    let format: UncheckedFormat =
-                        id.0.as_str()
-                            .parse::<AbstractFormat>()
-                            .unwrap()
-                            .try_into()
-                            .unwrap();
-                    let format: Format = format.try_into().unwrap();
-                    assert_eq!(tokens.get(1), Some(&Token::Punct(Punct::Colon)));
-                    let expected = tokens[2].as_string().unwrap();
-                    let actual = Value::Number(value)
-                        .display(format, UTF_8)
-                        .with_settings(&settings)
-                        .with_endian(endian)
-                        .to_string();
-                    assert_eq!(
-                        expected,
-                        &actual,
-                        "{}:{line_number}: Error formatting {value_name} as {format}",
-                        filename.display()
-                    );
-                }
-                _ => panic!(),
-            }
-        }
-    }
-
-    #[test]
-    fn comma() {
-        test("comma.txt");
-    }
-
-    #[test]
-    fn dot() {
-        test("dot.txt");
-    }
-
-    #[test]
-    fn dollar() {
-        test("dollar.txt");
-    }
-
-    #[test]
-    fn pct() {
-        test("pct.txt");
-    }
-
-    #[test]
-    fn e() {
-        test("e.txt");
-    }
-
-    #[test]
-    fn f() {
-        test("f.txt");
-    }
-
-    #[test]
-    fn n() {
-        test("n.txt");
-    }
-
-    #[test]
-    fn z() {
-        test("z.txt");
-    }
-
-    #[test]
-    fn cca() {
-        test("cca.txt");
-    }
-
-    #[test]
-    fn ccb() {
-        test("ccb.txt");
-    }
-
-    #[test]
-    fn ccc() {
-        test("ccc.txt");
-    }
-
-    #[test]
-    fn ccd() {
-        test("ccd.txt");
-    }
-
-    #[test]
-    fn cce() {
-        test("cce.txt");
-    }
-
-    #[test]
-    fn pibhex() {
-        test("pibhex.txt");
-    }
-
-    #[test]
-    fn rbhex() {
-        test("rbhex.txt");
-    }
-
-    #[test]
-    fn leading_zeros() {
-        struct Test {
-            with_leading_zero: Settings,
-            without_leading_zero: Settings,
-        }
-
-        impl Test {
-            fn new() -> Self {
-                Self {
-                    without_leading_zero: Settings::default(),
-                    with_leading_zero: Settings::default().with_leading_zero(true),
-                }
-            }
-
-            fn test_with_settings(value: f64, expected: [&str; 2], settings: &Settings) {
-                let value = Value::from(value);
-                for (expected, d) in expected.into_iter().zip([2, 1].into_iter()) {
-                    assert_eq!(
-                        &value
-                            .display(Format::new(Type::F, 5, d).unwrap(), UTF_8)
-                            .with_settings(settings)
-                            .to_string(),
-                        expected
-                    );
-                }
-            }
-            fn test(&self, value: f64, without: [&str; 2], with: [&str; 2]) {
-                Self::test_with_settings(value, without, &self.without_leading_zero);
-                Self::test_with_settings(value, with, &self.with_leading_zero);
-            }
-        }
-        let test = Test::new();
-        test.test(0.5, ["  .50", "   .5"], [" 0.50", "  0.5"]);
-        test.test(0.99, ["  .99", "  1.0"], [" 0.99", "  1.0"]);
-        test.test(0.01, ["  .01", "   .0"], [" 0.01", "  0.0"]);
-        test.test(0.0, ["  .00", "   .0"], [" 0.00", "  0.0"]);
-        test.test(-0.0, ["  .00", "   .0"], [" 0.00", "  0.0"]);
-        test.test(-0.5, [" -.50", "  -.5"], ["-0.50", " -0.5"]);
-        test.test(-0.99, [" -.99", " -1.0"], ["-0.99", " -1.0"]);
-        test.test(-0.01, [" -.01", "   .0"], ["-0.01", "  0.0"]);
-    }
-
-    #[test]
-    fn non_ascii_cc() {
-        fn test(settings: &Settings, value: f64, expected: &str) {
-            assert_eq!(
-                &Value::from(value)
-                    .display(Format::new(Type::CC(CC::A), 10, 2).unwrap(), UTF_8)
-                    .with_settings(settings)
-                    .to_string(),
-                expected
-            );
-        }
-
-        let settings = Settings::default().with_cc(CC::A, "«,¥,€,»".parse().unwrap());
-        test(&settings, 1.0, "   ¥1.00€ ");
-        test(&settings, -1.0, "  «¥1.00€»");
-        test(&settings, 1.5, "   ¥1.50€ ");
-        test(&settings, -1.5, "  «¥1.50€»");
-        test(&settings, 0.75, "    ¥.75€ ");
-        test(&settings, 1.5e10, " ¥2E+010€ ");
-        test(&settings, -1.5e10, "«¥2E+010€»");
-    }
-
-    fn test_binhex(name: &str) {
-        let filename = Path::new(env!("CARGO_MANIFEST_DIR"))
-            .join("src/format/testdata/display")
-            .join(name);
-        let input = BufReader::new(File::open(&filename).unwrap());
-        let mut value = None;
-        let mut value_name = String::new();
-
-        let endian = EndianSettings::new(Endian::Big);
-        for (line, line_number) in input.lines().map(|r| r.unwrap()).zip(1..) {
-            let line = line.trim();
-            let tokens = StringScanner::new(line, Syntax::Interactive, true)
-                .unwrapped()
-                .collect::<Vec<_>>();
-            match &tokens[0] {
-                Token::Number(number) => {
-                    value = Some(*number);
-                    value_name = String::from(line);
-                }
-                Token::End => {
-                    value = None;
-                    value_name = String::from(line);
-                }
-                Token::Id(id) => {
-                    let format: UncheckedFormat =
-                        id.0.as_str()
-                            .parse::<AbstractFormat>()
-                            .unwrap()
-                            .try_into()
-                            .unwrap();
-                    let format: Format = format.try_into().unwrap();
-                    assert_eq!(tokens.get(1), Some(&Token::Punct(Punct::Colon)));
-                    let expected = tokens[2].as_string().unwrap();
-                    let mut actual = SmallVec::<[u8; 16]>::new();
-                    Value::Number(value)
-                        .display(format, UTF_8)
-                        .with_endian(endian)
-                        .write(&mut actual, UTF_8)
-                        .unwrap();
-                    let mut actual_s = SmallString::<[u8; 32]>::new();
-                    for b in actual {
-                        write!(&mut actual_s, "{:02x}", b).unwrap();
-                    }
-                    assert_eq!(
-                        expected,
-                        &*actual_s,
-                        "{}:{line_number}: Error formatting {value_name} as {format}",
-                        filename.display()
-                    );
-                }
-                _ => panic!(),
-            }
-        }
-    }
-
-    #[test]
-    fn p() {
-        test_binhex("p.txt");
-    }
-
-    #[test]
-    fn pk() {
-        test_binhex("pk.txt");
-    }
-
-    #[test]
-    fn ib() {
-        test_binhex("ib.txt");
-    }
-
-    #[test]
-    fn pib() {
-        test_binhex("pib.txt");
-    }
-
-    #[test]
-    fn rb() {
-        test_binhex("rb.txt");
-    }
-
-    fn test_dates(format: Format, expect: &[&str]) {
-        let settings = Settings::default().with_epoch(Epoch(1930));
-        let parser = Type::DateTime.parser(UTF_8).with_settings(&settings);
-        static INPUTS: &[&str; 20] = &[
-            "10-6-1648 0:0:0",
-            "30-6-1680 4:50:38.12301",
-            "24-7-1716 12:31:35.23453",
-            "19-6-1768 12:47:53.34505",
-            "2-8-1819 1:26:0.45615",
-            "27-3-1839 20:58:11.56677",
-            "19-4-1903 7:36:5.18964",
-            "25-8-1929 15:43:49.83132",
-            "29-9-1941 4:25:9.01293",
-            "19-4-1943 6:49:27.52375",
-            "7-10-1943 2:57:52.01565",
-            "17-3-1992 16:45:44.86529",
-            "25-2-1996 21:30:57.82047",
-            "29-9-41 4:25:9.15395",
-            "19-4-43 6:49:27.10533",
-            "7-10-43 2:57:52.48229",
-            "17-3-92 16:45:44.65827",
-            "25-2-96 21:30:57.58219",
-            "10-11-2038 22:30:4.18347",
-            "18-7-2094 1:56:51.59319",
-        ];
-        assert_eq!(expect.len(), INPUTS.len());
-        for (input, expect) in INPUTS.iter().copied().zip_eq(expect.iter().copied()) {
-            let value = parser.parse(input).unwrap();
-            let formatted = value
-                .display(format, UTF_8)
-                .with_settings(&settings)
-                .to_string();
-            assert_eq!(&formatted, expect);
-        }
-    }
-
-    #[test]
-    fn date9() {
-        test_dates(
-            Format::new(Type::Date, 9, 0).unwrap(),
-            &[
-                "*********",
-                "*********",
-                "*********",
-                "*********",
-                "*********",
-                "*********",
-                "*********",
-                "*********",
-                "29-SEP-41",
-                "19-APR-43",
-                "07-OCT-43",
-                "17-MAR-92",
-                "25-FEB-96",
-                "29-SEP-41",
-                "19-APR-43",
-                "07-OCT-43",
-                "17-MAR-92",
-                "25-FEB-96",
-                "*********",
-                "*********",
-            ],
-        );
-    }
-
-    #[test]
-    fn date11() {
-        test_dates(
-            Format::new(Type::Date, 11, 0).unwrap(),
-            &[
-                "10-JUN-1648",
-                "30-JUN-1680",
-                "24-JUL-1716",
-                "19-JUN-1768",
-                "02-AUG-1819",
-                "27-MAR-1839",
-                "19-APR-1903",
-                "25-AUG-1929",
-                "29-SEP-1941",
-                "19-APR-1943",
-                "07-OCT-1943",
-                "17-MAR-1992",
-                "25-FEB-1996",
-                "29-SEP-1941",
-                "19-APR-1943",
-                "07-OCT-1943",
-                "17-MAR-1992",
-                "25-FEB-1996",
-                "10-NOV-2038",
-                "18-JUL-2094",
-            ],
-        );
-    }
-
-    #[test]
-    fn adate8() {
-        test_dates(
-            Format::new(Type::ADate, 8, 0).unwrap(),
-            &[
-                "********", "********", "********", "********", "********", "********", "********",
-                "********", "09/29/41", "04/19/43", "10/07/43", "03/17/92", "02/25/96", "09/29/41",
-                "04/19/43", "10/07/43", "03/17/92", "02/25/96", "********", "********",
-            ],
-        );
-    }
-
-    #[test]
-    fn adate10() {
-        test_dates(
-            Format::new(Type::ADate, 10, 0).unwrap(),
-            &[
-                "06/10/1648",
-                "06/30/1680",
-                "07/24/1716",
-                "06/19/1768",
-                "08/02/1819",
-                "03/27/1839",
-                "04/19/1903",
-                "08/25/1929",
-                "09/29/1941",
-                "04/19/1943",
-                "10/07/1943",
-                "03/17/1992",
-                "02/25/1996",
-                "09/29/1941",
-                "04/19/1943",
-                "10/07/1943",
-                "03/17/1992",
-                "02/25/1996",
-                "11/10/2038",
-                "07/18/2094",
-            ],
-        );
-    }
-
-    #[test]
-    fn edate8() {
-        test_dates(
-            Format::new(Type::EDate, 8, 0).unwrap(),
-            &[
-                "********", "********", "********", "********", "********", "********", "********",
-                "********", "29.09.41", "19.04.43", "07.10.43", "17.03.92", "25.02.96", "29.09.41",
-                "19.04.43", "07.10.43", "17.03.92", "25.02.96", "********", "********",
-            ],
-        );
-    }
-
-    #[test]
-    fn edate10() {
-        test_dates(
-            Format::new(Type::EDate, 10, 0).unwrap(),
-            &[
-                "10.06.1648",
-                "30.06.1680",
-                "24.07.1716",
-                "19.06.1768",
-                "02.08.1819",
-                "27.03.1839",
-                "19.04.1903",
-                "25.08.1929",
-                "29.09.1941",
-                "19.04.1943",
-                "07.10.1943",
-                "17.03.1992",
-                "25.02.1996",
-                "29.09.1941",
-                "19.04.1943",
-                "07.10.1943",
-                "17.03.1992",
-                "25.02.1996",
-                "10.11.2038",
-                "18.07.2094",
-            ],
-        );
-    }
-
-    #[test]
-    fn jdate5() {
-        test_dates(
-            Format::new(Type::JDate, 5, 0).unwrap(),
-            &[
-                "*****", "*****", "*****", "*****", "*****", "*****", "*****", "*****", "41272",
-                "43109", "43280", "92077", "96056", "41272", "43109", "43280", "92077", "96056",
-                "*****", "*****",
-            ],
-        );
-    }
-
-    #[test]
-    fn jdate7() {
-        test_dates(
-            Format::new(Type::JDate, 7, 0).unwrap(),
-            &[
-                "1648162", "1680182", "1716206", "1768171", "1819214", "1839086", "1903109",
-                "1929237", "1941272", "1943109", "1943280", "1992077", "1996056", "1941272",
-                "1943109", "1943280", "1992077", "1996056", "2038314", "2094199",
-            ],
-        );
-    }
-
-    #[test]
-    fn sdate8() {
-        test_dates(
-            Format::new(Type::SDate, 8, 0).unwrap(),
-            &[
-                "********", "********", "********", "********", "********", "********", "********",
-                "********", "41/09/29", "43/04/19", "43/10/07", "92/03/17", "96/02/25", "41/09/29",
-                "43/04/19", "43/10/07", "92/03/17", "96/02/25", "********", "********",
-            ],
-        );
-    }
-
-    #[test]
-    fn sdate10() {
-        test_dates(
-            Format::new(Type::SDate, 10, 0).unwrap(),
-            &[
-                "1648/06/10",
-                "1680/06/30",
-                "1716/07/24",
-                "1768/06/19",
-                "1819/08/02",
-                "1839/03/27",
-                "1903/04/19",
-                "1929/08/25",
-                "1941/09/29",
-                "1943/04/19",
-                "1943/10/07",
-                "1992/03/17",
-                "1996/02/25",
-                "1941/09/29",
-                "1943/04/19",
-                "1943/10/07",
-                "1992/03/17",
-                "1996/02/25",
-                "2038/11/10",
-                "2094/07/18",
-            ],
-        );
-    }
-
-    #[test]
-    fn qyr6() {
-        test_dates(
-            Format::new(Type::QYr, 6, 0).unwrap(),
-            &[
-                "******", "******", "******", "******", "******", "******", "******", "******",
-                "3 Q 41", "2 Q 43", "4 Q 43", "1 Q 92", "1 Q 96", "3 Q 41", "2 Q 43", "4 Q 43",
-                "1 Q 92", "1 Q 96", "******", "******",
-            ],
-        );
-    }
-
-    #[test]
-    fn qyr8() {
-        test_dates(
-            Format::new(Type::QYr, 8, 0).unwrap(),
-            &[
-                "2 Q 1648", "2 Q 1680", "3 Q 1716", "2 Q 1768", "3 Q 1819", "1 Q 1839", "2 Q 1903",
-                "3 Q 1929", "3 Q 1941", "2 Q 1943", "4 Q 1943", "1 Q 1992", "1 Q 1996", "3 Q 1941",
-                "2 Q 1943", "4 Q 1943", "1 Q 1992", "1 Q 1996", "4 Q 2038", "3 Q 2094",
-            ],
-        );
-    }
-
-    #[test]
-    fn moyr6() {
-        test_dates(
-            Format::new(Type::MoYr, 6, 0).unwrap(),
-            &[
-                "******", "******", "******", "******", "******", "******", "******", "******",
-                "SEP 41", "APR 43", "OCT 43", "MAR 92", "FEB 96", "SEP 41", "APR 43", "OCT 43",
-                "MAR 92", "FEB 96", "******", "******",
-            ],
-        );
-    }
-
-    #[test]
-    fn moyr8() {
-        test_dates(
-            Format::new(Type::MoYr, 8, 0).unwrap(),
-            &[
-                "JUN 1648", "JUN 1680", "JUL 1716", "JUN 1768", "AUG 1819", "MAR 1839", "APR 1903",
-                "AUG 1929", "SEP 1941", "APR 1943", "OCT 1943", "MAR 1992", "FEB 1996", "SEP 1941",
-                "APR 1943", "OCT 1943", "MAR 1992", "FEB 1996", "NOV 2038", "JUL 2094",
-            ],
-        );
-    }
-
-    #[test]
-    fn wkyr8() {
-        test_dates(
-            Format::new(Type::WkYr, 8, 0).unwrap(),
-            &[
-                "********", "********", "********", "********", "********", "********", "********",
-                "********", "39 WK 41", "16 WK 43", "40 WK 43", "11 WK 92", " 8 WK 96", "39 WK 41",
-                "16 WK 43", "40 WK 43", "11 WK 92", " 8 WK 96", "********", "********",
-            ],
-        );
-    }
-
-    #[test]
-    fn wkyr10() {
-        test_dates(
-            Format::new(Type::WkYr, 10, 0).unwrap(),
-            &[
-                "24 WK 1648",
-                "26 WK 1680",
-                "30 WK 1716",
-                "25 WK 1768",
-                "31 WK 1819",
-                "13 WK 1839",
-                "16 WK 1903",
-                "34 WK 1929",
-                "39 WK 1941",
-                "16 WK 1943",
-                "40 WK 1943",
-                "11 WK 1992",
-                " 8 WK 1996",
-                "39 WK 1941",
-                "16 WK 1943",
-                "40 WK 1943",
-                "11 WK 1992",
-                " 8 WK 1996",
-                "45 WK 2038",
-                "29 WK 2094",
-            ],
-        );
-    }
-
-    #[test]
-    fn datetime17() {
-        test_dates(
-            Format::new(Type::DateTime, 17, 0).unwrap(),
-            &[
-                "10-JUN-1648 00:00",
-                "30-JUN-1680 04:50",
-                "24-JUL-1716 12:31",
-                "19-JUN-1768 12:47",
-                "02-AUG-1819 01:26",
-                "27-MAR-1839 20:58",
-                "19-APR-1903 07:36",
-                "25-AUG-1929 15:43",
-                "29-SEP-1941 04:25",
-                "19-APR-1943 06:49",
-                "07-OCT-1943 02:57",
-                "17-MAR-1992 16:45",
-                "25-FEB-1996 21:30",
-                "29-SEP-1941 04:25",
-                "19-APR-1943 06:49",
-                "07-OCT-1943 02:57",
-                "17-MAR-1992 16:45",
-                "25-FEB-1996 21:30",
-                "10-NOV-2038 22:30",
-                "18-JUL-2094 01:56",
-            ],
-        );
-    }
-
-    #[test]
-    fn datetime18() {
-        test_dates(
-            Format::new(Type::DateTime, 18, 0).unwrap(),
-            &[
-                " 10-JUN-1648 00:00",
-                " 30-JUN-1680 04:50",
-                " 24-JUL-1716 12:31",
-                " 19-JUN-1768 12:47",
-                " 02-AUG-1819 01:26",
-                " 27-MAR-1839 20:58",
-                " 19-APR-1903 07:36",
-                " 25-AUG-1929 15:43",
-                " 29-SEP-1941 04:25",
-                " 19-APR-1943 06:49",
-                " 07-OCT-1943 02:57",
-                " 17-MAR-1992 16:45",
-                " 25-FEB-1996 21:30",
-                " 29-SEP-1941 04:25",
-                " 19-APR-1943 06:49",
-                " 07-OCT-1943 02:57",
-                " 17-MAR-1992 16:45",
-                " 25-FEB-1996 21:30",
-                " 10-NOV-2038 22:30",
-                " 18-JUL-2094 01:56",
-            ],
-        );
-    }
-
-    #[test]
-    fn datetime19() {
-        test_dates(
-            Format::new(Type::DateTime, 19, 0).unwrap(),
-            &[
-                "  10-JUN-1648 00:00",
-                "  30-JUN-1680 04:50",
-                "  24-JUL-1716 12:31",
-                "  19-JUN-1768 12:47",
-                "  02-AUG-1819 01:26",
-                "  27-MAR-1839 20:58",
-                "  19-APR-1903 07:36",
-                "  25-AUG-1929 15:43",
-                "  29-SEP-1941 04:25",
-                "  19-APR-1943 06:49",
-                "  07-OCT-1943 02:57",
-                "  17-MAR-1992 16:45",
-                "  25-FEB-1996 21:30",
-                "  29-SEP-1941 04:25",
-                "  19-APR-1943 06:49",
-                "  07-OCT-1943 02:57",
-                "  17-MAR-1992 16:45",
-                "  25-FEB-1996 21:30",
-                "  10-NOV-2038 22:30",
-                "  18-JUL-2094 01:56",
-            ],
-        );
-    }
-
-    #[test]
-    fn datetime20() {
-        test_dates(
-            Format::new(Type::DateTime, 20, 0).unwrap(),
-            &[
-                "10-JUN-1648 00:00:00",
-                "30-JUN-1680 04:50:38",
-                "24-JUL-1716 12:31:35",
-                "19-JUN-1768 12:47:53",
-                "02-AUG-1819 01:26:00",
-                "27-MAR-1839 20:58:11",
-                "19-APR-1903 07:36:05",
-                "25-AUG-1929 15:43:49",
-                "29-SEP-1941 04:25:09",
-                "19-APR-1943 06:49:27",
-                "07-OCT-1943 02:57:52",
-                "17-MAR-1992 16:45:44",
-                "25-FEB-1996 21:30:57",
-                "29-SEP-1941 04:25:09",
-                "19-APR-1943 06:49:27",
-                "07-OCT-1943 02:57:52",
-                "17-MAR-1992 16:45:44",
-                "25-FEB-1996 21:30:57",
-                "10-NOV-2038 22:30:04",
-                "18-JUL-2094 01:56:51",
-            ],
-        );
-    }
-
-    #[test]
-    fn datetime21() {
-        test_dates(
-            Format::new(Type::DateTime, 21, 0).unwrap(),
-            &[
-                " 10-JUN-1648 00:00:00",
-                " 30-JUN-1680 04:50:38",
-                " 24-JUL-1716 12:31:35",
-                " 19-JUN-1768 12:47:53",
-                " 02-AUG-1819 01:26:00",
-                " 27-MAR-1839 20:58:11",
-                " 19-APR-1903 07:36:05",
-                " 25-AUG-1929 15:43:49",
-                " 29-SEP-1941 04:25:09",
-                " 19-APR-1943 06:49:27",
-                " 07-OCT-1943 02:57:52",
-                " 17-MAR-1992 16:45:44",
-                " 25-FEB-1996 21:30:57",
-                " 29-SEP-1941 04:25:09",
-                " 19-APR-1943 06:49:27",
-                " 07-OCT-1943 02:57:52",
-                " 17-MAR-1992 16:45:44",
-                " 25-FEB-1996 21:30:57",
-                " 10-NOV-2038 22:30:04",
-                " 18-JUL-2094 01:56:51",
-            ],
-        );
-    }
-
-    #[test]
-    fn datetime22() {
-        test_dates(
-            Format::new(Type::DateTime, 22, 0).unwrap(),
-            &[
-                "  10-JUN-1648 00:00:00",
-                "  30-JUN-1680 04:50:38",
-                "  24-JUL-1716 12:31:35",
-                "  19-JUN-1768 12:47:53",
-                "  02-AUG-1819 01:26:00",
-                "  27-MAR-1839 20:58:11",
-                "  19-APR-1903 07:36:05",
-                "  25-AUG-1929 15:43:49",
-                "  29-SEP-1941 04:25:09",
-                "  19-APR-1943 06:49:27",
-                "  07-OCT-1943 02:57:52",
-                "  17-MAR-1992 16:45:44",
-                "  25-FEB-1996 21:30:57",
-                "  29-SEP-1941 04:25:09",
-                "  19-APR-1943 06:49:27",
-                "  07-OCT-1943 02:57:52",
-                "  17-MAR-1992 16:45:44",
-                "  25-FEB-1996 21:30:57",
-                "  10-NOV-2038 22:30:04",
-                "  18-JUL-2094 01:56:51",
-            ],
-        );
-    }
-
-    #[test]
-    fn datetime22_1() {
-        test_dates(
-            Format::new(Type::DateTime, 22, 1).unwrap(),
-            &[
-                "10-JUN-1648 00:00:00.0",
-                "30-JUN-1680 04:50:38.1",
-                "24-JUL-1716 12:31:35.2",
-                "19-JUN-1768 12:47:53.3",
-                "02-AUG-1819 01:26:00.5",
-                "27-MAR-1839 20:58:11.6",
-                "19-APR-1903 07:36:05.2",
-                "25-AUG-1929 15:43:49.8",
-                "29-SEP-1941 04:25:09.0",
-                "19-APR-1943 06:49:27.5",
-                "07-OCT-1943 02:57:52.0",
-                "17-MAR-1992 16:45:44.9",
-                "25-FEB-1996 21:30:57.8",
-                "29-SEP-1941 04:25:09.2",
-                "19-APR-1943 06:49:27.1",
-                "07-OCT-1943 02:57:52.5",
-                "17-MAR-1992 16:45:44.7",
-                "25-FEB-1996 21:30:57.6",
-                "10-NOV-2038 22:30:04.2",
-                "18-JUL-2094 01:56:51.6",
-            ],
-        );
-    }
-
-    #[test]
-    fn datetime23_2() {
-        test_dates(
-            Format::new(Type::DateTime, 23, 2).unwrap(),
-            &[
-                "10-JUN-1648 00:00:00.00",
-                "30-JUN-1680 04:50:38.12",
-                "24-JUL-1716 12:31:35.23",
-                "19-JUN-1768 12:47:53.35",
-                "02-AUG-1819 01:26:00.46",
-                "27-MAR-1839 20:58:11.57",
-                "19-APR-1903 07:36:05.19",
-                "25-AUG-1929 15:43:49.83",
-                "29-SEP-1941 04:25:09.01",
-                "19-APR-1943 06:49:27.52",
-                "07-OCT-1943 02:57:52.02",
-                "17-MAR-1992 16:45:44.87",
-                "25-FEB-1996 21:30:57.82",
-                "29-SEP-1941 04:25:09.15",
-                "19-APR-1943 06:49:27.11",
-                "07-OCT-1943 02:57:52.48",
-                "17-MAR-1992 16:45:44.66",
-                "25-FEB-1996 21:30:57.58",
-                "10-NOV-2038 22:30:04.18",
-                "18-JUL-2094 01:56:51.59",
-            ],
-        );
-    }
-
-    #[test]
-    fn datetime24_3() {
-        test_dates(
-            Format::new(Type::DateTime, 24, 3).unwrap(),
-            &[
-                "10-JUN-1648 00:00:00.000",
-                "30-JUN-1680 04:50:38.123",
-                "24-JUL-1716 12:31:35.235",
-                "19-JUN-1768 12:47:53.345",
-                "02-AUG-1819 01:26:00.456",
-                "27-MAR-1839 20:58:11.567",
-                "19-APR-1903 07:36:05.190",
-                "25-AUG-1929 15:43:49.831",
-                "29-SEP-1941 04:25:09.013",
-                "19-APR-1943 06:49:27.524",
-                "07-OCT-1943 02:57:52.016",
-                "17-MAR-1992 16:45:44.865",
-                "25-FEB-1996 21:30:57.820",
-                "29-SEP-1941 04:25:09.154",
-                "19-APR-1943 06:49:27.105",
-                "07-OCT-1943 02:57:52.482",
-                "17-MAR-1992 16:45:44.658",
-                "25-FEB-1996 21:30:57.582",
-                "10-NOV-2038 22:30:04.183",
-                "18-JUL-2094 01:56:51.593",
-            ],
-        );
-    }
-
-    #[test]
-    fn datetime25_4() {
-        test_dates(
-            Format::new(Type::DateTime, 25, 4).unwrap(),
-            &[
-                "10-JUN-1648 00:00:00.0000",
-                "30-JUN-1680 04:50:38.1230",
-                "24-JUL-1716 12:31:35.2345",
-                "19-JUN-1768 12:47:53.3450",
-                "02-AUG-1819 01:26:00.4562",
-                "27-MAR-1839 20:58:11.5668",
-                "19-APR-1903 07:36:05.1896",
-                "25-AUG-1929 15:43:49.8313",
-                "29-SEP-1941 04:25:09.0129",
-                "19-APR-1943 06:49:27.5238",
-                "07-OCT-1943 02:57:52.0156",
-                "17-MAR-1992 16:45:44.8653",
-                "25-FEB-1996 21:30:57.8205",
-                "29-SEP-1941 04:25:09.1539",
-                "19-APR-1943 06:49:27.1053",
-                "07-OCT-1943 02:57:52.4823",
-                "17-MAR-1992 16:45:44.6583",
-                "25-FEB-1996 21:30:57.5822",
-                "10-NOV-2038 22:30:04.1835",
-                "18-JUL-2094 01:56:51.5932",
-            ],
-        );
-    }
-
-    #[test]
-    fn datetime26_5() {
-        test_dates(
-            Format::new(Type::DateTime, 26, 5).unwrap(),
-            &[
-                "10-JUN-1648 00:00:00.00000",
-                "30-JUN-1680 04:50:38.12301",
-                "24-JUL-1716 12:31:35.23453",
-                "19-JUN-1768 12:47:53.34505",
-                "02-AUG-1819 01:26:00.45615",
-                "27-MAR-1839 20:58:11.56677",
-                "19-APR-1903 07:36:05.18964",
-                "25-AUG-1929 15:43:49.83132",
-                "29-SEP-1941 04:25:09.01293",
-                "19-APR-1943 06:49:27.52375",
-                "07-OCT-1943 02:57:52.01565",
-                "17-MAR-1992 16:45:44.86529",
-                "25-FEB-1996 21:30:57.82047",
-                "29-SEP-1941 04:25:09.15395",
-                "19-APR-1943 06:49:27.10533",
-                "07-OCT-1943 02:57:52.48229",
-                "17-MAR-1992 16:45:44.65827",
-                "25-FEB-1996 21:30:57.58219",
-                "10-NOV-2038 22:30:04.18347",
-                "18-JUL-2094 01:56:51.59319",
-            ],
-        );
-    }
-
-    #[test]
-    fn ymdhms16() {
-        test_dates(
-            Format::new(Type::YmdHms, 16, 0).unwrap(),
-            &[
-                "1648-06-10 00:00",
-                "1680-06-30 04:50",
-                "1716-07-24 12:31",
-                "1768-06-19 12:47",
-                "1819-08-02 01:26",
-                "1839-03-27 20:58",
-                "1903-04-19 07:36",
-                "1929-08-25 15:43",
-                "1941-09-29 04:25",
-                "1943-04-19 06:49",
-                "1943-10-07 02:57",
-                "1992-03-17 16:45",
-                "1996-02-25 21:30",
-                "1941-09-29 04:25",
-                "1943-04-19 06:49",
-                "1943-10-07 02:57",
-                "1992-03-17 16:45",
-                "1996-02-25 21:30",
-                "2038-11-10 22:30",
-                "2094-07-18 01:56",
-            ],
-        );
-    }
-
-    #[test]
-    fn ymdhms17() {
-        test_dates(
-            Format::new(Type::YmdHms, 17, 0).unwrap(),
-            &[
-                " 1648-06-10 00:00",
-                " 1680-06-30 04:50",
-                " 1716-07-24 12:31",
-                " 1768-06-19 12:47",
-                " 1819-08-02 01:26",
-                " 1839-03-27 20:58",
-                " 1903-04-19 07:36",
-                " 1929-08-25 15:43",
-                " 1941-09-29 04:25",
-                " 1943-04-19 06:49",
-                " 1943-10-07 02:57",
-                " 1992-03-17 16:45",
-                " 1996-02-25 21:30",
-                " 1941-09-29 04:25",
-                " 1943-04-19 06:49",
-                " 1943-10-07 02:57",
-                " 1992-03-17 16:45",
-                " 1996-02-25 21:30",
-                " 2038-11-10 22:30",
-                " 2094-07-18 01:56",
-            ],
-        );
-    }
-
-    #[test]
-    fn ymdhms18() {
-        test_dates(
-            Format::new(Type::YmdHms, 18, 0).unwrap(),
-            &[
-                "  1648-06-10 00:00",
-                "  1680-06-30 04:50",
-                "  1716-07-24 12:31",
-                "  1768-06-19 12:47",
-                "  1819-08-02 01:26",
-                "  1839-03-27 20:58",
-                "  1903-04-19 07:36",
-                "  1929-08-25 15:43",
-                "  1941-09-29 04:25",
-                "  1943-04-19 06:49",
-                "  1943-10-07 02:57",
-                "  1992-03-17 16:45",
-                "  1996-02-25 21:30",
-                "  1941-09-29 04:25",
-                "  1943-04-19 06:49",
-                "  1943-10-07 02:57",
-                "  1992-03-17 16:45",
-                "  1996-02-25 21:30",
-                "  2038-11-10 22:30",
-                "  2094-07-18 01:56",
-            ],
-        );
-    }
-
-    #[test]
-    fn ymdhms19() {
-        test_dates(
-            Format::new(Type::YmdHms, 19, 0).unwrap(),
-            &[
-                "1648-06-10 00:00:00",
-                "1680-06-30 04:50:38",
-                "1716-07-24 12:31:35",
-                "1768-06-19 12:47:53",
-                "1819-08-02 01:26:00",
-                "1839-03-27 20:58:11",
-                "1903-04-19 07:36:05",
-                "1929-08-25 15:43:49",
-                "1941-09-29 04:25:09",
-                "1943-04-19 06:49:27",
-                "1943-10-07 02:57:52",
-                "1992-03-17 16:45:44",
-                "1996-02-25 21:30:57",
-                "1941-09-29 04:25:09",
-                "1943-04-19 06:49:27",
-                "1943-10-07 02:57:52",
-                "1992-03-17 16:45:44",
-                "1996-02-25 21:30:57",
-                "2038-11-10 22:30:04",
-                "2094-07-18 01:56:51",
-            ],
-        );
-    }
-
-    #[test]
-    fn ymdhms20() {
-        test_dates(
-            Format::new(Type::YmdHms, 20, 0).unwrap(),
-            &[
-                " 1648-06-10 00:00:00",
-                " 1680-06-30 04:50:38",
-                " 1716-07-24 12:31:35",
-                " 1768-06-19 12:47:53",
-                " 1819-08-02 01:26:00",
-                " 1839-03-27 20:58:11",
-                " 1903-04-19 07:36:05",
-                " 1929-08-25 15:43:49",
-                " 1941-09-29 04:25:09",
-                " 1943-04-19 06:49:27",
-                " 1943-10-07 02:57:52",
-                " 1992-03-17 16:45:44",
-                " 1996-02-25 21:30:57",
-                " 1941-09-29 04:25:09",
-                " 1943-04-19 06:49:27",
-                " 1943-10-07 02:57:52",
-                " 1992-03-17 16:45:44",
-                " 1996-02-25 21:30:57",
-                " 2038-11-10 22:30:04",
-                " 2094-07-18 01:56:51",
-            ],
-        );
-    }
-
-    #[test]
-    fn ymdhms21() {
-        test_dates(
-            Format::new(Type::YmdHms, 21, 0).unwrap(),
-            &[
-                "  1648-06-10 00:00:00",
-                "  1680-06-30 04:50:38",
-                "  1716-07-24 12:31:35",
-                "  1768-06-19 12:47:53",
-                "  1819-08-02 01:26:00",
-                "  1839-03-27 20:58:11",
-                "  1903-04-19 07:36:05",
-                "  1929-08-25 15:43:49",
-                "  1941-09-29 04:25:09",
-                "  1943-04-19 06:49:27",
-                "  1943-10-07 02:57:52",
-                "  1992-03-17 16:45:44",
-                "  1996-02-25 21:30:57",
-                "  1941-09-29 04:25:09",
-                "  1943-04-19 06:49:27",
-                "  1943-10-07 02:57:52",
-                "  1992-03-17 16:45:44",
-                "  1996-02-25 21:30:57",
-                "  2038-11-10 22:30:04",
-                "  2094-07-18 01:56:51",
-            ],
-        );
-    }
-
-    #[test]
-    fn ymdhms21_1() {
-        test_dates(
-            Format::new(Type::YmdHms, 21, 1).unwrap(),
-            &[
-                "1648-06-10 00:00:00.0",
-                "1680-06-30 04:50:38.1",
-                "1716-07-24 12:31:35.2",
-                "1768-06-19 12:47:53.3",
-                "1819-08-02 01:26:00.5",
-                "1839-03-27 20:58:11.6",
-                "1903-04-19 07:36:05.2",
-                "1929-08-25 15:43:49.8",
-                "1941-09-29 04:25:09.0",
-                "1943-04-19 06:49:27.5",
-                "1943-10-07 02:57:52.0",
-                "1992-03-17 16:45:44.9",
-                "1996-02-25 21:30:57.8",
-                "1941-09-29 04:25:09.2",
-                "1943-04-19 06:49:27.1",
-                "1943-10-07 02:57:52.5",
-                "1992-03-17 16:45:44.7",
-                "1996-02-25 21:30:57.6",
-                "2038-11-10 22:30:04.2",
-                "2094-07-18 01:56:51.6",
-            ],
-        );
-    }
-
-    #[test]
-    fn ymdhms22_2() {
-        test_dates(
-            Format::new(Type::YmdHms, 22, 2).unwrap(),
-            &[
-                "1648-06-10 00:00:00.00",
-                "1680-06-30 04:50:38.12",
-                "1716-07-24 12:31:35.23",
-                "1768-06-19 12:47:53.35",
-                "1819-08-02 01:26:00.46",
-                "1839-03-27 20:58:11.57",
-                "1903-04-19 07:36:05.19",
-                "1929-08-25 15:43:49.83",
-                "1941-09-29 04:25:09.01",
-                "1943-04-19 06:49:27.52",
-                "1943-10-07 02:57:52.02",
-                "1992-03-17 16:45:44.87",
-                "1996-02-25 21:30:57.82",
-                "1941-09-29 04:25:09.15",
-                "1943-04-19 06:49:27.11",
-                "1943-10-07 02:57:52.48",
-                "1992-03-17 16:45:44.66",
-                "1996-02-25 21:30:57.58",
-                "2038-11-10 22:30:04.18",
-                "2094-07-18 01:56:51.59",
-            ],
-        );
-    }
-
-    #[test]
-    fn ymdhms23_3() {
-        test_dates(
-            Format::new(Type::YmdHms, 23, 3).unwrap(),
-            &[
-                "1648-06-10 00:00:00.000",
-                "1680-06-30 04:50:38.123",
-                "1716-07-24 12:31:35.235",
-                "1768-06-19 12:47:53.345",
-                "1819-08-02 01:26:00.456",
-                "1839-03-27 20:58:11.567",
-                "1903-04-19 07:36:05.190",
-                "1929-08-25 15:43:49.831",
-                "1941-09-29 04:25:09.013",
-                "1943-04-19 06:49:27.524",
-                "1943-10-07 02:57:52.016",
-                "1992-03-17 16:45:44.865",
-                "1996-02-25 21:30:57.820",
-                "1941-09-29 04:25:09.154",
-                "1943-04-19 06:49:27.105",
-                "1943-10-07 02:57:52.482",
-                "1992-03-17 16:45:44.658",
-                "1996-02-25 21:30:57.582",
-                "2038-11-10 22:30:04.183",
-                "2094-07-18 01:56:51.593",
-            ],
-        );
-    }
-
-    #[test]
-    fn ymdhms24_4() {
-        test_dates(
-            Format::new(Type::YmdHms, 24, 4).unwrap(),
-            &[
-                "1648-06-10 00:00:00.0000",
-                "1680-06-30 04:50:38.1230",
-                "1716-07-24 12:31:35.2345",
-                "1768-06-19 12:47:53.3450",
-                "1819-08-02 01:26:00.4562",
-                "1839-03-27 20:58:11.5668",
-                "1903-04-19 07:36:05.1896",
-                "1929-08-25 15:43:49.8313",
-                "1941-09-29 04:25:09.0129",
-                "1943-04-19 06:49:27.5238",
-                "1943-10-07 02:57:52.0156",
-                "1992-03-17 16:45:44.8653",
-                "1996-02-25 21:30:57.8205",
-                "1941-09-29 04:25:09.1539",
-                "1943-04-19 06:49:27.1053",
-                "1943-10-07 02:57:52.4823",
-                "1992-03-17 16:45:44.6583",
-                "1996-02-25 21:30:57.5822",
-                "2038-11-10 22:30:04.1835",
-                "2094-07-18 01:56:51.5932",
-            ],
-        );
-    }
-
-    #[test]
-    fn ymdhms25_5() {
-        test_dates(
-            Format::new(Type::YmdHms, 25, 5).unwrap(),
-            &[
-                "1648-06-10 00:00:00.00000",
-                "1680-06-30 04:50:38.12301",
-                "1716-07-24 12:31:35.23453",
-                "1768-06-19 12:47:53.34505",
-                "1819-08-02 01:26:00.45615",
-                "1839-03-27 20:58:11.56677",
-                "1903-04-19 07:36:05.18964",
-                "1929-08-25 15:43:49.83132",
-                "1941-09-29 04:25:09.01293",
-                "1943-04-19 06:49:27.52375",
-                "1943-10-07 02:57:52.01565",
-                "1992-03-17 16:45:44.86529",
-                "1996-02-25 21:30:57.82047",
-                "1941-09-29 04:25:09.15395",
-                "1943-04-19 06:49:27.10533",
-                "1943-10-07 02:57:52.48229",
-                "1992-03-17 16:45:44.65827",
-                "1996-02-25 21:30:57.58219",
-                "2038-11-10 22:30:04.18347",
-                "2094-07-18 01:56:51.59319",
-            ],
-        );
-    }
-
-    fn test_times(format: Format, name: &str) {
-        let directory = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/format/testdata/display");
-        let input_filename = directory.join("time-input.txt");
-        let input = BufReader::new(File::open(&input_filename).unwrap());
-
-        let output_filename = directory.join(name);
-        let output = BufReader::new(File::open(&output_filename).unwrap());
-
-        let parser = Type::DTime.parser(UTF_8);
-        for ((input, expect), line_number) in input
-            .lines()
-            .map(|r| r.unwrap())
-            .zip_eq(output.lines().map(|r| r.unwrap()))
-            .zip(1..)
-        {
-            let value = parser.parse(&input).unwrap();
-            let formatted = value.display(format, UTF_8).to_string();
-            assert!(
-                formatted == expect,
-                "formatting {}:{line_number} as {format}:\n  actual: {formatted:?}\nexpected: {expect:?}",
-                input_filename.display()
-            );
-        }
-    }
-
-    #[test]
-    fn time5() {
-        test_times(Format::new(Type::Time, 5, 0).unwrap(), "time5.txt");
-    }
-
-    #[test]
-    fn time6() {
-        test_times(Format::new(Type::Time, 6, 0).unwrap(), "time6.txt");
-    }
-
-    #[test]
-    fn time7() {
-        test_times(Format::new(Type::Time, 7, 0).unwrap(), "time7.txt");
-    }
-
-    #[test]
-    fn time8() {
-        test_times(Format::new(Type::Time, 8, 0).unwrap(), "time8.txt");
-    }
-
-    #[test]
-    fn time9() {
-        test_times(Format::new(Type::Time, 9, 0).unwrap(), "time9.txt");
-    }
-
-    #[test]
-    fn time10() {
-        test_times(Format::new(Type::Time, 10, 0).unwrap(), "time10.txt");
-    }
-
-    #[test]
-    fn time10_1() {
-        test_times(Format::new(Type::Time, 10, 1).unwrap(), "time10.1.txt");
-    }
-
-    #[test]
-    fn time11() {
-        test_times(Format::new(Type::Time, 11, 0).unwrap(), "time11.txt");
-    }
-
-    #[test]
-    fn time11_1() {
-        test_times(Format::new(Type::Time, 11, 1).unwrap(), "time11.1.txt");
-    }
-
-    #[test]
-    fn time11_2() {
-        test_times(Format::new(Type::Time, 11, 2).unwrap(), "time11.2.txt");
-    }
-
-    #[test]
-    fn time12() {
-        test_times(Format::new(Type::Time, 12, 0).unwrap(), "time12.txt");
-    }
-
-    #[test]
-    fn time12_1() {
-        test_times(Format::new(Type::Time, 12, 1).unwrap(), "time12.1.txt");
-    }
-
-    #[test]
-    fn time12_2() {
-        test_times(Format::new(Type::Time, 12, 2).unwrap(), "time12.2.txt");
-    }
-
-    #[test]
-    fn time12_3() {
-        test_times(Format::new(Type::Time, 12, 3).unwrap(), "time12.3.txt");
-    }
-
-    #[test]
-    fn time13() {
-        test_times(Format::new(Type::Time, 13, 0).unwrap(), "time13.txt");
-    }
-
-    #[test]
-    fn time13_1() {
-        test_times(Format::new(Type::Time, 13, 1).unwrap(), "time13.1.txt");
-    }
-
-    #[test]
-    fn time13_2() {
-        test_times(Format::new(Type::Time, 13, 2).unwrap(), "time13.2.txt");
-    }
-
-    #[test]
-    fn time13_3() {
-        test_times(Format::new(Type::Time, 13, 3).unwrap(), "time13.3.txt");
-    }
-
-    #[test]
-    fn time13_4() {
-        test_times(Format::new(Type::Time, 13, 4).unwrap(), "time13.4.txt");
-    }
-
-    #[test]
-    fn time14() {
-        test_times(Format::new(Type::Time, 14, 0).unwrap(), "time14.txt");
-    }
-
-    #[test]
-    fn time14_1() {
-        test_times(Format::new(Type::Time, 14, 1).unwrap(), "time14.1.txt");
-    }
-
-    #[test]
-    fn time14_2() {
-        test_times(Format::new(Type::Time, 14, 2).unwrap(), "time14.2.txt");
-    }
-
-    #[test]
-    fn time14_3() {
-        test_times(Format::new(Type::Time, 14, 3).unwrap(), "time14.3.txt");
-    }
-
-    #[test]
-    fn time14_4() {
-        test_times(Format::new(Type::Time, 14, 4).unwrap(), "time14.4.txt");
-    }
-
-    #[test]
-    fn time14_5() {
-        test_times(Format::new(Type::Time, 14, 5).unwrap(), "time14.5.txt");
-    }
-
-    #[test]
-    fn time15() {
-        test_times(Format::new(Type::Time, 15, 0).unwrap(), "time15.txt");
-    }
-
-    #[test]
-    fn time15_1() {
-        test_times(Format::new(Type::Time, 15, 1).unwrap(), "time15.1.txt");
-    }
-
-    #[test]
-    fn time15_2() {
-        test_times(Format::new(Type::Time, 15, 2).unwrap(), "time15.2.txt");
-    }
-
-    #[test]
-    fn time15_3() {
-        test_times(Format::new(Type::Time, 15, 3).unwrap(), "time15.3.txt");
-    }
-
-    #[test]
-    fn time15_4() {
-        test_times(Format::new(Type::Time, 15, 4).unwrap(), "time15.4.txt");
-    }
-
-    #[test]
-    fn time15_5() {
-        test_times(Format::new(Type::Time, 15, 5).unwrap(), "time15.5.txt");
-    }
-
-    #[test]
-    fn time15_6() {
-        test_times(Format::new(Type::Time, 15, 6).unwrap(), "time15.6.txt");
-    }
-
-    #[test]
-    fn mtime5() {
-        test_times(Format::new(Type::MTime, 5, 0).unwrap(), "mtime5.txt");
-    }
-
-    #[test]
-    fn mtime6() {
-        test_times(Format::new(Type::MTime, 6, 0).unwrap(), "mtime6.txt");
-    }
-
-    #[test]
-    fn mtime7() {
-        test_times(Format::new(Type::MTime, 7, 0).unwrap(), "mtime7.txt");
-    }
-
-    #[test]
-    fn mtime7_1() {
-        test_times(Format::new(Type::MTime, 7, 1).unwrap(), "mtime7.1.txt");
-    }
-
-    #[test]
-    fn mtime8() {
-        test_times(Format::new(Type::MTime, 8, 0).unwrap(), "mtime8.txt");
-    }
-
-    #[test]
-    fn mtime8_1() {
-        test_times(Format::new(Type::MTime, 8, 1).unwrap(), "mtime8.1.txt");
-    }
-
-    #[test]
-    fn mtime8_2() {
-        test_times(Format::new(Type::MTime, 8, 2).unwrap(), "mtime8.2.txt");
-    }
-
-    #[test]
-    fn mtime9() {
-        test_times(Format::new(Type::MTime, 9, 0).unwrap(), "mtime9.txt");
-    }
-
-    #[test]
-    fn mtime9_1() {
-        test_times(Format::new(Type::MTime, 9, 1).unwrap(), "mtime9.1.txt");
-    }
-
-    #[test]
-    fn mtime9_2() {
-        test_times(Format::new(Type::MTime, 9, 2).unwrap(), "mtime9.2.txt");
-    }
-
-    #[test]
-    fn mtime9_3() {
-        test_times(Format::new(Type::MTime, 9, 3).unwrap(), "mtime9.3.txt");
-    }
-
-    #[test]
-    fn mtime10() {
-        test_times(Format::new(Type::MTime, 10, 0).unwrap(), "mtime10.txt");
-    }
-
-    #[test]
-    fn mtime10_1() {
-        test_times(Format::new(Type::MTime, 10, 1).unwrap(), "mtime10.1.txt");
-    }
-
-    #[test]
-    fn mtime10_2() {
-        test_times(Format::new(Type::MTime, 10, 2).unwrap(), "mtime10.2.txt");
-    }
-
-    #[test]
-    fn mtime10_3() {
-        test_times(Format::new(Type::MTime, 10, 3).unwrap(), "mtime10.3.txt");
-    }
-
-    #[test]
-    fn mtime10_4() {
-        test_times(Format::new(Type::MTime, 10, 4).unwrap(), "mtime10.4.txt");
-    }
-
-    #[test]
-    fn mtime11() {
-        test_times(Format::new(Type::MTime, 11, 0).unwrap(), "mtime11.txt");
-    }
-
-    #[test]
-    fn mtime11_1() {
-        test_times(Format::new(Type::MTime, 11, 1).unwrap(), "mtime11.1.txt");
-    }
-
-    #[test]
-    fn mtime11_2() {
-        test_times(Format::new(Type::MTime, 11, 2).unwrap(), "mtime11.2.txt");
-    }
-
-    #[test]
-    fn mtime11_3() {
-        test_times(Format::new(Type::MTime, 11, 3).unwrap(), "mtime11.3.txt");
-    }
-
-    #[test]
-    fn mtime11_4() {
-        test_times(Format::new(Type::MTime, 11, 4).unwrap(), "mtime11.4.txt");
-    }
-
-    #[test]
-    fn mtime11_5() {
-        test_times(Format::new(Type::MTime, 11, 5).unwrap(), "mtime11.5.txt");
-    }
-
-    #[test]
-    fn mtime12_5() {
-        test_times(Format::new(Type::MTime, 12, 5).unwrap(), "mtime12.5.txt");
-    }
-
-    #[test]
-    fn mtime13_5() {
-        test_times(Format::new(Type::MTime, 13, 5).unwrap(), "mtime13.5.txt");
-    }
-
-    #[test]
-    fn mtime14_5() {
-        test_times(Format::new(Type::MTime, 14, 5).unwrap(), "mtime14.5.txt");
-    }
-
-    #[test]
-    fn mtime15_5() {
-        test_times(Format::new(Type::MTime, 15, 5).unwrap(), "mtime15.5.txt");
-    }
-
-    #[test]
-    fn mtime16_5() {
-        test_times(Format::new(Type::MTime, 16, 5).unwrap(), "mtime16.5.txt");
-    }
-
-    #[test]
-    fn dtime8() {
-        test_times(Format::new(Type::DTime, 8, 0).unwrap(), "dtime8.txt");
-    }
-
-    #[test]
-    fn dtime9() {
-        test_times(Format::new(Type::DTime, 9, 0).unwrap(), "dtime9.txt");
-    }
-
-    #[test]
-    fn dtime10() {
-        test_times(Format::new(Type::DTime, 10, 0).unwrap(), "dtime10.txt");
-    }
-
-    #[test]
-    fn dtime11() {
-        test_times(Format::new(Type::DTime, 11, 0).unwrap(), "dtime11.txt");
-    }
-
-    #[test]
-    fn dtime12() {
-        test_times(Format::new(Type::DTime, 12, 0).unwrap(), "dtime12.txt");
-    }
-
-    #[test]
-    fn dtime13() {
-        test_times(Format::new(Type::DTime, 13, 0).unwrap(), "dtime13.txt");
-    }
-
-    #[test]
-    fn dtime13_1() {
-        test_times(Format::new(Type::DTime, 13, 1).unwrap(), "dtime13.1.txt");
-    }
-
-    #[test]
-    fn dtime14() {
-        test_times(Format::new(Type::DTime, 14, 0).unwrap(), "dtime14.txt");
-    }
-
-    #[test]
-    fn dtime14_1() {
-        test_times(Format::new(Type::DTime, 14, 1).unwrap(), "dtime14.1.txt");
-    }
-
-    #[test]
-    fn dtime14_2() {
-        test_times(Format::new(Type::DTime, 14, 2).unwrap(), "dtime14.2.txt");
-    }
-
-    #[test]
-    fn dtime15() {
-        test_times(Format::new(Type::DTime, 15, 0).unwrap(), "dtime15.txt");
-    }
-
-    #[test]
-    fn dtime15_1() {
-        test_times(Format::new(Type::DTime, 15, 1).unwrap(), "dtime15.1.txt");
-    }
-
-    #[test]
-    fn dtime15_2() {
-        test_times(Format::new(Type::DTime, 15, 2).unwrap(), "dtime15.2.txt");
-    }
-
-    #[test]
-    fn dtime15_3() {
-        test_times(Format::new(Type::DTime, 15, 3).unwrap(), "dtime15.3.txt");
-    }
-
-    #[test]
-    fn dtime16() {
-        test_times(Format::new(Type::DTime, 16, 0).unwrap(), "dtime16.txt");
-    }
-
-    #[test]
-    fn dtime16_1() {
-        test_times(Format::new(Type::DTime, 16, 1).unwrap(), "dtime16.1.txt");
-    }
-
-    #[test]
-    fn dtime16_2() {
-        test_times(Format::new(Type::DTime, 16, 2).unwrap(), "dtime16.2.txt");
-    }
-
-    #[test]
-    fn dtime16_3() {
-        test_times(Format::new(Type::DTime, 16, 3).unwrap(), "dtime16.3.txt");
-    }
-
-    #[test]
-    fn dtime16_4() {
-        test_times(Format::new(Type::DTime, 16, 4).unwrap(), "dtime16.4.txt");
-    }
-
-    #[test]
-    fn dtime17() {
-        test_times(Format::new(Type::DTime, 17, 0).unwrap(), "dtime17.txt");
-    }
-
-    #[test]
-    fn dtime17_1() {
-        test_times(Format::new(Type::DTime, 17, 1).unwrap(), "dtime17.1.txt");
-    }
-
-    #[test]
-    fn dtime17_2() {
-        test_times(Format::new(Type::DTime, 17, 2).unwrap(), "dtime17.2.txt");
-    }
-
-    #[test]
-    fn dtime17_3() {
-        test_times(Format::new(Type::DTime, 17, 3).unwrap(), "dtime17.3.txt");
-    }
-
-    #[test]
-    fn dtime17_4() {
-        test_times(Format::new(Type::DTime, 17, 4).unwrap(), "dtime17.4.txt");
-    }
-
-    #[test]
-    fn dtime17_5() {
-        test_times(Format::new(Type::DTime, 17, 5).unwrap(), "dtime17.5.txt");
-    }
-
-    #[test]
-    fn dtime18() {
-        test_times(Format::new(Type::DTime, 18, 0).unwrap(), "dtime18.txt");
-    }
-
-    #[test]
-    fn dtime18_1() {
-        test_times(Format::new(Type::DTime, 18, 1).unwrap(), "dtime18.1.txt");
-    }
-
-    #[test]
-    fn dtime18_2() {
-        test_times(Format::new(Type::DTime, 18, 2).unwrap(), "dtime18.2.txt");
-    }
-
-    #[test]
-    fn dtime18_3() {
-        test_times(Format::new(Type::DTime, 18, 3).unwrap(), "dtime18.3.txt");
-    }
-
-    #[test]
-    fn dtime18_4() {
-        test_times(Format::new(Type::DTime, 18, 4).unwrap(), "dtime18.4.txt");
-    }
-
-    #[test]
-    fn dtime18_5() {
-        test_times(Format::new(Type::DTime, 18, 5).unwrap(), "dtime18.5.txt");
-    }
-
-    #[test]
-    fn dtime18_6() {
-        test_times(Format::new(Type::DTime, 18, 6).unwrap(), "dtime18.6.txt");
-    }
-}
diff --git a/rust/pspp/src/format/display/mod.rs b/rust/pspp/src/format/display/mod.rs
new file mode 100644 (file)
index 0000000..b315c88
--- /dev/null
@@ -0,0 +1,1116 @@
+use std::{
+    cmp::min,
+    fmt::{Display, Error as FmtError, Formatter, Result as FmtResult, Write as _},
+    io::{Error as IoError, Write as IoWrite},
+    str::from_utf8_unchecked,
+};
+
+use chrono::{Datelike, NaiveDate};
+use encoding_rs::{Encoding, UTF_8};
+use libm::frexp;
+use smallstr::SmallString;
+use smallvec::{Array, SmallVec};
+
+use crate::{
+    calendar::{calendar_offset_to_gregorian, day_of_year, month_name, short_month_name},
+    dictionary::Value,
+    endian::ToBytes,
+    format::{Category, DateTemplate, Decimal, Format, NumberStyle, Settings, TemplateItem, Type},
+    settings::{EndianSettings, Settings as PsppSettings},
+};
+
+pub struct DisplayValue<'a, 'b> {
+    format: Format,
+    settings: &'b Settings,
+    endian: EndianSettings,
+    value: &'a Value,
+    encoding: &'static Encoding,
+}
+
+#[cfg(test)]
+mod test;
+
+pub trait DisplayPlain {
+    fn display_plain(&self) -> impl Display;
+}
+
+impl DisplayPlain for f64 {
+    fn display_plain(&self) -> impl Display {
+        DisplayPlainF64(*self)
+    }
+}
+
+pub struct DisplayPlainF64(f64);
+
+impl Display for DisplayPlainF64 {
+    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
+        if self.0.abs() < 0.0005 || self.0.abs() > 1e15 {
+            // Print self.0s that would otherwise have lots of leading or
+            // trailing zeros in scientific notation with full precision.
+            write!(f, "{:.e}", self.0)
+        } else if self.0 == self.0.trunc() {
+            // Print integers without decimal places.
+            write!(f, "{:.0}", self.0)
+        } else {
+            // Print other numbers with full precision.
+            write!(f, "{:.}", self.0)
+        }
+    }
+}
+
+impl Value {
+    /// Returns an object that implements [Display] for printing this `Value` as
+    /// `format`.  `encoding` specifies this `Value`'s encoding (therefore, it
+    /// is used only if this is a `Value::String`).
+    ///
+    /// [Display]: std::fmt::Display
+    pub fn display(&self, format: Format, encoding: &'static Encoding) -> DisplayValue {
+        DisplayValue::new(format, self, encoding)
+    }
+
+    pub fn display_plain(&self, encoding: &'static Encoding) -> DisplayValuePlain {
+        DisplayValuePlain {
+            value: self,
+            encoding,
+            quote_strings: true,
+        }
+    }
+}
+
+pub struct DisplayValuePlain<'a> {
+    value: &'a Value,
+    encoding: &'static Encoding,
+    quote_strings: bool,
+}
+
+impl DisplayValuePlain<'_> {
+    pub fn without_quotes(self) -> Self {
+        Self {
+            quote_strings: false,
+            ..self
+        }
+    }
+}
+
+impl Display for DisplayValuePlain<'_> {
+    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
+        match self.value {
+            Value::Number(None) => write!(f, "SYSMIS"),
+            Value::Number(Some(number)) => number.display_plain().fmt(f),
+            Value::String(string) => {
+                if self.quote_strings {
+                    write!(f, "\"{}\"", string.display(self.encoding))
+                } else {
+                    string.display(self.encoding).fmt(f)
+                }
+            }
+        }
+    }
+}
+
+impl Display for DisplayValue<'_, '_> {
+    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
+        let number = match self.value {
+            Value::Number(number) => *number,
+            Value::String(string) => {
+                if self.format.type_() == Type::AHex {
+                    for byte in &string.0 {
+                        write!(f, "{byte:02x}")?;
+                    }
+                } else {
+                    write!(
+                        f,
+                        "{}",
+                        self.encoding.decode_without_bom_handling(&string.0).0
+                    )?;
+                }
+                return Ok(());
+            }
+        };
+
+        let Some(number) = number else {
+            return self.missing(f);
+        };
+
+        match self.format.type_() {
+            Type::F
+            | Type::Comma
+            | Type::Dot
+            | Type::Dollar
+            | Type::Pct
+            | Type::E
+            | Type::CC(_) => self.number(f, number),
+            Type::N => self.n(f, number),
+            Type::Z => self.z(f, number),
+
+            Type::P | Type::PK | Type::IB | Type::PIB | Type::RB => self.fmt_binary(f),
+
+            Type::PIBHex => self.pibhex(f, number),
+            Type::RBHex => self.rbhex(f, number),
+            Type::Date
+            | Type::ADate
+            | Type::EDate
+            | Type::JDate
+            | Type::SDate
+            | Type::QYr
+            | Type::MoYr
+            | Type::WkYr
+            | Type::DateTime
+            | Type::YmdHms
+            | Type::MTime
+            | Type::Time
+            | Type::DTime
+            | Type::WkDay => self.date(f, number),
+            Type::Month => self.month(f, number),
+            Type::A | Type::AHex => unreachable!(),
+        }
+    }
+}
+
+impl<'a, 'b> DisplayValue<'a, 'b> {
+    pub fn new(format: Format, value: &'a Value, encoding: &'static Encoding) -> Self {
+        let settings = PsppSettings::global();
+        Self {
+            format,
+            value,
+            encoding,
+            settings: &settings.formats,
+            endian: settings.endian,
+        }
+    }
+    pub fn with_settings(self, settings: &'b Settings) -> Self {
+        Self { settings, ..self }
+    }
+    pub fn with_endian(self, endian: EndianSettings) -> Self {
+        Self { endian, ..self }
+    }
+    fn fmt_binary(&self, f: &mut Formatter) -> FmtResult {
+        let output = self.to_binary().unwrap();
+        for b in output {
+            f.write_char(b as char)?;
+        }
+        Ok(())
+    }
+    fn number(&self, f: &mut Formatter<'_>, number: f64) -> FmtResult {
+        if number.is_finite() {
+            let style = self.settings.number_style(self.format.type_);
+            if self.format.type_ != Type::E && number.abs() < 1.5 * power10(self.format.w()) {
+                let rounder = Rounder::new(style, number, self.format.d);
+                if self.decimal(f, &rounder, style, true)?
+                    || self.scientific(f, number, style, true)?
+                    || self.decimal(f, &rounder, style, false)?
+                {
+                    return Ok(());
+                }
+            }
+
+            if !self.scientific(f, number, style, false)? {
+                self.overflow(f)?;
+            }
+            Ok(())
+        } else {
+            self.infinite(f, number)
+        }
+    }
+
+    fn infinite(&self, f: &mut Formatter<'_>, number: f64) -> FmtResult {
+        if self.format.w >= 3 {
+            let s = if number.is_nan() {
+                "NaN"
+            } else if number.is_infinite() {
+                if number.is_sign_positive() {
+                    "+Infinity"
+                } else {
+                    "-Infinity"
+                }
+            } else {
+                "Unknown"
+            };
+            let w = self.format.w();
+            write!(f, "{s:>0$.*}", w)
+        } else {
+            self.overflow(f)
+        }
+    }
+
+    fn missing(&self, f: &mut Formatter<'_>) -> FmtResult {
+        match self.format.type_ {
+            Type::P | Type::PK | Type::IB | Type::PIB | Type::RB => return self.fmt_binary(f),
+            Type::RBHex => return self.rbhex(f, -f64::MAX),
+            _ => (),
+        }
+
+        let w = self.format.w() as isize;
+        let d = self.format.d() as isize;
+        let dot_position = match self.format.type_ {
+            Type::N => w - 1,
+            Type::Pct => w - d - 2,
+            Type::E => w - d - 5,
+            _ => w - d - 1,
+        };
+        let dot_position = dot_position.max(0) as u16;
+
+        for i in 0..self.format.w {
+            if i == dot_position {
+                write!(f, ".")?;
+            } else {
+                write!(f, " ")?;
+            }
+        }
+        Ok(())
+    }
+
+    fn overflow(&self, f: &mut Formatter<'_>) -> FmtResult {
+        for _ in 0..self.format.w {
+            write!(f, "*")?;
+        }
+        Ok(())
+    }
+
+    fn decimal(
+        &self,
+        f: &mut Formatter<'_>,
+        rounder: &Rounder,
+        style: &NumberStyle,
+        require_affixes: bool,
+    ) -> Result<bool, FmtError> {
+        for decimals in (0..=self.format.d).rev() {
+            // Make sure there's room for the number's magnitude, plus the
+            // negative suffix, plus (if negative) the negative prefix.
+            let RounderWidth {
+                mut width,
+                integer_digits,
+                negative,
+            } = rounder.width(decimals as usize);
+            width += style.neg_suffix.width;
+            if negative {
+                width += style.neg_prefix.width;
+            }
+            if width > self.format.w() {
+                continue;
+            }
+
+            // If there's room for the prefix and suffix, allocate
+            // space.  If the affixes are required, but there's no
+            // space, give up.
+            let add_affixes = allocate_space(style.affix_width(), self.format.w(), &mut width);
+            if !add_affixes && require_affixes {
+                continue;
+            }
+
+            // Check whether we should include grouping characters.  We need
+            // room for a complete set or we don't insert any at all.  We don't
+            // include grouping characters if decimal places were requested but
+            // they were all dropped.
+            let grouping = style.grouping.filter(|_| {
+                integer_digits > 3
+                    && (self.format.d == 0 || decimals > 0)
+                    && allocate_space((integer_digits - 1) / 3, self.format.w(), &mut width)
+            });
+
+            // Assemble number.
+            let magnitude = rounder.format(decimals as usize);
+            let mut output = SmallString::<[u8; 40]>::new();
+            for _ in width..self.format.w() {
+                output.push(' ');
+            }
+            if negative {
+                output.push_str(&style.neg_prefix.s);
+            }
+            if add_affixes {
+                output.push_str(&style.prefix.s);
+            }
+            if let Some(grouping) = grouping {
+                for (i, digit) in magnitude[..integer_digits].bytes().enumerate() {
+                    if i > 0 && (integer_digits - i) % 3 == 0 {
+                        output.push(grouping.into());
+                    }
+                    output.push(digit as char);
+                }
+            } else {
+                output.push_str(&magnitude[..integer_digits]);
+            }
+            if decimals > 0 {
+                output.push(style.decimal.into());
+                let s = &magnitude[integer_digits + 1..];
+                output.push_str(&s[..decimals as usize]);
+            }
+            if add_affixes {
+                output.push_str(&style.suffix.s);
+            }
+            if negative {
+                output.push_str(&style.neg_suffix.s);
+            } else {
+                for _ in 0..style.neg_suffix.width {
+                    output.push(' ');
+                }
+            }
+
+            debug_assert!(output.len() >= self.format.w());
+            debug_assert!(output.len() <= self.format.w() + style.extra_bytes);
+            f.write_str(&output)?;
+            return Ok(true);
+        }
+        Ok(false)
+    }
+
+    fn scientific(
+        &self,
+        f: &mut Formatter<'_>,
+        number: f64,
+        style: &NumberStyle,
+        require_affixes: bool,
+    ) -> Result<bool, FmtError> {
+        // Allocate minimum required space.
+        let mut width = 6 + style.neg_suffix.width;
+        if number < 0.0 {
+            width += style.neg_prefix.width;
+        }
+        if width > self.format.w() {
+            return Ok(false);
+        }
+
+        // Check for room for prefix and suffix.
+        let add_affixes = allocate_space(style.affix_width(), self.format.w(), &mut width);
+        if require_affixes && !add_affixes {
+            return Ok(false);
+        }
+
+        // Figure out number of characters we can use for the fraction, if any.
+        // (If that turns out to be `1`, then we'll output a decimal point
+        // without any digits following.)
+        let mut fraction_width = min(self.format.d as usize + 1, self.format.w() - width).min(16);
+        if self.format.type_ != Type::E && fraction_width == 1 {
+            fraction_width = 0;
+        }
+        width += fraction_width;
+
+        let mut output = SmallString::<[u8; 40]>::new();
+        for _ in width..self.format.w() {
+            output.push(' ');
+        }
+        if number < 0.0 {
+            output.push_str(&style.neg_prefix.s);
+        }
+        if add_affixes {
+            output.push_str(&style.prefix.s);
+        }
+        write!(
+            &mut output,
+            "{:.*E}",
+            fraction_width.saturating_sub(1),
+            number.abs()
+        )
+        .unwrap();
+        if fraction_width == 1 {
+            // Insert `.` before the `E`, to get a value like "1.E+000".
+            output.insert(output.find('E').unwrap(), '.');
+        }
+
+        // Rust always uses `.` as the decimal point. Translate to `,` if
+        // necessary.
+        if style.decimal == Decimal::Comma {
+            fix_decimal_point(&mut output);
+        }
+
+        // Make exponent have exactly three digits, plus sign.
+        let e = output.as_bytes().iter().position(|c| *c == b'E').unwrap();
+        let exponent: isize = output[e + 1..].parse().unwrap();
+        if exponent.abs() > 999 {
+            return Ok(false);
+        }
+        output.truncate(e + 1);
+        write!(&mut output, "{exponent:+04}").unwrap();
+
+        // Add suffixes.
+        if add_affixes {
+            output.push_str(&style.suffix.s);
+        }
+        if number.is_sign_negative() {
+            output.push_str(&style.neg_suffix.s);
+        } else {
+            for _ in 0..style.neg_suffix.width {
+                output.push(' ');
+            }
+        }
+
+        println!(
+            "{} for {number} width={width} fraction_width={fraction_width}: {output:?}",
+            self.format
+        );
+        debug_assert!(output.len() >= self.format.w());
+        debug_assert!(output.len() <= self.format.w() + style.extra_bytes);
+        f.write_str(&output)?;
+        Ok(true)
+    }
+
+    fn n(&self, f: &mut Formatter<'_>, number: f64) -> FmtResult {
+        if number < 0.0 {
+            return self.missing(f);
+        }
+
+        let legacy = LegacyFormat::new(number, self.format.d());
+        let w = self.format.w();
+        let len = legacy.len();
+        if len > w {
+            self.overflow(f)
+        } else {
+            write!(f, "{}{legacy}", Zeros(w.saturating_sub(len)))
+        }
+    }
+
+    fn z(&self, f: &mut Formatter<'_>, number: f64) -> FmtResult {
+        let legacy = LegacyFormat::new(number, self.format.d());
+        let w = self.format.w();
+        let len = legacy.len();
+        if len > w {
+            self.overflow(f)
+        } else {
+            let mut s = SmallString::<[u8; 40]>::new();
+            write!(&mut s, "{legacy}")?;
+            if number < 0.0 {
+                if let Some(last) = s.pop() {
+                    let last = last.to_digit(10).unwrap();
+                    s.push(b"}JKLMNOPQR"[last as usize] as char);
+                }
+            }
+            write!(f, "{}{s}", Zeros(w.saturating_sub(len)))
+        }
+    }
+
+    fn pibhex(&self, f: &mut Formatter<'_>, number: f64) -> FmtResult {
+        if number < 0.0 {
+            self.overflow(f)
+        } else {
+            let number = number.round();
+            if number >= power256(self.format.w / 2) {
+                self.overflow(f)
+            } else {
+                let binary = integer_to_binary(number as u64, self.format.w / 2);
+                output_hex(f, &binary)
+            }
+        }
+    }
+
+    fn rbhex(&self, f: &mut Formatter<'_>, number: f64) -> FmtResult {
+        let rb = self.rb(Some(number), self.format.w() / 2);
+        output_hex(f, &rb)
+    }
+
+    fn date(&self, f: &mut Formatter<'_>, number: f64) -> FmtResult {
+        const MINUTE: f64 = 60.0;
+        const HOUR: f64 = 60.0 * 60.0;
+        const DAY: f64 = 60.0 * 60.0 * 24.0;
+
+        let (date, mut time) = match self.format.type_.category() {
+            Category::Date => {
+                if number < 0.0 {
+                    return self.missing(f);
+                }
+                let Some(date) = calendar_offset_to_gregorian(number / DAY) else {
+                    return self.missing(f);
+                };
+                (date, number % DAY)
+            }
+            Category::Time => (NaiveDate::MIN, number),
+            _ => unreachable!(),
+        };
+
+        let mut output = SmallString::<[u8; 40]>::new();
+        for TemplateItem { c, n } in DateTemplate::for_format(self.format).unwrap() {
+            match c {
+                'd' if n < 3 => write!(&mut output, "{:02}", date.day()).unwrap(),
+                'd' => write!(&mut output, "{:03}", day_of_year(date).unwrap_or(1)).unwrap(),
+                'm' if n < 3 => write!(&mut output, "{:02}", date.month()).unwrap(),
+                'm' => write!(&mut output, "{}", short_month_name(date.month()).unwrap()).unwrap(),
+                'y' if n >= 4 => {
+                    let year = date.year();
+                    if year <= 9999 {
+                        write!(&mut output, "{year:04}").unwrap();
+                    } else if self.format.type_ == Type::DateTime
+                        || self.format.type_ == Type::YmdHms
+                    {
+                        write!(&mut output, "****").unwrap();
+                    } else {
+                        return self.overflow(f);
+                    }
+                }
+                'y' => {
+                    let epoch = self.settings.epoch.0;
+                    let offset = date.year() - epoch;
+                    if !(0..=99).contains(&offset) {
+                        return self.overflow(f);
+                    }
+                    write!(&mut output, "{:02}", date.year().abs() % 100).unwrap();
+                }
+                'q' => write!(&mut output, "{}", date.month0() / 3 + 1).unwrap(),
+                'w' => write!(
+                    &mut output,
+                    "{:2}",
+                    (day_of_year(date).unwrap_or(1) - 1) / 7 + 1
+                )
+                .unwrap(),
+                'D' => {
+                    if time < 0.0 {
+                        output.push('-');
+                    }
+                    time = time.abs();
+                    write!(&mut output, "{:1$.0}", (time / DAY).floor(), n).unwrap();
+                    time %= DAY;
+                }
+                'H' => {
+                    if time < 0.0 {
+                        output.push('-');
+                    }
+                    time = time.abs();
+                    write!(&mut output, "{:01$.0}", (time / HOUR).floor(), n).unwrap();
+                    time %= HOUR;
+                }
+                'M' => {
+                    if time < 0.0 {
+                        output.push('-');
+                    }
+                    time = time.abs();
+                    write!(&mut output, "{:02.0}", (time / MINUTE).floor()).unwrap();
+                    time %= MINUTE;
+
+                    let excess_width = self.format.w() as isize - output.len() as isize;
+                    if excess_width < 0 || (self.format.type_ == Type::MTime && excess_width < 3) {
+                        return self.overflow(f);
+                    }
+                    if excess_width == 3
+                        || excess_width == 4
+                        || (excess_width >= 5 && self.format.d == 0)
+                    {
+                        write!(&mut output, ":{:02.0}", time.floor()).unwrap();
+                    } else if excess_width >= 5 {
+                        let d = min(self.format.d(), excess_width as usize - 4);
+                        let w = d + 3;
+                        write!(&mut output, ":{:02$.*}", d, time, w).unwrap();
+                        if self.settings.decimal == Decimal::Comma {
+                            fix_decimal_point(&mut output);
+                        }
+                    }
+                    break;
+                }
+                c if n == 1 => output.push(c),
+                _ => unreachable!(),
+            }
+        }
+        write!(f, "{:>1$}", &output, self.format.w())
+    }
+
+    fn month(&self, f: &mut Formatter<'_>, number: f64) -> FmtResult {
+        if let Some(month) = month_name(number as u32) {
+            write!(f, "{month:.*}", self.format.w())
+        } else {
+            self.missing(f)
+        }
+    }
+
+    /// Writes this object to `w`. Writes binary formats ([Type::P],
+    /// [Type::PIB], and so on) as binary values, and writes other output
+    /// formats in the given `encoding`.
+    ///
+    /// If `dv` is a [DisplayValue], the difference between `write!(f, "{}",
+    /// dv)` and `dv.write(f, encoding)` is:
+    ///
+    /// * `write!` always outputs UTF-8. Binary formats are encoded as the
+    ///   Unicode characters corresponding to their bytes.
+    ///
+    /// * `dv.write` outputs the desired `encoding`. Binary formats are not
+    ///   encoded in `encoding` (and thus they might be invalid for the
+    ///   encoding).
+    pub fn write<W>(&self, mut w: W, encoding: &'static Encoding) -> Result<(), IoError>
+    where
+        W: IoWrite,
+    {
+        match self.to_binary() {
+            Some(binary) => w.write_all(&binary),
+            None if encoding == UTF_8 => {
+                write!(&mut w, "{}", self)
+            }
+            None => {
+                let mut temp = SmallString::<[u8; 64]>::new();
+                write!(&mut temp, "{}", self).unwrap();
+                w.write_all(&encoding.encode(&temp).0)
+            }
+        }
+    }
+
+    fn to_binary(&self) -> Option<SmallVec<[u8; 16]>> {
+        let number = self.value.as_number()?;
+        match self.format.type_() {
+            Type::P => Some(self.p(number)),
+            Type::PK => Some(self.pk(number)),
+            Type::IB => Some(self.ib(number)),
+            Type::PIB => Some(self.pib(number)),
+            Type::RB => Some(self.rb(number, self.format.w())),
+            _ => None,
+        }
+    }
+
+    fn bcd(&self, number: Option<f64>, digits: usize) -> (bool, SmallVec<[u8; 16]>) {
+        let legacy = LegacyFormat::new(number.unwrap_or_default(), self.format.d());
+        let len = legacy.len();
+
+        let mut output = SmallVec::new();
+        if len > digits {
+            output.resize(digits.div_ceil(2), 0);
+            (false, output)
+        } else {
+            let mut decimal = SmallString::<[u8; 16]>::new();
+            write!(
+                &mut decimal,
+                "{}{legacy}",
+                Zeros(digits.saturating_sub(len))
+            )
+            .unwrap();
+
+            let mut src = decimal.bytes();
+            for _ in 0..digits / 2 {
+                let d0 = src.next().unwrap() - b'0';
+                let d1 = src.next().unwrap() - b'0';
+                output.push((d0 << 4) + d1);
+            }
+            if digits % 2 != 0 {
+                let d = src.next().unwrap() - b'0';
+                output.push(d << 4);
+            }
+            (true, output)
+        }
+    }
+
+    fn p(&self, number: Option<f64>) -> SmallVec<[u8; 16]> {
+        let (valid, mut output) = self.bcd(number, self.format.w() * 2 - 1);
+        if valid && number.is_some_and(|number| number < 0.0) {
+            *output.last_mut().unwrap() |= 0xd;
+        } else {
+            *output.last_mut().unwrap() |= 0xf;
+        }
+        output
+    }
+
+    fn pk(&self, number: Option<f64>) -> SmallVec<[u8; 16]> {
+        let number = match number {
+            Some(number) if number < 0.0 => None,
+            other => other,
+        };
+        let (_valid, output) = self.bcd(number, self.format.w() * 2);
+        output
+    }
+
+    fn ib(&self, number: Option<f64>) -> SmallVec<[u8; 16]> {
+        let number = number.map_or(0.0, |number| (number * power10(self.format.d())).round());
+        let number = if number >= power256(self.format.w) / 2.0 - 1.0
+            || number < -power256(self.format.w) / 2.0
+        {
+            0.0
+        } else {
+            number
+        };
+        let integer = number.abs() as u64;
+        let integer = if number < 0.0 {
+            (-(integer as i64)) as u64
+        } else {
+            integer
+        };
+        self.endian.output.to_smallvec(integer, self.format.w())
+    }
+
+    fn pib(&self, number: Option<f64>) -> SmallVec<[u8; 16]> {
+        let number = number.map_or(0.0, |number| (number * power10(self.format.d())).round());
+        let number = if number >= power256(self.format.w) || number < 0.0 {
+            0.0
+        } else {
+            number
+        };
+        let integer = number.abs() as u64;
+        self.endian.output.to_smallvec(integer, self.format.w())
+    }
+
+    fn rb(&self, number: Option<f64>, w: usize) -> SmallVec<[u8; 16]> {
+        let number = number.unwrap_or(-f64::MAX);
+        let bytes: [u8; 8] = self.endian.output.to_bytes(number);
+        let mut vec = SmallVec::new();
+        vec.extend_from_slice(&bytes);
+        vec.resize(w, 0);
+        vec
+    }
+}
+
+struct LegacyFormat {
+    s: SmallVec<[u8; 40]>,
+    trailing_zeros: usize,
+}
+
+impl LegacyFormat {
+    fn new(number: f64, d: usize) -> Self {
+        let mut s = SmallVec::<[u8; 40]>::new();
+        write!(&mut s, "{:E}", number.abs()).unwrap();
+        debug_assert!(s.is_ascii());
+
+        // Parse exponent.
+        //
+        // Add 1 because of the transformation we will do just below, and `d` so
+        // that we just need to round to the nearest integer.
+        let e_index = s.iter().position(|c| *c == b'E').unwrap();
+        let mut exponent = unsafe { from_utf8_unchecked(&s[e_index + 1..]) }
+            .parse::<i32>()
+            .unwrap()
+            + 1
+            + d as i32;
+
+        // Transform `1.234E56` into `1234`.
+        if e_index == 1 {
+            // No decimals, e.g. `1E4` or `0E0`.
+            s.truncate(1)
+        } else {
+            s.remove(1);
+            s.truncate(e_index - 1);
+        };
+        debug_assert!(s.iter().all(|c| c.is_ascii_digit()));
+
+        if exponent >= 0 && exponent < s.len() as i32 {
+            // The first `exponent` digits are before the decimal point.  We
+            // need to round off there.
+            let exp = exponent as usize;
+
+            fn round_up(digits: &mut [u8], position: usize) -> bool {
+                for index in (0..position).rev() {
+                    match digits[index] {
+                        b'0'..=b'8' => {
+                            digits[index] += 1;
+                            return true;
+                        }
+                        b'9' => {
+                            digits[index] = b'0';
+                        }
+                        _ => unreachable!(),
+                    }
+                }
+                false
+            }
+
+            if s[exp] >= b'5' && !round_up(&mut s, exp) {
+                s.clear();
+                s.push(b'1');
+                exponent += 1;
+            }
+        }
+
+        let exponent = exponent.max(0) as usize;
+        s.truncate(exponent);
+        s.resize(exponent, b'0');
+        let trailing_zeros = exponent.saturating_sub(s.len());
+        Self { s, trailing_zeros }
+    }
+    fn s(&self) -> &str {
+        unsafe { from_utf8_unchecked(&self.s) }
+    }
+    fn len(&self) -> usize {
+        self.s.len() + self.trailing_zeros
+    }
+}
+
+impl Display for LegacyFormat {
+    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
+        write!(f, "{}{}", self.s(), Zeros(self.trailing_zeros))
+    }
+}
+
+struct Zeros(usize);
+
+impl Display for Zeros {
+    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
+        let mut n = self.0;
+        while n > 0 {
+            static ZEROS: &str = "0000000000000000000000000000000000000000";
+            let chunk = n.min(ZEROS.len());
+            f.write_str(&ZEROS[..chunk])?;
+            n -= chunk;
+        }
+        Ok(())
+    }
+}
+
+fn integer_to_binary(number: u64, width: u16) -> SmallVec<[u8; 8]> {
+    let bytes = (number << ((8 - width) * 8)).to_be_bytes();
+    SmallVec::from_slice(&bytes[..width as usize])
+}
+
+fn output_hex(f: &mut Formatter<'_>, bytes: &[u8]) -> FmtResult {
+    for byte in bytes {
+        write!(f, "{byte:02X}")?;
+    }
+    Ok(())
+}
+
+fn allocate_space(want: usize, capacity: usize, used: &mut usize) -> bool {
+    if *used + want <= capacity {
+        *used += want;
+        true
+    } else {
+        false
+    }
+}
+
+/// A representation of a number that can be quickly rounded to any desired
+/// number of decimal places (up to a specified maximum).
+#[derive(Debug)]
+struct Rounder {
+    /// Magnitude of number with excess precision.
+    string: SmallString<[u8; 40]>,
+
+    /// Number of digits before decimal point.
+    integer_digits: usize,
+
+    /// Number of `9`s or `.`s at start of string.
+    leading_nines: usize,
+
+    /// Number of `0`s or `.`s at start of string.
+    leading_zeros: usize,
+
+    /// Is the number negative?
+    negative: bool,
+}
+
+impl Rounder {
+    fn new(style: &NumberStyle, number: f64, max_decimals: u8) -> Self {
+        debug_assert!(number.abs() < 1e41);
+        debug_assert!((0..=16).contains(&max_decimals));
+
+        let mut string = SmallString::new();
+        if max_decimals == 0 {
+            // Fast path.  No rounding needed.
+            //
+            // We append `.00` to the integer representation because
+            // [Self::round_up] assumes that fractional digits are present.
+            write!(&mut string, "{:.0}.00", number.round().abs()).unwrap()
+        } else {
+            // Slow path.
+            //
+            // This is more difficult than it really should be because we have
+            // to make sure that numbers that are exactly halfway between two
+            // representations are always rounded away from zero.  This is not
+            // what format! normally does (usually it rounds to even), so we
+            // have to fake it as best we can, by formatting with extra
+            // precision and then doing the rounding ourselves.
+            //
+            // We take up to two rounds to format numbers.  In the first round,
+            // we obtain 2 digits of precision beyond those requested by the
+            // user.  If those digits are exactly "50", then in a second round
+            // we format with as many digits as are significant in a "double".
+            //
+            // It might be better to directly implement our own floating-point
+            // formatting routine instead of relying on the system's sprintf
+            // implementation.  But the classic Steele and White paper on
+            // printing floating-point numbers does not hint how to do what we
+            // want, and it's not obvious how to change their algorithms to do
+            // so.  It would also be a lot of work.
+            write!(
+                &mut string,
+                "{:.*}",
+                max_decimals as usize + 2,
+                number.abs()
+            )
+            .unwrap();
+            if string.ends_with("50") {
+                let (_sig, binary_exponent) = frexp(number);
+                let decimal_exponent = binary_exponent * 3 / 10;
+                let format_decimals = (f64::DIGITS as i32 + 1) - decimal_exponent;
+                if format_decimals > max_decimals as i32 + 2 {
+                    string.clear();
+                    write!(&mut string, "{:.*}", format_decimals as usize, number.abs()).unwrap();
+                }
+            }
+        };
+
+        if !style.leading_zero && string.starts_with("0") {
+            string.remove(0);
+        }
+        let leading_zeros = string
+            .bytes()
+            .take_while(|c| *c == b'0' || *c == b'.')
+            .count();
+        let leading_nines = string
+            .bytes()
+            .take_while(|c| *c == b'9' || *c == b'.')
+            .count();
+        let integer_digits = string.bytes().take_while(u8::is_ascii_digit).count();
+        let negative = number.is_sign_negative();
+        Self {
+            string,
+            integer_digits,
+            leading_nines,
+            leading_zeros,
+            negative,
+        }
+    }
+
+    /// Returns a [RounderWdith] for formatting the magnitude to `decimals`
+    /// decimal places. `decimals` must be in `0..=16`.
+    fn width(&self, decimals: usize) -> RounderWidth {
+        // Calculate base measures.
+        let mut width = self.integer_digits;
+        if decimals > 0 {
+            width += decimals + 1;
+        }
+        let mut integer_digits = self.integer_digits;
+        let mut negative = self.negative;
+
+        // Rounding can cause adjustments.
+        if self.should_round_up(decimals) {
+            // Rounding up leading `9s` adds a new digit (a `1`).
+            if self.leading_nines >= width {
+                width += 1;
+                integer_digits += 1;
+            }
+        } else {
+            // Rounding down.
+            if self.leading_zeros >= width {
+                // All digits that remain after rounding are zeros.  Therefore
+                // we drop the negative sign.
+                negative = false;
+                if self.integer_digits == 0 && decimals == 0 {
+                    // No digits at all are left.  We need to display
+                    // at least a single digit (a zero).
+                    debug_assert_eq!(width, 0);
+                    width += 1;
+                    integer_digits = 1;
+                }
+            }
+        }
+        RounderWidth {
+            width,
+            integer_digits,
+            negative,
+        }
+    }
+
+    /// Returns true if the number should be rounded up when chopped off at
+    /// `decimals` decimal places, false if it should be rounded down.
+    fn should_round_up(&self, decimals: usize) -> bool {
+        let digit = self.string.as_bytes()[self.integer_digits + decimals + 1];
+        debug_assert!(digit.is_ascii_digit());
+        digit >= b'5'
+    }
+
+    /// Formats the number, rounding to `decimals` decimal places.  Exactly as
+    /// many characters as indicated by [Self::width(decimals)] are written.
+    fn format(&self, decimals: usize) -> SmallString<[u8; 40]> {
+        let mut output = SmallString::new();
+        let mut base_width = self.integer_digits;
+        if decimals > 0 {
+            base_width += decimals + 1;
+        }
+
+        if self.should_round_up(decimals) {
+            if self.leading_nines < base_width {
+                // Rounding up.  This is the common case where rounding up
+                // doesn't add an extra digit.
+                output.push_str(&self.string[..base_width]);
+
+                // SAFETY: This loop only changes ASCII characters to other
+                // ASCII characters.
+                unsafe {
+                    for c in output.as_bytes_mut().iter_mut().rev() {
+                        match *c {
+                            b'9' => *c = b'0',
+                            b'0'..=b'8' => {
+                                *c += 1;
+                                break;
+                            }
+                            b'.' => (),
+                            _ => unreachable!(),
+                        }
+                    }
+                }
+            } else {
+                // Rounding up leading 9s causes the result to be a 1 followed
+                // by a number of 0s, plus a decimal point.
+                output.push('1');
+                for _ in 0..self.integer_digits {
+                    output.push('0');
+                }
+                if decimals > 0 {
+                    output.push('.');
+                    for _ in 0..decimals {
+                        output.push('0');
+                    }
+                }
+                debug_assert_eq!(output.len(), base_width + 1);
+            }
+        } else {
+            // Rounding down.
+            if self.integer_digits != 0 || decimals != 0 {
+                // Common case: just copy the digits.
+                output.push_str(&self.string);
+            } else {
+                // No digits remain.  The output is just a zero.
+                output.push('0');
+            }
+        }
+        output
+    }
+}
+
+struct RounderWidth {
+    /// Number of characters required to format the number to a specified number
+    /// of decimal places.  This includes integer digits and a decimal point and
+    /// fractional digits, if any, but it does not include any negative prefix
+    /// or suffix or other affixes.
+    width: usize,
+
+    /// Number of digits before the decimal point, between 0 and 40.
+    integer_digits: usize,
+
+    /// True if the number is negative and its rounded representation would
+    /// include at least one nonzero digit.
+    negative: bool,
+}
+
+/// Returns `10^x`.
+fn power10(x: usize) -> f64 {
+    const POWERS: [f64; 41] = [
+        1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16,
+        1e17, 1e18, 1e19, 1e20, 1e21, 1e22, 1e23, 1e24, 1e25, 1e26, 1e27, 1e28, 1e29, 1e30, 1e31,
+        1e32, 1e33, 1e34, 1e35, 1e36, 1e37, 1e38, 1e39, 1e40,
+    ];
+    POWERS
+        .get(x)
+        .copied()
+        .unwrap_or_else(|| 10.0_f64.powi(x as i32))
+}
+
+/// Returns `256^x`.
+fn power256(x: u16) -> f64 {
+    const POWERS: [f64; 9] = [
+        1.0,
+        256.0,
+        65536.0,
+        16777216.0,
+        4294967296.0,
+        1099511627776.0,
+        281474976710656.0,
+        72057594037927936.0,
+        18446744073709551616.0,
+    ];
+    POWERS
+        .get(x as usize)
+        .copied()
+        .unwrap_or_else(|| 256.0_f64.powi(x as i32))
+}
+
+fn fix_decimal_point<A>(s: &mut SmallString<A>)
+where
+    A: Array<Item = u8>,
+{
+    // SAFETY: This only changes only one ASCII character (`.`) to
+    // another ASCII character (`,`).
+    unsafe {
+        if let Some(dot) = s.as_bytes_mut().iter_mut().find(|c| **c == b'.') {
+            *dot = b',';
+        }
+    }
+}
diff --git a/rust/pspp/src/format/display/test.rs b/rust/pspp/src/format/display/test.rs
new file mode 100644 (file)
index 0000000..cae7648
--- /dev/null
@@ -0,0 +1,1749 @@
+use std::{fmt::Write, fs::File, io::BufRead, path::Path};
+
+use binrw::io::BufReader;
+use encoding_rs::UTF_8;
+use itertools::Itertools;
+use smallstr::SmallString;
+use smallvec::SmallVec;
+
+use crate::{
+    dictionary::Value,
+    endian::Endian,
+    format::{AbstractFormat, Epoch, Format, Settings, Type, UncheckedFormat, CC},
+    lex::{
+        scan::StringScanner,
+        segment::Syntax,
+        token::{Punct, Token},
+    },
+    settings::EndianSettings,
+};
+
+fn test(name: &str) {
+    let filename = Path::new(env!("CARGO_MANIFEST_DIR"))
+        .join("src/format/testdata/display")
+        .join(name);
+    let input = BufReader::new(File::open(&filename).unwrap());
+    let settings = Settings::default()
+        .with_cc(CC::A, ",,,".parse().unwrap())
+        .with_cc(CC::B, "-,[[[,]]],-".parse().unwrap())
+        .with_cc(CC::C, "((,[,],))".parse().unwrap())
+        .with_cc(CC::D, ",XXX,,-".parse().unwrap())
+        .with_cc(CC::E, ",,YYY,-".parse().unwrap());
+    let endian = EndianSettings::new(Endian::Big);
+    let mut value = Some(0.0);
+    let mut value_name = String::new();
+    for (line, line_number) in input.lines().map(|r| r.unwrap()).zip(1..) {
+        let line = line.trim();
+        let tokens = StringScanner::new(line, Syntax::Interactive, true)
+            .unwrapped()
+            .collect::<Vec<_>>();
+        match &tokens[0] {
+            Token::Number(number) => {
+                value = if let Some(Token::Punct(Punct::Exp)) = tokens.get(1) {
+                    assert_eq!(tokens.len(), 3);
+                    let exponent = tokens[2].as_number().unwrap();
+                    Some(number.powf(exponent))
+                } else {
+                    assert_eq!(tokens.len(), 1);
+                    Some(*number)
+                };
+                value_name = String::from(line);
+            }
+            Token::End => {
+                value = None;
+                value_name = String::from(line);
+            }
+            Token::Id(id) => {
+                let format: UncheckedFormat =
+                    id.0.as_str()
+                        .parse::<AbstractFormat>()
+                        .unwrap()
+                        .try_into()
+                        .unwrap();
+                let format: Format = format.try_into().unwrap();
+                assert_eq!(tokens.get(1), Some(&Token::Punct(Punct::Colon)));
+                let expected = tokens[2].as_string().unwrap();
+                let actual = Value::Number(value)
+                    .display(format, UTF_8)
+                    .with_settings(&settings)
+                    .with_endian(endian)
+                    .to_string();
+                assert_eq!(
+                    expected,
+                    &actual,
+                    "{}:{line_number}: Error formatting {value_name} as {format}",
+                    filename.display()
+                );
+            }
+            _ => panic!(),
+        }
+    }
+}
+
+#[test]
+fn comma() {
+    test("comma.txt");
+}
+
+#[test]
+fn dot() {
+    test("dot.txt");
+}
+
+#[test]
+fn dollar() {
+    test("dollar.txt");
+}
+
+#[test]
+fn pct() {
+    test("pct.txt");
+}
+
+#[test]
+fn e() {
+    test("e.txt");
+}
+
+#[test]
+fn f() {
+    test("f.txt");
+}
+
+#[test]
+fn n() {
+    test("n.txt");
+}
+
+#[test]
+fn z() {
+    test("z.txt");
+}
+
+#[test]
+fn cca() {
+    test("cca.txt");
+}
+
+#[test]
+fn ccb() {
+    test("ccb.txt");
+}
+
+#[test]
+fn ccc() {
+    test("ccc.txt");
+}
+
+#[test]
+fn ccd() {
+    test("ccd.txt");
+}
+
+#[test]
+fn cce() {
+    test("cce.txt");
+}
+
+#[test]
+fn pibhex() {
+    test("pibhex.txt");
+}
+
+#[test]
+fn rbhex() {
+    test("rbhex.txt");
+}
+
+#[test]
+fn leading_zeros() {
+    struct Test {
+        with_leading_zero: Settings,
+        without_leading_zero: Settings,
+    }
+
+    impl Test {
+        fn new() -> Self {
+            Self {
+                without_leading_zero: Settings::default(),
+                with_leading_zero: Settings::default().with_leading_zero(true),
+            }
+        }
+
+        fn test_with_settings(value: f64, expected: [&str; 2], settings: &Settings) {
+            let value = Value::from(value);
+            for (expected, d) in expected.into_iter().zip([2, 1].into_iter()) {
+                assert_eq!(
+                    &value
+                        .display(Format::new(Type::F, 5, d).unwrap(), UTF_8)
+                        .with_settings(settings)
+                        .to_string(),
+                    expected
+                );
+            }
+        }
+        fn test(&self, value: f64, without: [&str; 2], with: [&str; 2]) {
+            Self::test_with_settings(value, without, &self.without_leading_zero);
+            Self::test_with_settings(value, with, &self.with_leading_zero);
+        }
+    }
+    let test = Test::new();
+    test.test(0.5, ["  .50", "   .5"], [" 0.50", "  0.5"]);
+    test.test(0.99, ["  .99", "  1.0"], [" 0.99", "  1.0"]);
+    test.test(0.01, ["  .01", "   .0"], [" 0.01", "  0.0"]);
+    test.test(0.0, ["  .00", "   .0"], [" 0.00", "  0.0"]);
+    test.test(-0.0, ["  .00", "   .0"], [" 0.00", "  0.0"]);
+    test.test(-0.5, [" -.50", "  -.5"], ["-0.50", " -0.5"]);
+    test.test(-0.99, [" -.99", " -1.0"], ["-0.99", " -1.0"]);
+    test.test(-0.01, [" -.01", "   .0"], ["-0.01", "  0.0"]);
+}
+
+#[test]
+fn non_ascii_cc() {
+    fn test(settings: &Settings, value: f64, expected: &str) {
+        assert_eq!(
+            &Value::from(value)
+                .display(Format::new(Type::CC(CC::A), 10, 2).unwrap(), UTF_8)
+                .with_settings(settings)
+                .to_string(),
+            expected
+        );
+    }
+
+    let settings = Settings::default().with_cc(CC::A, "«,¥,€,»".parse().unwrap());
+    test(&settings, 1.0, "   ¥1.00€ ");
+    test(&settings, -1.0, "  «¥1.00€»");
+    test(&settings, 1.5, "   ¥1.50€ ");
+    test(&settings, -1.5, "  «¥1.50€»");
+    test(&settings, 0.75, "    ¥.75€ ");
+    test(&settings, 1.5e10, " ¥2E+010€ ");
+    test(&settings, -1.5e10, "«¥2E+010€»");
+}
+
+fn test_binhex(name: &str) {
+    let filename = Path::new(env!("CARGO_MANIFEST_DIR"))
+        .join("src/format/testdata/display")
+        .join(name);
+    let input = BufReader::new(File::open(&filename).unwrap());
+    let mut value = None;
+    let mut value_name = String::new();
+
+    let endian = EndianSettings::new(Endian::Big);
+    for (line, line_number) in input.lines().map(|r| r.unwrap()).zip(1..) {
+        let line = line.trim();
+        let tokens = StringScanner::new(line, Syntax::Interactive, true)
+            .unwrapped()
+            .collect::<Vec<_>>();
+        match &tokens[0] {
+            Token::Number(number) => {
+                value = Some(*number);
+                value_name = String::from(line);
+            }
+            Token::End => {
+                value = None;
+                value_name = String::from(line);
+            }
+            Token::Id(id) => {
+                let format: UncheckedFormat =
+                    id.0.as_str()
+                        .parse::<AbstractFormat>()
+                        .unwrap()
+                        .try_into()
+                        .unwrap();
+                let format: Format = format.try_into().unwrap();
+                assert_eq!(tokens.get(1), Some(&Token::Punct(Punct::Colon)));
+                let expected = tokens[2].as_string().unwrap();
+                let mut actual = SmallVec::<[u8; 16]>::new();
+                Value::Number(value)
+                    .display(format, UTF_8)
+                    .with_endian(endian)
+                    .write(&mut actual, UTF_8)
+                    .unwrap();
+                let mut actual_s = SmallString::<[u8; 32]>::new();
+                for b in actual {
+                    write!(&mut actual_s, "{:02x}", b).unwrap();
+                }
+                assert_eq!(
+                    expected,
+                    &*actual_s,
+                    "{}:{line_number}: Error formatting {value_name} as {format}",
+                    filename.display()
+                );
+            }
+            _ => panic!(),
+        }
+    }
+}
+
+#[test]
+fn p() {
+    test_binhex("p.txt");
+}
+
+#[test]
+fn pk() {
+    test_binhex("pk.txt");
+}
+
+#[test]
+fn ib() {
+    test_binhex("ib.txt");
+}
+
+#[test]
+fn pib() {
+    test_binhex("pib.txt");
+}
+
+#[test]
+fn rb() {
+    test_binhex("rb.txt");
+}
+
+fn test_dates(format: Format, expect: &[&str]) {
+    let settings = Settings::default().with_epoch(Epoch(1930));
+    let parser = Type::DateTime.parser(UTF_8).with_settings(&settings);
+    static INPUTS: &[&str; 20] = &[
+        "10-6-1648 0:0:0",
+        "30-6-1680 4:50:38.12301",
+        "24-7-1716 12:31:35.23453",
+        "19-6-1768 12:47:53.34505",
+        "2-8-1819 1:26:0.45615",
+        "27-3-1839 20:58:11.56677",
+        "19-4-1903 7:36:5.18964",
+        "25-8-1929 15:43:49.83132",
+        "29-9-1941 4:25:9.01293",
+        "19-4-1943 6:49:27.52375",
+        "7-10-1943 2:57:52.01565",
+        "17-3-1992 16:45:44.86529",
+        "25-2-1996 21:30:57.82047",
+        "29-9-41 4:25:9.15395",
+        "19-4-43 6:49:27.10533",
+        "7-10-43 2:57:52.48229",
+        "17-3-92 16:45:44.65827",
+        "25-2-96 21:30:57.58219",
+        "10-11-2038 22:30:4.18347",
+        "18-7-2094 1:56:51.59319",
+    ];
+    assert_eq!(expect.len(), INPUTS.len());
+    for (input, expect) in INPUTS.iter().copied().zip_eq(expect.iter().copied()) {
+        let value = parser.parse(input).unwrap();
+        let formatted = value
+            .display(format, UTF_8)
+            .with_settings(&settings)
+            .to_string();
+        assert_eq!(&formatted, expect);
+    }
+}
+
+#[test]
+fn date9() {
+    test_dates(
+        Format::new(Type::Date, 9, 0).unwrap(),
+        &[
+            "*********",
+            "*********",
+            "*********",
+            "*********",
+            "*********",
+            "*********",
+            "*********",
+            "*********",
+            "29-SEP-41",
+            "19-APR-43",
+            "07-OCT-43",
+            "17-MAR-92",
+            "25-FEB-96",
+            "29-SEP-41",
+            "19-APR-43",
+            "07-OCT-43",
+            "17-MAR-92",
+            "25-FEB-96",
+            "*********",
+            "*********",
+        ],
+    );
+}
+
+#[test]
+fn date11() {
+    test_dates(
+        Format::new(Type::Date, 11, 0).unwrap(),
+        &[
+            "10-JUN-1648",
+            "30-JUN-1680",
+            "24-JUL-1716",
+            "19-JUN-1768",
+            "02-AUG-1819",
+            "27-MAR-1839",
+            "19-APR-1903",
+            "25-AUG-1929",
+            "29-SEP-1941",
+            "19-APR-1943",
+            "07-OCT-1943",
+            "17-MAR-1992",
+            "25-FEB-1996",
+            "29-SEP-1941",
+            "19-APR-1943",
+            "07-OCT-1943",
+            "17-MAR-1992",
+            "25-FEB-1996",
+            "10-NOV-2038",
+            "18-JUL-2094",
+        ],
+    );
+}
+
+#[test]
+fn adate8() {
+    test_dates(
+        Format::new(Type::ADate, 8, 0).unwrap(),
+        &[
+            "********", "********", "********", "********", "********", "********", "********",
+            "********", "09/29/41", "04/19/43", "10/07/43", "03/17/92", "02/25/96", "09/29/41",
+            "04/19/43", "10/07/43", "03/17/92", "02/25/96", "********", "********",
+        ],
+    );
+}
+
+#[test]
+fn adate10() {
+    test_dates(
+        Format::new(Type::ADate, 10, 0).unwrap(),
+        &[
+            "06/10/1648",
+            "06/30/1680",
+            "07/24/1716",
+            "06/19/1768",
+            "08/02/1819",
+            "03/27/1839",
+            "04/19/1903",
+            "08/25/1929",
+            "09/29/1941",
+            "04/19/1943",
+            "10/07/1943",
+            "03/17/1992",
+            "02/25/1996",
+            "09/29/1941",
+            "04/19/1943",
+            "10/07/1943",
+            "03/17/1992",
+            "02/25/1996",
+            "11/10/2038",
+            "07/18/2094",
+        ],
+    );
+}
+
+#[test]
+fn edate8() {
+    test_dates(
+        Format::new(Type::EDate, 8, 0).unwrap(),
+        &[
+            "********", "********", "********", "********", "********", "********", "********",
+            "********", "29.09.41", "19.04.43", "07.10.43", "17.03.92", "25.02.96", "29.09.41",
+            "19.04.43", "07.10.43", "17.03.92", "25.02.96", "********", "********",
+        ],
+    );
+}
+
+#[test]
+fn edate10() {
+    test_dates(
+        Format::new(Type::EDate, 10, 0).unwrap(),
+        &[
+            "10.06.1648",
+            "30.06.1680",
+            "24.07.1716",
+            "19.06.1768",
+            "02.08.1819",
+            "27.03.1839",
+            "19.04.1903",
+            "25.08.1929",
+            "29.09.1941",
+            "19.04.1943",
+            "07.10.1943",
+            "17.03.1992",
+            "25.02.1996",
+            "29.09.1941",
+            "19.04.1943",
+            "07.10.1943",
+            "17.03.1992",
+            "25.02.1996",
+            "10.11.2038",
+            "18.07.2094",
+        ],
+    );
+}
+
+#[test]
+fn jdate5() {
+    test_dates(
+        Format::new(Type::JDate, 5, 0).unwrap(),
+        &[
+            "*****", "*****", "*****", "*****", "*****", "*****", "*****", "*****", "41272",
+            "43109", "43280", "92077", "96056", "41272", "43109", "43280", "92077", "96056",
+            "*****", "*****",
+        ],
+    );
+}
+
+#[test]
+fn jdate7() {
+    test_dates(
+        Format::new(Type::JDate, 7, 0).unwrap(),
+        &[
+            "1648162", "1680182", "1716206", "1768171", "1819214", "1839086", "1903109", "1929237",
+            "1941272", "1943109", "1943280", "1992077", "1996056", "1941272", "1943109", "1943280",
+            "1992077", "1996056", "2038314", "2094199",
+        ],
+    );
+}
+
+#[test]
+fn sdate8() {
+    test_dates(
+        Format::new(Type::SDate, 8, 0).unwrap(),
+        &[
+            "********", "********", "********", "********", "********", "********", "********",
+            "********", "41/09/29", "43/04/19", "43/10/07", "92/03/17", "96/02/25", "41/09/29",
+            "43/04/19", "43/10/07", "92/03/17", "96/02/25", "********", "********",
+        ],
+    );
+}
+
+#[test]
+fn sdate10() {
+    test_dates(
+        Format::new(Type::SDate, 10, 0).unwrap(),
+        &[
+            "1648/06/10",
+            "1680/06/30",
+            "1716/07/24",
+            "1768/06/19",
+            "1819/08/02",
+            "1839/03/27",
+            "1903/04/19",
+            "1929/08/25",
+            "1941/09/29",
+            "1943/04/19",
+            "1943/10/07",
+            "1992/03/17",
+            "1996/02/25",
+            "1941/09/29",
+            "1943/04/19",
+            "1943/10/07",
+            "1992/03/17",
+            "1996/02/25",
+            "2038/11/10",
+            "2094/07/18",
+        ],
+    );
+}
+
+#[test]
+fn qyr6() {
+    test_dates(
+        Format::new(Type::QYr, 6, 0).unwrap(),
+        &[
+            "******", "******", "******", "******", "******", "******", "******", "******",
+            "3 Q 41", "2 Q 43", "4 Q 43", "1 Q 92", "1 Q 96", "3 Q 41", "2 Q 43", "4 Q 43",
+            "1 Q 92", "1 Q 96", "******", "******",
+        ],
+    );
+}
+
+#[test]
+fn qyr8() {
+    test_dates(
+        Format::new(Type::QYr, 8, 0).unwrap(),
+        &[
+            "2 Q 1648", "2 Q 1680", "3 Q 1716", "2 Q 1768", "3 Q 1819", "1 Q 1839", "2 Q 1903",
+            "3 Q 1929", "3 Q 1941", "2 Q 1943", "4 Q 1943", "1 Q 1992", "1 Q 1996", "3 Q 1941",
+            "2 Q 1943", "4 Q 1943", "1 Q 1992", "1 Q 1996", "4 Q 2038", "3 Q 2094",
+        ],
+    );
+}
+
+#[test]
+fn moyr6() {
+    test_dates(
+        Format::new(Type::MoYr, 6, 0).unwrap(),
+        &[
+            "******", "******", "******", "******", "******", "******", "******", "******",
+            "SEP 41", "APR 43", "OCT 43", "MAR 92", "FEB 96", "SEP 41", "APR 43", "OCT 43",
+            "MAR 92", "FEB 96", "******", "******",
+        ],
+    );
+}
+
+#[test]
+fn moyr8() {
+    test_dates(
+        Format::new(Type::MoYr, 8, 0).unwrap(),
+        &[
+            "JUN 1648", "JUN 1680", "JUL 1716", "JUN 1768", "AUG 1819", "MAR 1839", "APR 1903",
+            "AUG 1929", "SEP 1941", "APR 1943", "OCT 1943", "MAR 1992", "FEB 1996", "SEP 1941",
+            "APR 1943", "OCT 1943", "MAR 1992", "FEB 1996", "NOV 2038", "JUL 2094",
+        ],
+    );
+}
+
+#[test]
+fn wkyr8() {
+    test_dates(
+        Format::new(Type::WkYr, 8, 0).unwrap(),
+        &[
+            "********", "********", "********", "********", "********", "********", "********",
+            "********", "39 WK 41", "16 WK 43", "40 WK 43", "11 WK 92", " 8 WK 96", "39 WK 41",
+            "16 WK 43", "40 WK 43", "11 WK 92", " 8 WK 96", "********", "********",
+        ],
+    );
+}
+
+#[test]
+fn wkyr10() {
+    test_dates(
+        Format::new(Type::WkYr, 10, 0).unwrap(),
+        &[
+            "24 WK 1648",
+            "26 WK 1680",
+            "30 WK 1716",
+            "25 WK 1768",
+            "31 WK 1819",
+            "13 WK 1839",
+            "16 WK 1903",
+            "34 WK 1929",
+            "39 WK 1941",
+            "16 WK 1943",
+            "40 WK 1943",
+            "11 WK 1992",
+            " 8 WK 1996",
+            "39 WK 1941",
+            "16 WK 1943",
+            "40 WK 1943",
+            "11 WK 1992",
+            " 8 WK 1996",
+            "45 WK 2038",
+            "29 WK 2094",
+        ],
+    );
+}
+
+#[test]
+fn datetime17() {
+    test_dates(
+        Format::new(Type::DateTime, 17, 0).unwrap(),
+        &[
+            "10-JUN-1648 00:00",
+            "30-JUN-1680 04:50",
+            "24-JUL-1716 12:31",
+            "19-JUN-1768 12:47",
+            "02-AUG-1819 01:26",
+            "27-MAR-1839 20:58",
+            "19-APR-1903 07:36",
+            "25-AUG-1929 15:43",
+            "29-SEP-1941 04:25",
+            "19-APR-1943 06:49",
+            "07-OCT-1943 02:57",
+            "17-MAR-1992 16:45",
+            "25-FEB-1996 21:30",
+            "29-SEP-1941 04:25",
+            "19-APR-1943 06:49",
+            "07-OCT-1943 02:57",
+            "17-MAR-1992 16:45",
+            "25-FEB-1996 21:30",
+            "10-NOV-2038 22:30",
+            "18-JUL-2094 01:56",
+        ],
+    );
+}
+
+#[test]
+fn datetime18() {
+    test_dates(
+        Format::new(Type::DateTime, 18, 0).unwrap(),
+        &[
+            " 10-JUN-1648 00:00",
+            " 30-JUN-1680 04:50",
+            " 24-JUL-1716 12:31",
+            " 19-JUN-1768 12:47",
+            " 02-AUG-1819 01:26",
+            " 27-MAR-1839 20:58",
+            " 19-APR-1903 07:36",
+            " 25-AUG-1929 15:43",
+            " 29-SEP-1941 04:25",
+            " 19-APR-1943 06:49",
+            " 07-OCT-1943 02:57",
+            " 17-MAR-1992 16:45",
+            " 25-FEB-1996 21:30",
+            " 29-SEP-1941 04:25",
+            " 19-APR-1943 06:49",
+            " 07-OCT-1943 02:57",
+            " 17-MAR-1992 16:45",
+            " 25-FEB-1996 21:30",
+            " 10-NOV-2038 22:30",
+            " 18-JUL-2094 01:56",
+        ],
+    );
+}
+
+#[test]
+fn datetime19() {
+    test_dates(
+        Format::new(Type::DateTime, 19, 0).unwrap(),
+        &[
+            "  10-JUN-1648 00:00",
+            "  30-JUN-1680 04:50",
+            "  24-JUL-1716 12:31",
+            "  19-JUN-1768 12:47",
+            "  02-AUG-1819 01:26",
+            "  27-MAR-1839 20:58",
+            "  19-APR-1903 07:36",
+            "  25-AUG-1929 15:43",
+            "  29-SEP-1941 04:25",
+            "  19-APR-1943 06:49",
+            "  07-OCT-1943 02:57",
+            "  17-MAR-1992 16:45",
+            "  25-FEB-1996 21:30",
+            "  29-SEP-1941 04:25",
+            "  19-APR-1943 06:49",
+            "  07-OCT-1943 02:57",
+            "  17-MAR-1992 16:45",
+            "  25-FEB-1996 21:30",
+            "  10-NOV-2038 22:30",
+            "  18-JUL-2094 01:56",
+        ],
+    );
+}
+
+#[test]
+fn datetime20() {
+    test_dates(
+        Format::new(Type::DateTime, 20, 0).unwrap(),
+        &[
+            "10-JUN-1648 00:00:00",
+            "30-JUN-1680 04:50:38",
+            "24-JUL-1716 12:31:35",
+            "19-JUN-1768 12:47:53",
+            "02-AUG-1819 01:26:00",
+            "27-MAR-1839 20:58:11",
+            "19-APR-1903 07:36:05",
+            "25-AUG-1929 15:43:49",
+            "29-SEP-1941 04:25:09",
+            "19-APR-1943 06:49:27",
+            "07-OCT-1943 02:57:52",
+            "17-MAR-1992 16:45:44",
+            "25-FEB-1996 21:30:57",
+            "29-SEP-1941 04:25:09",
+            "19-APR-1943 06:49:27",
+            "07-OCT-1943 02:57:52",
+            "17-MAR-1992 16:45:44",
+            "25-FEB-1996 21:30:57",
+            "10-NOV-2038 22:30:04",
+            "18-JUL-2094 01:56:51",
+        ],
+    );
+}
+
+#[test]
+fn datetime21() {
+    test_dates(
+        Format::new(Type::DateTime, 21, 0).unwrap(),
+        &[
+            " 10-JUN-1648 00:00:00",
+            " 30-JUN-1680 04:50:38",
+            " 24-JUL-1716 12:31:35",
+            " 19-JUN-1768 12:47:53",
+            " 02-AUG-1819 01:26:00",
+            " 27-MAR-1839 20:58:11",
+            " 19-APR-1903 07:36:05",
+            " 25-AUG-1929 15:43:49",
+            " 29-SEP-1941 04:25:09",
+            " 19-APR-1943 06:49:27",
+            " 07-OCT-1943 02:57:52",
+            " 17-MAR-1992 16:45:44",
+            " 25-FEB-1996 21:30:57",
+            " 29-SEP-1941 04:25:09",
+            " 19-APR-1943 06:49:27",
+            " 07-OCT-1943 02:57:52",
+            " 17-MAR-1992 16:45:44",
+            " 25-FEB-1996 21:30:57",
+            " 10-NOV-2038 22:30:04",
+            " 18-JUL-2094 01:56:51",
+        ],
+    );
+}
+
+#[test]
+fn datetime22() {
+    test_dates(
+        Format::new(Type::DateTime, 22, 0).unwrap(),
+        &[
+            "  10-JUN-1648 00:00:00",
+            "  30-JUN-1680 04:50:38",
+            "  24-JUL-1716 12:31:35",
+            "  19-JUN-1768 12:47:53",
+            "  02-AUG-1819 01:26:00",
+            "  27-MAR-1839 20:58:11",
+            "  19-APR-1903 07:36:05",
+            "  25-AUG-1929 15:43:49",
+            "  29-SEP-1941 04:25:09",
+            "  19-APR-1943 06:49:27",
+            "  07-OCT-1943 02:57:52",
+            "  17-MAR-1992 16:45:44",
+            "  25-FEB-1996 21:30:57",
+            "  29-SEP-1941 04:25:09",
+            "  19-APR-1943 06:49:27",
+            "  07-OCT-1943 02:57:52",
+            "  17-MAR-1992 16:45:44",
+            "  25-FEB-1996 21:30:57",
+            "  10-NOV-2038 22:30:04",
+            "  18-JUL-2094 01:56:51",
+        ],
+    );
+}
+
+#[test]
+fn datetime22_1() {
+    test_dates(
+        Format::new(Type::DateTime, 22, 1).unwrap(),
+        &[
+            "10-JUN-1648 00:00:00.0",
+            "30-JUN-1680 04:50:38.1",
+            "24-JUL-1716 12:31:35.2",
+            "19-JUN-1768 12:47:53.3",
+            "02-AUG-1819 01:26:00.5",
+            "27-MAR-1839 20:58:11.6",
+            "19-APR-1903 07:36:05.2",
+            "25-AUG-1929 15:43:49.8",
+            "29-SEP-1941 04:25:09.0",
+            "19-APR-1943 06:49:27.5",
+            "07-OCT-1943 02:57:52.0",
+            "17-MAR-1992 16:45:44.9",
+            "25-FEB-1996 21:30:57.8",
+            "29-SEP-1941 04:25:09.2",
+            "19-APR-1943 06:49:27.1",
+            "07-OCT-1943 02:57:52.5",
+            "17-MAR-1992 16:45:44.7",
+            "25-FEB-1996 21:30:57.6",
+            "10-NOV-2038 22:30:04.2",
+            "18-JUL-2094 01:56:51.6",
+        ],
+    );
+}
+
+#[test]
+fn datetime23_2() {
+    test_dates(
+        Format::new(Type::DateTime, 23, 2).unwrap(),
+        &[
+            "10-JUN-1648 00:00:00.00",
+            "30-JUN-1680 04:50:38.12",
+            "24-JUL-1716 12:31:35.23",
+            "19-JUN-1768 12:47:53.35",
+            "02-AUG-1819 01:26:00.46",
+            "27-MAR-1839 20:58:11.57",
+            "19-APR-1903 07:36:05.19",
+            "25-AUG-1929 15:43:49.83",
+            "29-SEP-1941 04:25:09.01",
+            "19-APR-1943 06:49:27.52",
+            "07-OCT-1943 02:57:52.02",
+            "17-MAR-1992 16:45:44.87",
+            "25-FEB-1996 21:30:57.82",
+            "29-SEP-1941 04:25:09.15",
+            "19-APR-1943 06:49:27.11",
+            "07-OCT-1943 02:57:52.48",
+            "17-MAR-1992 16:45:44.66",
+            "25-FEB-1996 21:30:57.58",
+            "10-NOV-2038 22:30:04.18",
+            "18-JUL-2094 01:56:51.59",
+        ],
+    );
+}
+
+#[test]
+fn datetime24_3() {
+    test_dates(
+        Format::new(Type::DateTime, 24, 3).unwrap(),
+        &[
+            "10-JUN-1648 00:00:00.000",
+            "30-JUN-1680 04:50:38.123",
+            "24-JUL-1716 12:31:35.235",
+            "19-JUN-1768 12:47:53.345",
+            "02-AUG-1819 01:26:00.456",
+            "27-MAR-1839 20:58:11.567",
+            "19-APR-1903 07:36:05.190",
+            "25-AUG-1929 15:43:49.831",
+            "29-SEP-1941 04:25:09.013",
+            "19-APR-1943 06:49:27.524",
+            "07-OCT-1943 02:57:52.016",
+            "17-MAR-1992 16:45:44.865",
+            "25-FEB-1996 21:30:57.820",
+            "29-SEP-1941 04:25:09.154",
+            "19-APR-1943 06:49:27.105",
+            "07-OCT-1943 02:57:52.482",
+            "17-MAR-1992 16:45:44.658",
+            "25-FEB-1996 21:30:57.582",
+            "10-NOV-2038 22:30:04.183",
+            "18-JUL-2094 01:56:51.593",
+        ],
+    );
+}
+
+#[test]
+fn datetime25_4() {
+    test_dates(
+        Format::new(Type::DateTime, 25, 4).unwrap(),
+        &[
+            "10-JUN-1648 00:00:00.0000",
+            "30-JUN-1680 04:50:38.1230",
+            "24-JUL-1716 12:31:35.2345",
+            "19-JUN-1768 12:47:53.3450",
+            "02-AUG-1819 01:26:00.4562",
+            "27-MAR-1839 20:58:11.5668",
+            "19-APR-1903 07:36:05.1896",
+            "25-AUG-1929 15:43:49.8313",
+            "29-SEP-1941 04:25:09.0129",
+            "19-APR-1943 06:49:27.5238",
+            "07-OCT-1943 02:57:52.0156",
+            "17-MAR-1992 16:45:44.8653",
+            "25-FEB-1996 21:30:57.8205",
+            "29-SEP-1941 04:25:09.1539",
+            "19-APR-1943 06:49:27.1053",
+            "07-OCT-1943 02:57:52.4823",
+            "17-MAR-1992 16:45:44.6583",
+            "25-FEB-1996 21:30:57.5822",
+            "10-NOV-2038 22:30:04.1835",
+            "18-JUL-2094 01:56:51.5932",
+        ],
+    );
+}
+
+#[test]
+fn datetime26_5() {
+    test_dates(
+        Format::new(Type::DateTime, 26, 5).unwrap(),
+        &[
+            "10-JUN-1648 00:00:00.00000",
+            "30-JUN-1680 04:50:38.12301",
+            "24-JUL-1716 12:31:35.23453",
+            "19-JUN-1768 12:47:53.34505",
+            "02-AUG-1819 01:26:00.45615",
+            "27-MAR-1839 20:58:11.56677",
+            "19-APR-1903 07:36:05.18964",
+            "25-AUG-1929 15:43:49.83132",
+            "29-SEP-1941 04:25:09.01293",
+            "19-APR-1943 06:49:27.52375",
+            "07-OCT-1943 02:57:52.01565",
+            "17-MAR-1992 16:45:44.86529",
+            "25-FEB-1996 21:30:57.82047",
+            "29-SEP-1941 04:25:09.15395",
+            "19-APR-1943 06:49:27.10533",
+            "07-OCT-1943 02:57:52.48229",
+            "17-MAR-1992 16:45:44.65827",
+            "25-FEB-1996 21:30:57.58219",
+            "10-NOV-2038 22:30:04.18347",
+            "18-JUL-2094 01:56:51.59319",
+        ],
+    );
+}
+
+#[test]
+fn ymdhms16() {
+    test_dates(
+        Format::new(Type::YmdHms, 16, 0).unwrap(),
+        &[
+            "1648-06-10 00:00",
+            "1680-06-30 04:50",
+            "1716-07-24 12:31",
+            "1768-06-19 12:47",
+            "1819-08-02 01:26",
+            "1839-03-27 20:58",
+            "1903-04-19 07:36",
+            "1929-08-25 15:43",
+            "1941-09-29 04:25",
+            "1943-04-19 06:49",
+            "1943-10-07 02:57",
+            "1992-03-17 16:45",
+            "1996-02-25 21:30",
+            "1941-09-29 04:25",
+            "1943-04-19 06:49",
+            "1943-10-07 02:57",
+            "1992-03-17 16:45",
+            "1996-02-25 21:30",
+            "2038-11-10 22:30",
+            "2094-07-18 01:56",
+        ],
+    );
+}
+
+#[test]
+fn ymdhms17() {
+    test_dates(
+        Format::new(Type::YmdHms, 17, 0).unwrap(),
+        &[
+            " 1648-06-10 00:00",
+            " 1680-06-30 04:50",
+            " 1716-07-24 12:31",
+            " 1768-06-19 12:47",
+            " 1819-08-02 01:26",
+            " 1839-03-27 20:58",
+            " 1903-04-19 07:36",
+            " 1929-08-25 15:43",
+            " 1941-09-29 04:25",
+            " 1943-04-19 06:49",
+            " 1943-10-07 02:57",
+            " 1992-03-17 16:45",
+            " 1996-02-25 21:30",
+            " 1941-09-29 04:25",
+            " 1943-04-19 06:49",
+            " 1943-10-07 02:57",
+            " 1992-03-17 16:45",
+            " 1996-02-25 21:30",
+            " 2038-11-10 22:30",
+            " 2094-07-18 01:56",
+        ],
+    );
+}
+
+#[test]
+fn ymdhms18() {
+    test_dates(
+        Format::new(Type::YmdHms, 18, 0).unwrap(),
+        &[
+            "  1648-06-10 00:00",
+            "  1680-06-30 04:50",
+            "  1716-07-24 12:31",
+            "  1768-06-19 12:47",
+            "  1819-08-02 01:26",
+            "  1839-03-27 20:58",
+            "  1903-04-19 07:36",
+            "  1929-08-25 15:43",
+            "  1941-09-29 04:25",
+            "  1943-04-19 06:49",
+            "  1943-10-07 02:57",
+            "  1992-03-17 16:45",
+            "  1996-02-25 21:30",
+            "  1941-09-29 04:25",
+            "  1943-04-19 06:49",
+            "  1943-10-07 02:57",
+            "  1992-03-17 16:45",
+            "  1996-02-25 21:30",
+            "  2038-11-10 22:30",
+            "  2094-07-18 01:56",
+        ],
+    );
+}
+
+#[test]
+fn ymdhms19() {
+    test_dates(
+        Format::new(Type::YmdHms, 19, 0).unwrap(),
+        &[
+            "1648-06-10 00:00:00",
+            "1680-06-30 04:50:38",
+            "1716-07-24 12:31:35",
+            "1768-06-19 12:47:53",
+            "1819-08-02 01:26:00",
+            "1839-03-27 20:58:11",
+            "1903-04-19 07:36:05",
+            "1929-08-25 15:43:49",
+            "1941-09-29 04:25:09",
+            "1943-04-19 06:49:27",
+            "1943-10-07 02:57:52",
+            "1992-03-17 16:45:44",
+            "1996-02-25 21:30:57",
+            "1941-09-29 04:25:09",
+            "1943-04-19 06:49:27",
+            "1943-10-07 02:57:52",
+            "1992-03-17 16:45:44",
+            "1996-02-25 21:30:57",
+            "2038-11-10 22:30:04",
+            "2094-07-18 01:56:51",
+        ],
+    );
+}
+
+#[test]
+fn ymdhms20() {
+    test_dates(
+        Format::new(Type::YmdHms, 20, 0).unwrap(),
+        &[
+            " 1648-06-10 00:00:00",
+            " 1680-06-30 04:50:38",
+            " 1716-07-24 12:31:35",
+            " 1768-06-19 12:47:53",
+            " 1819-08-02 01:26:00",
+            " 1839-03-27 20:58:11",
+            " 1903-04-19 07:36:05",
+            " 1929-08-25 15:43:49",
+            " 1941-09-29 04:25:09",
+            " 1943-04-19 06:49:27",
+            " 1943-10-07 02:57:52",
+            " 1992-03-17 16:45:44",
+            " 1996-02-25 21:30:57",
+            " 1941-09-29 04:25:09",
+            " 1943-04-19 06:49:27",
+            " 1943-10-07 02:57:52",
+            " 1992-03-17 16:45:44",
+            " 1996-02-25 21:30:57",
+            " 2038-11-10 22:30:04",
+            " 2094-07-18 01:56:51",
+        ],
+    );
+}
+
+#[test]
+fn ymdhms21() {
+    test_dates(
+        Format::new(Type::YmdHms, 21, 0).unwrap(),
+        &[
+            "  1648-06-10 00:00:00",
+            "  1680-06-30 04:50:38",
+            "  1716-07-24 12:31:35",
+            "  1768-06-19 12:47:53",
+            "  1819-08-02 01:26:00",
+            "  1839-03-27 20:58:11",
+            "  1903-04-19 07:36:05",
+            "  1929-08-25 15:43:49",
+            "  1941-09-29 04:25:09",
+            "  1943-04-19 06:49:27",
+            "  1943-10-07 02:57:52",
+            "  1992-03-17 16:45:44",
+            "  1996-02-25 21:30:57",
+            "  1941-09-29 04:25:09",
+            "  1943-04-19 06:49:27",
+            "  1943-10-07 02:57:52",
+            "  1992-03-17 16:45:44",
+            "  1996-02-25 21:30:57",
+            "  2038-11-10 22:30:04",
+            "  2094-07-18 01:56:51",
+        ],
+    );
+}
+
+#[test]
+fn ymdhms21_1() {
+    test_dates(
+        Format::new(Type::YmdHms, 21, 1).unwrap(),
+        &[
+            "1648-06-10 00:00:00.0",
+            "1680-06-30 04:50:38.1",
+            "1716-07-24 12:31:35.2",
+            "1768-06-19 12:47:53.3",
+            "1819-08-02 01:26:00.5",
+            "1839-03-27 20:58:11.6",
+            "1903-04-19 07:36:05.2",
+            "1929-08-25 15:43:49.8",
+            "1941-09-29 04:25:09.0",
+            "1943-04-19 06:49:27.5",
+            "1943-10-07 02:57:52.0",
+            "1992-03-17 16:45:44.9",
+            "1996-02-25 21:30:57.8",
+            "1941-09-29 04:25:09.2",
+            "1943-04-19 06:49:27.1",
+            "1943-10-07 02:57:52.5",
+            "1992-03-17 16:45:44.7",
+            "1996-02-25 21:30:57.6",
+            "2038-11-10 22:30:04.2",
+            "2094-07-18 01:56:51.6",
+        ],
+    );
+}
+
+#[test]
+fn ymdhms22_2() {
+    test_dates(
+        Format::new(Type::YmdHms, 22, 2).unwrap(),
+        &[
+            "1648-06-10 00:00:00.00",
+            "1680-06-30 04:50:38.12",
+            "1716-07-24 12:31:35.23",
+            "1768-06-19 12:47:53.35",
+            "1819-08-02 01:26:00.46",
+            "1839-03-27 20:58:11.57",
+            "1903-04-19 07:36:05.19",
+            "1929-08-25 15:43:49.83",
+            "1941-09-29 04:25:09.01",
+            "1943-04-19 06:49:27.52",
+            "1943-10-07 02:57:52.02",
+            "1992-03-17 16:45:44.87",
+            "1996-02-25 21:30:57.82",
+            "1941-09-29 04:25:09.15",
+            "1943-04-19 06:49:27.11",
+            "1943-10-07 02:57:52.48",
+            "1992-03-17 16:45:44.66",
+            "1996-02-25 21:30:57.58",
+            "2038-11-10 22:30:04.18",
+            "2094-07-18 01:56:51.59",
+        ],
+    );
+}
+
+#[test]
+fn ymdhms23_3() {
+    test_dates(
+        Format::new(Type::YmdHms, 23, 3).unwrap(),
+        &[
+            "1648-06-10 00:00:00.000",
+            "1680-06-30 04:50:38.123",
+            "1716-07-24 12:31:35.235",
+            "1768-06-19 12:47:53.345",
+            "1819-08-02 01:26:00.456",
+            "1839-03-27 20:58:11.567",
+            "1903-04-19 07:36:05.190",
+            "1929-08-25 15:43:49.831",
+            "1941-09-29 04:25:09.013",
+            "1943-04-19 06:49:27.524",
+            "1943-10-07 02:57:52.016",
+            "1992-03-17 16:45:44.865",
+            "1996-02-25 21:30:57.820",
+            "1941-09-29 04:25:09.154",
+            "1943-04-19 06:49:27.105",
+            "1943-10-07 02:57:52.482",
+            "1992-03-17 16:45:44.658",
+            "1996-02-25 21:30:57.582",
+            "2038-11-10 22:30:04.183",
+            "2094-07-18 01:56:51.593",
+        ],
+    );
+}
+
+#[test]
+fn ymdhms24_4() {
+    test_dates(
+        Format::new(Type::YmdHms, 24, 4).unwrap(),
+        &[
+            "1648-06-10 00:00:00.0000",
+            "1680-06-30 04:50:38.1230",
+            "1716-07-24 12:31:35.2345",
+            "1768-06-19 12:47:53.3450",
+            "1819-08-02 01:26:00.4562",
+            "1839-03-27 20:58:11.5668",
+            "1903-04-19 07:36:05.1896",
+            "1929-08-25 15:43:49.8313",
+            "1941-09-29 04:25:09.0129",
+            "1943-04-19 06:49:27.5238",
+            "1943-10-07 02:57:52.0156",
+            "1992-03-17 16:45:44.8653",
+            "1996-02-25 21:30:57.8205",
+            "1941-09-29 04:25:09.1539",
+            "1943-04-19 06:49:27.1053",
+            "1943-10-07 02:57:52.4823",
+            "1992-03-17 16:45:44.6583",
+            "1996-02-25 21:30:57.5822",
+            "2038-11-10 22:30:04.1835",
+            "2094-07-18 01:56:51.5932",
+        ],
+    );
+}
+
+#[test]
+fn ymdhms25_5() {
+    test_dates(
+        Format::new(Type::YmdHms, 25, 5).unwrap(),
+        &[
+            "1648-06-10 00:00:00.00000",
+            "1680-06-30 04:50:38.12301",
+            "1716-07-24 12:31:35.23453",
+            "1768-06-19 12:47:53.34505",
+            "1819-08-02 01:26:00.45615",
+            "1839-03-27 20:58:11.56677",
+            "1903-04-19 07:36:05.18964",
+            "1929-08-25 15:43:49.83132",
+            "1941-09-29 04:25:09.01293",
+            "1943-04-19 06:49:27.52375",
+            "1943-10-07 02:57:52.01565",
+            "1992-03-17 16:45:44.86529",
+            "1996-02-25 21:30:57.82047",
+            "1941-09-29 04:25:09.15395",
+            "1943-04-19 06:49:27.10533",
+            "1943-10-07 02:57:52.48229",
+            "1992-03-17 16:45:44.65827",
+            "1996-02-25 21:30:57.58219",
+            "2038-11-10 22:30:04.18347",
+            "2094-07-18 01:56:51.59319",
+        ],
+    );
+}
+
+fn test_times(format: Format, name: &str) {
+    let directory = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/format/testdata/display");
+    let input_filename = directory.join("time-input.txt");
+    let input = BufReader::new(File::open(&input_filename).unwrap());
+
+    let output_filename = directory.join(name);
+    let output = BufReader::new(File::open(&output_filename).unwrap());
+
+    let parser = Type::DTime.parser(UTF_8);
+    for ((input, expect), line_number) in input
+        .lines()
+        .map(|r| r.unwrap())
+        .zip_eq(output.lines().map(|r| r.unwrap()))
+        .zip(1..)
+    {
+        let value = parser.parse(&input).unwrap();
+        let formatted = value.display(format, UTF_8).to_string();
+        assert!(
+                formatted == expect,
+                "formatting {}:{line_number} as {format}:\n  actual: {formatted:?}\nexpected: {expect:?}",
+                input_filename.display()
+            );
+    }
+}
+
+#[test]
+fn time5() {
+    test_times(Format::new(Type::Time, 5, 0).unwrap(), "time5.txt");
+}
+
+#[test]
+fn time6() {
+    test_times(Format::new(Type::Time, 6, 0).unwrap(), "time6.txt");
+}
+
+#[test]
+fn time7() {
+    test_times(Format::new(Type::Time, 7, 0).unwrap(), "time7.txt");
+}
+
+#[test]
+fn time8() {
+    test_times(Format::new(Type::Time, 8, 0).unwrap(), "time8.txt");
+}
+
+#[test]
+fn time9() {
+    test_times(Format::new(Type::Time, 9, 0).unwrap(), "time9.txt");
+}
+
+#[test]
+fn time10() {
+    test_times(Format::new(Type::Time, 10, 0).unwrap(), "time10.txt");
+}
+
+#[test]
+fn time10_1() {
+    test_times(Format::new(Type::Time, 10, 1).unwrap(), "time10.1.txt");
+}
+
+#[test]
+fn time11() {
+    test_times(Format::new(Type::Time, 11, 0).unwrap(), "time11.txt");
+}
+
+#[test]
+fn time11_1() {
+    test_times(Format::new(Type::Time, 11, 1).unwrap(), "time11.1.txt");
+}
+
+#[test]
+fn time11_2() {
+    test_times(Format::new(Type::Time, 11, 2).unwrap(), "time11.2.txt");
+}
+
+#[test]
+fn time12() {
+    test_times(Format::new(Type::Time, 12, 0).unwrap(), "time12.txt");
+}
+
+#[test]
+fn time12_1() {
+    test_times(Format::new(Type::Time, 12, 1).unwrap(), "time12.1.txt");
+}
+
+#[test]
+fn time12_2() {
+    test_times(Format::new(Type::Time, 12, 2).unwrap(), "time12.2.txt");
+}
+
+#[test]
+fn time12_3() {
+    test_times(Format::new(Type::Time, 12, 3).unwrap(), "time12.3.txt");
+}
+
+#[test]
+fn time13() {
+    test_times(Format::new(Type::Time, 13, 0).unwrap(), "time13.txt");
+}
+
+#[test]
+fn time13_1() {
+    test_times(Format::new(Type::Time, 13, 1).unwrap(), "time13.1.txt");
+}
+
+#[test]
+fn time13_2() {
+    test_times(Format::new(Type::Time, 13, 2).unwrap(), "time13.2.txt");
+}
+
+#[test]
+fn time13_3() {
+    test_times(Format::new(Type::Time, 13, 3).unwrap(), "time13.3.txt");
+}
+
+#[test]
+fn time13_4() {
+    test_times(Format::new(Type::Time, 13, 4).unwrap(), "time13.4.txt");
+}
+
+#[test]
+fn time14() {
+    test_times(Format::new(Type::Time, 14, 0).unwrap(), "time14.txt");
+}
+
+#[test]
+fn time14_1() {
+    test_times(Format::new(Type::Time, 14, 1).unwrap(), "time14.1.txt");
+}
+
+#[test]
+fn time14_2() {
+    test_times(Format::new(Type::Time, 14, 2).unwrap(), "time14.2.txt");
+}
+
+#[test]
+fn time14_3() {
+    test_times(Format::new(Type::Time, 14, 3).unwrap(), "time14.3.txt");
+}
+
+#[test]
+fn time14_4() {
+    test_times(Format::new(Type::Time, 14, 4).unwrap(), "time14.4.txt");
+}
+
+#[test]
+fn time14_5() {
+    test_times(Format::new(Type::Time, 14, 5).unwrap(), "time14.5.txt");
+}
+
+#[test]
+fn time15() {
+    test_times(Format::new(Type::Time, 15, 0).unwrap(), "time15.txt");
+}
+
+#[test]
+fn time15_1() {
+    test_times(Format::new(Type::Time, 15, 1).unwrap(), "time15.1.txt");
+}
+
+#[test]
+fn time15_2() {
+    test_times(Format::new(Type::Time, 15, 2).unwrap(), "time15.2.txt");
+}
+
+#[test]
+fn time15_3() {
+    test_times(Format::new(Type::Time, 15, 3).unwrap(), "time15.3.txt");
+}
+
+#[test]
+fn time15_4() {
+    test_times(Format::new(Type::Time, 15, 4).unwrap(), "time15.4.txt");
+}
+
+#[test]
+fn time15_5() {
+    test_times(Format::new(Type::Time, 15, 5).unwrap(), "time15.5.txt");
+}
+
+#[test]
+fn time15_6() {
+    test_times(Format::new(Type::Time, 15, 6).unwrap(), "time15.6.txt");
+}
+
+#[test]
+fn mtime5() {
+    test_times(Format::new(Type::MTime, 5, 0).unwrap(), "mtime5.txt");
+}
+
+#[test]
+fn mtime6() {
+    test_times(Format::new(Type::MTime, 6, 0).unwrap(), "mtime6.txt");
+}
+
+#[test]
+fn mtime7() {
+    test_times(Format::new(Type::MTime, 7, 0).unwrap(), "mtime7.txt");
+}
+
+#[test]
+fn mtime7_1() {
+    test_times(Format::new(Type::MTime, 7, 1).unwrap(), "mtime7.1.txt");
+}
+
+#[test]
+fn mtime8() {
+    test_times(Format::new(Type::MTime, 8, 0).unwrap(), "mtime8.txt");
+}
+
+#[test]
+fn mtime8_1() {
+    test_times(Format::new(Type::MTime, 8, 1).unwrap(), "mtime8.1.txt");
+}
+
+#[test]
+fn mtime8_2() {
+    test_times(Format::new(Type::MTime, 8, 2).unwrap(), "mtime8.2.txt");
+}
+
+#[test]
+fn mtime9() {
+    test_times(Format::new(Type::MTime, 9, 0).unwrap(), "mtime9.txt");
+}
+
+#[test]
+fn mtime9_1() {
+    test_times(Format::new(Type::MTime, 9, 1).unwrap(), "mtime9.1.txt");
+}
+
+#[test]
+fn mtime9_2() {
+    test_times(Format::new(Type::MTime, 9, 2).unwrap(), "mtime9.2.txt");
+}
+
+#[test]
+fn mtime9_3() {
+    test_times(Format::new(Type::MTime, 9, 3).unwrap(), "mtime9.3.txt");
+}
+
+#[test]
+fn mtime10() {
+    test_times(Format::new(Type::MTime, 10, 0).unwrap(), "mtime10.txt");
+}
+
+#[test]
+fn mtime10_1() {
+    test_times(Format::new(Type::MTime, 10, 1).unwrap(), "mtime10.1.txt");
+}
+
+#[test]
+fn mtime10_2() {
+    test_times(Format::new(Type::MTime, 10, 2).unwrap(), "mtime10.2.txt");
+}
+
+#[test]
+fn mtime10_3() {
+    test_times(Format::new(Type::MTime, 10, 3).unwrap(), "mtime10.3.txt");
+}
+
+#[test]
+fn mtime10_4() {
+    test_times(Format::new(Type::MTime, 10, 4).unwrap(), "mtime10.4.txt");
+}
+
+#[test]
+fn mtime11() {
+    test_times(Format::new(Type::MTime, 11, 0).unwrap(), "mtime11.txt");
+}
+
+#[test]
+fn mtime11_1() {
+    test_times(Format::new(Type::MTime, 11, 1).unwrap(), "mtime11.1.txt");
+}
+
+#[test]
+fn mtime11_2() {
+    test_times(Format::new(Type::MTime, 11, 2).unwrap(), "mtime11.2.txt");
+}
+
+#[test]
+fn mtime11_3() {
+    test_times(Format::new(Type::MTime, 11, 3).unwrap(), "mtime11.3.txt");
+}
+
+#[test]
+fn mtime11_4() {
+    test_times(Format::new(Type::MTime, 11, 4).unwrap(), "mtime11.4.txt");
+}
+
+#[test]
+fn mtime11_5() {
+    test_times(Format::new(Type::MTime, 11, 5).unwrap(), "mtime11.5.txt");
+}
+
+#[test]
+fn mtime12_5() {
+    test_times(Format::new(Type::MTime, 12, 5).unwrap(), "mtime12.5.txt");
+}
+
+#[test]
+fn mtime13_5() {
+    test_times(Format::new(Type::MTime, 13, 5).unwrap(), "mtime13.5.txt");
+}
+
+#[test]
+fn mtime14_5() {
+    test_times(Format::new(Type::MTime, 14, 5).unwrap(), "mtime14.5.txt");
+}
+
+#[test]
+fn mtime15_5() {
+    test_times(Format::new(Type::MTime, 15, 5).unwrap(), "mtime15.5.txt");
+}
+
+#[test]
+fn mtime16_5() {
+    test_times(Format::new(Type::MTime, 16, 5).unwrap(), "mtime16.5.txt");
+}
+
+#[test]
+fn dtime8() {
+    test_times(Format::new(Type::DTime, 8, 0).unwrap(), "dtime8.txt");
+}
+
+#[test]
+fn dtime9() {
+    test_times(Format::new(Type::DTime, 9, 0).unwrap(), "dtime9.txt");
+}
+
+#[test]
+fn dtime10() {
+    test_times(Format::new(Type::DTime, 10, 0).unwrap(), "dtime10.txt");
+}
+
+#[test]
+fn dtime11() {
+    test_times(Format::new(Type::DTime, 11, 0).unwrap(), "dtime11.txt");
+}
+
+#[test]
+fn dtime12() {
+    test_times(Format::new(Type::DTime, 12, 0).unwrap(), "dtime12.txt");
+}
+
+#[test]
+fn dtime13() {
+    test_times(Format::new(Type::DTime, 13, 0).unwrap(), "dtime13.txt");
+}
+
+#[test]
+fn dtime13_1() {
+    test_times(Format::new(Type::DTime, 13, 1).unwrap(), "dtime13.1.txt");
+}
+
+#[test]
+fn dtime14() {
+    test_times(Format::new(Type::DTime, 14, 0).unwrap(), "dtime14.txt");
+}
+
+#[test]
+fn dtime14_1() {
+    test_times(Format::new(Type::DTime, 14, 1).unwrap(), "dtime14.1.txt");
+}
+
+#[test]
+fn dtime14_2() {
+    test_times(Format::new(Type::DTime, 14, 2).unwrap(), "dtime14.2.txt");
+}
+
+#[test]
+fn dtime15() {
+    test_times(Format::new(Type::DTime, 15, 0).unwrap(), "dtime15.txt");
+}
+
+#[test]
+fn dtime15_1() {
+    test_times(Format::new(Type::DTime, 15, 1).unwrap(), "dtime15.1.txt");
+}
+
+#[test]
+fn dtime15_2() {
+    test_times(Format::new(Type::DTime, 15, 2).unwrap(), "dtime15.2.txt");
+}
+
+#[test]
+fn dtime15_3() {
+    test_times(Format::new(Type::DTime, 15, 3).unwrap(), "dtime15.3.txt");
+}
+
+#[test]
+fn dtime16() {
+    test_times(Format::new(Type::DTime, 16, 0).unwrap(), "dtime16.txt");
+}
+
+#[test]
+fn dtime16_1() {
+    test_times(Format::new(Type::DTime, 16, 1).unwrap(), "dtime16.1.txt");
+}
+
+#[test]
+fn dtime16_2() {
+    test_times(Format::new(Type::DTime, 16, 2).unwrap(), "dtime16.2.txt");
+}
+
+#[test]
+fn dtime16_3() {
+    test_times(Format::new(Type::DTime, 16, 3).unwrap(), "dtime16.3.txt");
+}
+
+#[test]
+fn dtime16_4() {
+    test_times(Format::new(Type::DTime, 16, 4).unwrap(), "dtime16.4.txt");
+}
+
+#[test]
+fn dtime17() {
+    test_times(Format::new(Type::DTime, 17, 0).unwrap(), "dtime17.txt");
+}
+
+#[test]
+fn dtime17_1() {
+    test_times(Format::new(Type::DTime, 17, 1).unwrap(), "dtime17.1.txt");
+}
+
+#[test]
+fn dtime17_2() {
+    test_times(Format::new(Type::DTime, 17, 2).unwrap(), "dtime17.2.txt");
+}
+
+#[test]
+fn dtime17_3() {
+    test_times(Format::new(Type::DTime, 17, 3).unwrap(), "dtime17.3.txt");
+}
+
+#[test]
+fn dtime17_4() {
+    test_times(Format::new(Type::DTime, 17, 4).unwrap(), "dtime17.4.txt");
+}
+
+#[test]
+fn dtime17_5() {
+    test_times(Format::new(Type::DTime, 17, 5).unwrap(), "dtime17.5.txt");
+}
+
+#[test]
+fn dtime18() {
+    test_times(Format::new(Type::DTime, 18, 0).unwrap(), "dtime18.txt");
+}
+
+#[test]
+fn dtime18_1() {
+    test_times(Format::new(Type::DTime, 18, 1).unwrap(), "dtime18.1.txt");
+}
+
+#[test]
+fn dtime18_2() {
+    test_times(Format::new(Type::DTime, 18, 2).unwrap(), "dtime18.2.txt");
+}
+
+#[test]
+fn dtime18_3() {
+    test_times(Format::new(Type::DTime, 18, 3).unwrap(), "dtime18.3.txt");
+}
+
+#[test]
+fn dtime18_4() {
+    test_times(Format::new(Type::DTime, 18, 4).unwrap(), "dtime18.4.txt");
+}
+
+#[test]
+fn dtime18_5() {
+    test_times(Format::new(Type::DTime, 18, 5).unwrap(), "dtime18.5.txt");
+}
+
+#[test]
+fn dtime18_6() {
+    test_times(Format::new(Type::DTime, 18, 6).unwrap(), "dtime18.6.txt");
+}
index 442bf7fd4f10fe113be41b14f2f0b341d4d93058..51c06096d804c1ab897f95e5f000bfcb370d26f9 100644 (file)
@@ -18,7 +18,7 @@ use crate::{
 
 mod display;
 mod parse;
-pub use display::DisplayValue;
+pub use display::{DisplayPlain, DisplayValue};
 
 #[derive(Clone, ThisError, Debug, PartialEq, Eq)]
 pub enum Error {
index fba54d7ef5d1a149260fb6d6df237a1a630e7116..50b4892f3c8cafda4b5edd7a14990f4f25114061 100644 (file)
@@ -345,13 +345,13 @@ pub fn id_match_n_nonstatic(keyword: &str, token: &str, n: usize) -> bool {
 
 impl Display for Identifier {
     fn fmt(&self, f: &mut Formatter) -> FmtResult {
-        write!(f, "{}", self.0)
+        write!(f, "{:?}", self.0)
     }
 }
 
 impl Debug for Identifier {
     fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
-        write!(f, "{}", self.0)
+        write!(f, "{:?}", self.0)
     }
 }
 
index 2c0a57755d1ac7654b48e58ba5a295e8d1b31fb5..06ffee4c1ed4d7411e32554bb0e2a3ac568a50db 100644 (file)
@@ -28,7 +28,7 @@ impl From<TableProperties> for Look {
     fn from(table_properties: TableProperties) -> Self {
         Self {
                 name: table_properties.name,
-                omit_empty: table_properties.general_properties.hide_empty_rows,
+                hide_empty: table_properties.general_properties.hide_empty_rows,
                 row_label_position: table_properties.general_properties.row_label_position,
                 heading_widths: enum_map! {
                     HeadingRegion::Columns => table_properties.general_properties.minimum_column_width..=table_properties.general_properties.maximum_column_width,
index b83574acd8a79a1eea24de8d48c19f6d8d64b802..5d5a3d0ac148bf35fb52839c1a5723b43ae75e0f 100644 (file)
@@ -40,7 +40,7 @@ use binrw::Error as BinError;
 use chrono::NaiveDateTime;
 pub use color::ParseError as ParseColorError;
 use color::{palette::css::TRANSPARENT, AlphaColor, Rgba8, Srgb};
-use encoding_rs::UTF_8;
+use encoding_rs::{Encoding, UTF_8};
 use enum_iterator::Sequence;
 use enum_map::{enum_map, Enum, EnumMap};
 use look_xml::TableProperties;
@@ -601,7 +601,7 @@ pub struct Look {
     pub name: Option<String>,
 
     /// Whether to hide rows or columns whose cells are all empty.
-    pub omit_empty: bool,
+    pub hide_empty: bool,
 
     pub row_label_position: LabelPosition,
 
@@ -637,7 +637,7 @@ pub struct Look {
 
 impl Look {
     pub fn with_omit_empty(mut self, omit_empty: bool) -> Self {
-        self.omit_empty = omit_empty;
+        self.hide_empty = omit_empty;
         self
     }
     pub fn with_row_label_position(mut self, row_label_position: LabelPosition) -> Self {
@@ -654,7 +654,7 @@ impl Default for Look {
     fn default() -> Self {
         Self {
             name: None,
-            omit_empty: true,
+            hide_empty: true,
             row_label_position: LabelPosition::default(),
             heading_widths: EnumMap::from_fn(|region| match region {
                 HeadingRegion::Rows => 36..=72,
@@ -1365,6 +1365,13 @@ impl PivotTable {
         Arc::make_mut(&mut self.look)
     }
 
+    pub fn with_show_empty(mut self) -> Self {
+        if self.look.hide_empty {
+            self.look_mut().hide_empty = false;
+        }
+        self
+    }
+
     pub fn label(&self) -> String {
         match &self.title {
             Some(title) => title.display(self).to_string(),
@@ -1727,6 +1734,15 @@ impl Value {
             variable_label: variable.label.clone(),
         }))
     }
+    pub fn new_value(value: &DataValue, encoding: &'static Encoding) -> Self {
+        match value {
+            DataValue::Number(number) => Self::new_number(*number),
+            DataValue::String(string) => Self::new_user_text(string.decode(encoding).into_owned()),
+        }
+    }
+    pub fn new_variable_value(variable: &Variable, value: &DataValue) -> Self {
+        todo!()
+    }
     pub fn new_number(x: Option<f64>) -> Self {
         Self::new_number_with_format(x, Format::F8_2)
     }
@@ -1765,6 +1781,12 @@ impl From<&str> for Value {
     }
 }
 
+impl From<&Variable> for Value {
+    fn from(variable: &Variable) -> Self {
+        Self::new_variable(variable)
+    }
+}
+
 pub struct DisplayValue<'a> {
     inner: &'a ValueInner,
     markup: bool,
index fd63bf8174313772c3a92864b063e857a2d5b3e0..a5f4d188bc19b9365bf0d8e5e6c22517e6e6427e 100644 (file)
@@ -526,7 +526,7 @@ struct Headings<'a> {
 
 impl<'a> Headings<'a> {
     fn new(pt: &'a PivotTable, h: Axis2, layer_indexes: &[usize]) -> Self {
-        let column_enumeration = pt.enumerate_axis(h.into(), layer_indexes, pt.look.omit_empty);
+        let column_enumeration = pt.enumerate_axis(h.into(), layer_indexes, pt.look.hide_empty);
 
         let mut headings = pt.axes[h.into()]
             .dimensions
index 84c8117ac9a9db7e9d48d3bd64fa61319bf5684e..e86a6853f8e389ef503fb190432fb059384793f1 100644 (file)
@@ -763,7 +763,7 @@ fn footnote_alphabetic_superscript() {
         "\
 Pivot Table with Alphabetic Superscript Footnotes[*]
 ╭────────────┬──────────────────╮
-│            │       A[*]      1 │
+│            │       A[*]       │
 │            ├───────┬──────────┤
 │Corner[*][b]│  B[b] │  C[*][b] │
 ├────────────┼───────┼──────────┤
index 85648c2adef596d14154fb45140268395d057399..e5de5a9df170629bc5822f3764e51e3550ff340e 100644 (file)
@@ -60,7 +60,7 @@ impl From<TableLook> for Look {
         let flags = look.pt_table_look.flags;
         Self {
             name: None,
-            omit_empty: (flags & 2) != 0,
+            hide_empty: (flags & 2) != 0,
             row_label_position: if look.pt_table_look.nested_row_labels {
                 LabelPosition::Nested
             } else {
index 6e72ddb74dd7df919a7f7b30f796e25890abf80a..68adf5df99b497ece34c22a55f0c271c4d5b6fca 100644 (file)
@@ -726,7 +726,7 @@ impl Page {
             total += scp[z + 1] - scp[z];
         }
         dcp.push(total);
-        debug_assert_eq!(dcp.len(), 2 * n[a] + 1);
+        debug_assert_eq!(dcp.len(), 1 + 2 * n[a] + 1);
 
         let mut cp = EnumMap::default();
         cp[a] = dcp;
index 6b4aa39c902515c33fee15aaaeeeda6b735aae38..9635655355e1bd750a5aa0639e45f2b92b88d65e 100644 (file)
@@ -315,7 +315,7 @@ impl BinWrite for PivotTable {
             1u32,
             4u32,
             self.spv_layer() as u32,
-            SpvBool(self.look.omit_empty),
+            SpvBool(self.look.hide_empty),
             SpvBool(self.look.row_label_position == LabelPosition::Corner),
             SpvBool(self.look.footnote_marker_type == FootnoteMarkerType::Alphabetic),
             SpvBool(self.look.footnote_marker_position == FootnoteMarkerPosition::Superscript),
index 098214c675d5bac9599293ad158c087ffa082bf0..9094079f35c1130763b70550a69850bc32094329 100644 (file)
@@ -22,6 +22,44 @@ use super::{
     Details, Item,
 };
 
+#[derive(Clone, Debug, Default)]
+pub enum Boxes {
+    Ascii,
+    #[default]
+    Unicode,
+}
+
+impl Boxes {
+    fn box_chars(&self) -> &'static BoxChars {
+        match self {
+            Boxes::Ascii => &*ASCII_BOX,
+            Boxes::Unicode => &*UNICODE_BOX,
+        }
+    }
+}
+
+#[derive(Clone, Debug)]
+pub struct TextRendererConfig {
+    /// Enable bold and underline in output?
+    pub emphasis: bool,
+
+    /// Page width.
+    pub width: usize,
+
+    /// ASCII or Unicode
+    pub boxes: Boxes,
+}
+
+impl Default for TextRendererConfig {
+    fn default() -> Self {
+        Self {
+            emphasis: false,
+            width: usize::MAX,
+            boxes: Boxes::default(),
+        }
+    }
+}
+
 pub struct TextRenderer {
     /// Enable bold and underline in output?
     emphasis: bool,
@@ -41,21 +79,20 @@ pub struct TextRenderer {
 
 impl Default for TextRenderer {
     fn default() -> Self {
-        Self::new()
+        Self::new(&TextRendererConfig::default())
     }
 }
 
 impl TextRenderer {
-    pub fn new() -> Self {
-        let width = 80;
+    pub fn new(config: &TextRendererConfig) -> Self {
         Self {
-            emphasis: true,
-            width,
+            emphasis: config.emphasis,
+            width: config.width,
             min_hbreak: 20,
-            box_chars: &*UNICODE_BOX,
+            box_chars: config.boxes.box_chars(),
             n_objects: 0,
             params: Params {
-                size: Coord2::new(width, usize::MAX),
+                size: Coord2::new(config.width, usize::MAX),
                 font_size: EnumMap::from_fn(|_| 1),
                 line_widths: EnumMap::from_fn(|stroke| if stroke == Stroke::None { 0 } else { 1 }),
                 px_size: None,
@@ -289,7 +326,7 @@ impl<'a> DisplayPivotTable<'a> {
 
 impl Display for DisplayPivotTable<'_> {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        for line in TextRenderer::new().render(self.pt) {
+        for line in TextRenderer::default().render(self.pt) {
             writeln!(f, "{}", line)?;
         }
         Ok(())
@@ -305,7 +342,7 @@ impl TextDriver {
     pub fn new(file: File) -> TextDriver {
         Self {
             file: BufWriter::new(file),
-            renderer: TextRenderer::new(),
+            renderer: TextRenderer::default(),
         }
     }
 }
index 13be160ca544279b4bb43f936480c865a9946358..7b4c5235f7c0f430ec37bdb9be9889d3736280b8 100644 (file)
@@ -365,7 +365,7 @@ impl Headers {
     }
 }
 
-#[derive(Debug)]
+#[derive(Clone, Debug, PartialEq, Eq)]
 pub struct Metadata {
     pub creation: NaiveDateTime,
     pub endian: Endian,
@@ -379,7 +379,7 @@ pub struct Metadata {
 impl Metadata {
     fn decode(headers: &Headers, mut warn: impl FnMut(Error)) -> Self {
         let header = &headers.header;
-        let creation_date = NaiveDate::parse_from_str(&header.creation_date, "%e %b %Y")
+        let creation_date = NaiveDate::parse_from_str(&header.creation_date, "%e %b %y")
             .unwrap_or_else(|_| {
                 warn(Error::InvalidCreationDate {
                     creation_date: header.creation_date.to_string(),
@@ -499,7 +499,11 @@ pub fn decode(
                 new_name
             }
         };
-        let mut variable = Variable::new(name.clone(), VarWidth::try_from(input.width).unwrap());
+        let mut variable = Variable::new(
+            name.clone(),
+            VarWidth::try_from(input.width).unwrap(),
+            encoding,
+        );
 
         // Set the short name the same as the long name (even if we renamed it).
         variable.short_names = vec![name];
@@ -726,7 +730,8 @@ pub fn decode(
     for index in 0..dictionary.variables.len() {
         let variable = dictionary.variables.get_index_mut2(index).unwrap();
         match variable.attributes.role() {
-            Ok(role) => variable.role = role,
+            Ok(Some(role)) => variable.role = role,
+            Ok(None) => (),
             Err(error) => warn(Error::InvalidRole(error)),
         }
     }
index dbb0d3de9eb00732c8047033efef7c5954936791..fde85784e36f28f3a2b39365ba5ea491540786fc 100644 (file)
@@ -1,6 +1,7 @@
 use crate::{
     dictionary::{Attributes, Value, VarWidth},
     endian::{Endian, Parse, ToBytes},
+    format::DisplayPlain,
     identifier::{Error as IdError, Identifier},
     sys::encoding::{default_encoding, get_encoding, Error as EncodingError},
 };
@@ -1140,7 +1141,7 @@ impl Debug for MissingValues {
 }
 
 impl MissingValues {
-    fn is_empty(&self) -> bool {
+    pub fn is_empty(&self) -> bool {
         self.values.is_empty() && self.range.is_none()
     }
 
@@ -1216,9 +1217,16 @@ pub struct DisplayMissingValues<'a> {
 
 impl<'a> Display for DisplayMissingValues<'a> {
     fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
+        if let Some(range) = &self.mv.range {
+            write!(f, "{range}")?;
+            if !self.mv.values.is_empty() {
+                write!(f, "; ")?;
+            }
+        }
+
         for (i, value) in self.mv.values.iter().enumerate() {
             if i > 0 {
-                write!(f, ", ")?;
+                write!(f, "; ")?;
             }
             match self.encoding {
                 Some(encoding) => value.display_plain(encoding).fmt(f)?,
@@ -1226,13 +1234,6 @@ impl<'a> Display for DisplayMissingValues<'a> {
             }
         }
 
-        if let Some(range) = &self.mv.range {
-            if !self.mv.values.is_empty() {
-                write!(f, ", ")?;
-            }
-            write!(f, "{range}")?;
-        }
-
         if self.mv.is_empty() {
             write!(f, "none")?;
         }
@@ -1274,11 +1275,18 @@ impl MissingValueRange {
 
 impl Display for MissingValueRange {
     fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
-        match self {
-            MissingValueRange::In { low, high } => write!(f, "{low:?} THRU {high:?}"),
-            MissingValueRange::From { low } => write!(f, "{low:?} THRU HI"),
-            MissingValueRange::To { high } => write!(f, "LOW THRU {high:?}"),
+        match self.low() {
+            Some(low) => low.display_plain().fmt(f)?,
+            None => write!(f, "LOW")?,
+        }
+
+        write!(f, " THRU ")?;
+
+        match self.high() {
+            Some(high) => high.display_plain().fmt(f)?,
+            None => write!(f, "HIGH")?,
         }
+        Ok(())
     }
 }
 
@@ -1550,6 +1558,10 @@ impl RawStr {
     pub fn display(&self, encoding: &'static Encoding) -> DisplayRawString {
         DisplayRawString(encoding.decode_without_bom_handling(&self.0).0)
     }
+
+    pub fn decode(&self, encoding: &'static Encoding) -> Cow<'_, str> {
+        encoding.decode_without_bom_handling(&self.0).0
+    }
 }
 
 pub struct DisplayRawString<'a>(Cow<'a, str>);
index 895ba496bd03ecddc0b16091f6be7716e697dc7b..800ba97b44d3ed7b8a85c1b5c137518d8b33d2be 100644 (file)
@@ -4,15 +4,19 @@ use crate::{
     endian::Endian,
     output::pivot::test::assert_rendering,
     sys::{
-        cooked::{decode, Headers},
+        cooked::{decode, Headers, Metadata},
         raw::{encoding_from_headers, Decoder, Reader, Record},
         sack::sack,
     },
 };
 
+use chrono::{NaiveDate, NaiveTime};
+use enum_iterator::all;
+
 #[test]
 fn variable_labels_and_missing_values() {
-    let input = r#"
+    for endian in all::<Endian>() {
+        let input = r#"
 # File header.
 "$FL2"; s60 "$(#) SPSS DATA FILE PSPP synthetic test file";
 2; # Layout code
@@ -21,7 +25,7 @@ fn variable_labels_and_missing_values() {
 0; # Not weighted
 1; # 1 case.
 100.0; # Bias.
-"01 Jan 11"; "20:53:52";
+"05 Jan 11"; "20:53:52";
 "PSPP synthetic test file: "; i8 244; i8 245; i8 246; i8 248; s34 "";
 i8 0 *3;
 
@@ -131,27 +135,69 @@ s8 "abcd"; s8 "efgh"; s8 "ijkl"; s8 "mnop"; s8 "qrst"; s8 "uvwx";
 s16 "yzABCDEFGHI"; s16 "JKLMNOPQR"; s16 "STUVWXYZ01";
 s16 "23456789abc"; s32 "defghijklmnopqstuvwxyzABC";
 "#;
-    let sysfile = sack(input, None, Endian::Big).unwrap();
-    let cursor = Cursor::new(sysfile);
-    let reader = Reader::new(cursor, |warning| println!("{warning}")).unwrap();
-    let headers: Vec<Record> = reader.collect::<Result<Vec<_>, _>>().unwrap();
-    let encoding = encoding_from_headers(&headers, &|e| eprintln!("{e}")).unwrap();
-    let decoder = Decoder::new(encoding, |e| eprintln!("{e}"));
-    let mut decoded_records = Vec::new();
-    for header in headers {
-        decoded_records.push(header.decode(&decoder).unwrap());
+        let sysfile = sack(input, None, endian).unwrap();
+        let cursor = Cursor::new(sysfile);
+        let reader = Reader::new(cursor, |warning| println!("{warning}")).unwrap();
+        let headers: Vec<Record> = reader.collect::<Result<Vec<_>, _>>().unwrap();
+        let encoding = encoding_from_headers(&headers, &|e| eprintln!("{e}")).unwrap();
+        let decoder = Decoder::new(encoding, |e| eprintln!("{e}"));
+        let mut decoded_records = Vec::new();
+        for header in headers {
+            decoded_records.push(header.decode(&decoder).unwrap());
+        }
+
+        let mut errors = Vec::new();
+        let headers = Headers::new(decoded_records, &mut |e| errors.push(e)).unwrap();
+        let (dictionary, metadata) = decode(headers, encoding, |e| errors.push(e)).unwrap();
+        assert_eq!(errors, vec![]);
+        assert_eq!(
+            metadata,
+            Metadata {
+                creation: NaiveDate::from_ymd_opt(2011, 1, 5)
+                    .unwrap()
+                    .and_time(NaiveTime::from_hms_opt(20, 53, 52).unwrap()),
+                endian,
+                compression: None,
+                n_cases: Some(1),
+                product: "$(#) SPSS DATA FILE PSPP synthetic test file".into(),
+                product_ext: None,
+                version: Some((1, 2, 3)),
+            }
+        );
+        assert_eq!(
+            dictionary.file_label.as_ref().map(|s| s.as_str()),
+            Some("PSPP synthetic test file: ôõöø")
+        );
+        let pt = dictionary.output_variables().to_pivot_table();
+        assert_rendering(
+            "variable_labels_and_missing_values",
+            &pt,
+            r#"╭────────────────────────────────┬────────┬────────────────────────────────┬─────────────────┬─────┬─────┬─────────┬────────────┬────────────┬──────────────────────╮
+│                                │Position│              Label             │Measurement Level│ Role│Width│Alignment│Print Format│Write Format│    Missing Values    │
+├────────────────────────────────┼────────┼────────────────────────────────┼─────────────────┼─────┼─────┼─────────┼────────────┼────────────┼──────────────────────┤
+│num1                            │       1│                                │                 │Input│    8│Right    │F8.0        │F8.0        │                      │
+│Numeric variable 2's label (ùúû)│       2│Numeric variable 2's label (ùúû)│                 │Input│    8│Right    │F8.0        │F8.0        │                      │
+│num3                            │       3│                                │                 │Input│    8│Right    │F8.0        │F8.0        │1                     │
+│Another numeric variable label  │       4│Another numeric variable label  │                 │Input│    8│Right    │F8.0        │F8.0        │1                     │
+│num5                            │       5│                                │                 │Input│    8│Right    │F8.0        │F8.0        │1; 2                  │
+│num6                            │       6│                                │                 │Input│    8│Right    │F8.0        │F8.0        │1; 2; 3               │
+│num7                            │       7│                                │                 │Input│    8│Right    │F8.0        │F8.0        │1 THRU 3              │
+│num8                            │       8│                                │                 │Input│    8│Right    │F8.0        │F8.0        │1 THRU 3; 5           │
+│num9                            │       9│                                │                 │Input│    8│Right    │F8.0        │F8.0        │1 THRU HIGH; -5       │
+│numàèìñò                        │      10│                                │                 │Input│    8│Right    │F8.0        │F8.0        │LOW THRU 1; 5         │
+│str1                            │      11│                                │Nominal          │Input│    4│Left     │A4          │A4          │                      │
+│String variable 2's label       │      12│String variable 2's label       │Nominal          │Input│    4│Left     │A4          │A4          │                      │
+│str3                            │      13│                                │Nominal          │Input│    4│Left     │A4          │A4          │"MISS"                │
+│Another string variable label   │      14│Another string variable label   │Nominal          │Input│    4│Left     │A4          │A4          │"OTHR"                │
+│str5                            │      15│                                │Nominal          │Input│    4│Left     │A4          │A4          │"MISS"; "OTHR"        │
+│str6                            │      16│                                │Nominal          │Input│    4│Left     │A4          │A4          │"MISS"; "OTHR"; "MORE"│
+│str7                            │      17│                                │Nominal          │Input│   11│Left     │A11         │A11         │"first8by"            │
+│str8                            │      18│                                │Nominal          │Input│    9│Left     │A9          │A9          │                      │
+│str9                            │      19│                                │Nominal          │Input│   10│Left     │A10         │A10         │                      │
+│str10                           │      20│                                │Nominal          │Input│   11│Left     │A11         │A11         │                      │
+│25-byte string                  │      21│25-byte string                  │Nominal          │Input│   25│Left     │A25         │A25         │                      │
+╰────────────────────────────────┴────────┴────────────────────────────────┴─────────────────┴─────┴─────┴─────────┴────────────┴────────────┴──────────────────────╯
+"#,
+        );
     }
-
-    let mut errors = Vec::new();
-    let headers = Headers::new(decoded_records, &mut |e| errors.push(e)).unwrap();
-    let (dictionary, metadata) = decode(headers, encoding, |e| errors.push(e)).unwrap();
-    assert_eq!(errors, vec![]);
-    println!("{dictionary:#?}");
-    assert_eq!(metadata.endian, Endian::Big);
-    assert_eq!(metadata.compression, None);
-    assert_eq!(metadata.n_cases, Some(1));
-    assert_eq!(metadata.version, Some((1, 2, 3)));
-    println!("{metadata:#?}");
-    let pt = dictionary.display_variables().to_pivot_table();
-    assert_rendering("variable_labels_and_missing_values", &pt, "");
 }