From fa542dadcb99db72242b178135ec2c8d58c258a1 Mon Sep 17 00:00:00 2001 From: Ben Pfaff Date: Tue, 20 May 2025 13:07:31 -0700 Subject: [PATCH] work --- rust/pspp/src/dictionary.rs | 235 +- rust/pspp/src/endian.rs | 3 +- rust/pspp/src/format/display.rs | 2841 ------------------------ rust/pspp/src/format/display/mod.rs | 1116 ++++++++++ rust/pspp/src/format/display/test.rs | 1749 +++++++++++++++ rust/pspp/src/format/mod.rs | 2 +- rust/pspp/src/identifier.rs | 4 +- rust/pspp/src/output/pivot/look_xml.rs | 2 +- rust/pspp/src/output/pivot/mod.rs | 30 +- rust/pspp/src/output/pivot/output.rs | 2 +- rust/pspp/src/output/pivot/test.rs | 2 +- rust/pspp/src/output/pivot/tlo.rs | 2 +- rust/pspp/src/output/render.rs | 2 +- rust/pspp/src/output/spv.rs | 2 +- rust/pspp/src/output/text.rs | 55 +- rust/pspp/src/sys/cooked.rs | 13 +- rust/pspp/src/sys/raw.rs | 38 +- rust/pspp/src/sys/test.rs | 96 +- 18 files changed, 3223 insertions(+), 2971 deletions(-) delete mode 100644 rust/pspp/src/format/display.rs create mode 100644 rust/pspp/src/format/display/mod.rs create mode 100644 rust/pspp/src/format/display/test.rs diff --git a/rust/pspp/src/dictionary.rs b/rust/pspp/src/dictionary.rs index bbf97749f8..cbbde90349 100644 --- a/rust/pspp/src/dictionary.rs +++ b/rust/pspp/src/dictionary.rs @@ -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 { - 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 { + 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, } -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 { 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 { + 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::>(); + 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 { + 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` because defining traits on `Option` - /// is not allowed. - fn try_from_str(input: &str) -> Result, 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 { 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>` because defining traits on - /// `Option>` is not allowed. - fn try_from_integer(integer: i32) -> Result, 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 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 { + 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 { 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::() { + 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, /// Role in data analysis. - pub role: Option, + 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, } } diff --git a/rust/pspp/src/endian.rs b/rust/pspp/src/endian.rs index dc94b6d32e..5b68a47d27 100644 --- a/rust/pspp/src/endian.rs +++ b/rust/pspp/src/endian.rs @@ -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 index f82a14cec5..0000000000 --- a/rust/pspp/src/format/display.rs +++ /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 { - 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 { - // 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(&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> { - 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, 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) -> 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) -> 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) -> 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) -> 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, 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::() - .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(s: &mut SmallString) -where - A: Array, -{ - // 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::>(); - 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::() - .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::>(); - 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::() - .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 index 0000000000..b315c8873c --- /dev/null +++ b/rust/pspp/src/format/display/mod.rs @@ -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 { + 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 { + // 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(&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> { + 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, 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) -> 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) -> 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) -> 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) -> 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, 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::() + .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(s: &mut SmallString) +where + A: Array, +{ + // 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 index 0000000000..cae76487ba --- /dev/null +++ b/rust/pspp/src/format/display/test.rs @@ -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::>(); + 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::() + .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::>(); + 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::() + .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/mod.rs b/rust/pspp/src/format/mod.rs index 442bf7fd4f..51c06096d8 100644 --- a/rust/pspp/src/format/mod.rs +++ b/rust/pspp/src/format/mod.rs @@ -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 { diff --git a/rust/pspp/src/identifier.rs b/rust/pspp/src/identifier.rs index fba54d7ef5..50b4892f3c 100644 --- a/rust/pspp/src/identifier.rs +++ b/rust/pspp/src/identifier.rs @@ -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) } } diff --git a/rust/pspp/src/output/pivot/look_xml.rs b/rust/pspp/src/output/pivot/look_xml.rs index 2c0a57755d..06ffee4c1e 100644 --- a/rust/pspp/src/output/pivot/look_xml.rs +++ b/rust/pspp/src/output/pivot/look_xml.rs @@ -28,7 +28,7 @@ impl From 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, diff --git a/rust/pspp/src/output/pivot/mod.rs b/rust/pspp/src/output/pivot/mod.rs index b83574acd8..5d5a3d0ac1 100644 --- a/rust/pspp/src/output/pivot/mod.rs +++ b/rust/pspp/src/output/pivot/mod.rs @@ -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, /// 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) -> 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, diff --git a/rust/pspp/src/output/pivot/output.rs b/rust/pspp/src/output/pivot/output.rs index fd63bf8174..a5f4d188bc 100644 --- a/rust/pspp/src/output/pivot/output.rs +++ b/rust/pspp/src/output/pivot/output.rs @@ -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 diff --git a/rust/pspp/src/output/pivot/test.rs b/rust/pspp/src/output/pivot/test.rs index 84c8117ac9..e86a6853f8 100644 --- a/rust/pspp/src/output/pivot/test.rs +++ b/rust/pspp/src/output/pivot/test.rs @@ -763,7 +763,7 @@ fn footnote_alphabetic_superscript() { "\ Pivot Table with Alphabetic Superscript Footnotes[*] ╭────────────┬──────────────────╮ -│ │ A[*] 1 │ +│ │ A[*] │ │ ├───────┬──────────┤ │Corner[*][b]│ B[b] │ C[*][b] │ ├────────────┼───────┼──────────┤ diff --git a/rust/pspp/src/output/pivot/tlo.rs b/rust/pspp/src/output/pivot/tlo.rs index 85648c2ade..e5de5a9df1 100644 --- a/rust/pspp/src/output/pivot/tlo.rs +++ b/rust/pspp/src/output/pivot/tlo.rs @@ -60,7 +60,7 @@ impl From 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 { diff --git a/rust/pspp/src/output/render.rs b/rust/pspp/src/output/render.rs index 6e72ddb74d..68adf5df99 100644 --- a/rust/pspp/src/output/render.rs +++ b/rust/pspp/src/output/render.rs @@ -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; diff --git a/rust/pspp/src/output/spv.rs b/rust/pspp/src/output/spv.rs index 6b4aa39c90..9635655355 100644 --- a/rust/pspp/src/output/spv.rs +++ b/rust/pspp/src/output/spv.rs @@ -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), diff --git a/rust/pspp/src/output/text.rs b/rust/pspp/src/output/text.rs index 098214c675..9094079f35 100644 --- a/rust/pspp/src/output/text.rs +++ b/rust/pspp/src/output/text.rs @@ -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(), } } } diff --git a/rust/pspp/src/sys/cooked.rs b/rust/pspp/src/sys/cooked.rs index 13be160ca5..7b4c5235f7 100644 --- a/rust/pspp/src/sys/cooked.rs +++ b/rust/pspp/src/sys/cooked.rs @@ -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)), } } diff --git a/rust/pspp/src/sys/raw.rs b/rust/pspp/src/sys/raw.rs index dbb0d3de9e..fde85784e3 100644 --- a/rust/pspp/src/sys/raw.rs +++ b/rust/pspp/src/sys/raw.rs @@ -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>); diff --git a/rust/pspp/src/sys/test.rs b/rust/pspp/src/sys/test.rs index 895ba496bd..800ba97b44 100644 --- a/rust/pspp/src/sys/test.rs +++ b/rust/pspp/src/sys/test.rs @@ -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::() { + 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 = reader.collect::, _>>().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 = reader.collect::, _>>().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, ""); } -- 2.30.2