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