diff --git a/Cargo.toml b/Cargo.toml index cce4f87de..0541d3c93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,11 +8,14 @@ time-core = { path = "time-core", version = "=0.1.2" } time-macros = { path = "time-macros", version = "=0.2.15" } criterion = { version = "0.5.1", default-features = false } -deranged = { version = "0.3.7", default-features = false } +deranged = { version = "0.3.9", default-features = false, features = [ + "powerfmt", +] } itoa = "1.0.1" js-sys = "0.3.58" libc = "0.2.98" num_threads = "0.1.2" +powerfmt = { version = "0.2.0", default-features = false } quickcheck = { version = "1.0.3", default-features = false } quickcheck_macros = "1.0.0" rand = { version = "0.8.4", default-features = false } diff --git a/tests/formatting.rs b/tests/formatting.rs index 5e7cc2391..671a3f24a 100644 --- a/tests/formatting.rs +++ b/tests/formatting.rs @@ -342,6 +342,9 @@ fn display_time() { assert_eq!(time!(0:00:00.000_000_1).to_string(), "0:00:00.0000001"); assert_eq!(time!(0:00:00.000_000_01).to_string(), "0:00:00.00000001"); assert_eq!(time!(0:00:00.000_000_001).to_string(), "0:00:00.000000001"); + + assert_eq!(format!("{:>12}", time!(0:00)), " 0:00:00.0"); + assert_eq!(format!("{:x^14}", time!(0:00)), "xx0:00:00.0xxx"); } #[test] @@ -464,6 +467,9 @@ fn display_offset() { assert_eq!(offset!(-23:59).to_string(), "-23:59:00"); assert_eq!(offset!(+23:59:59).to_string(), "+23:59:59"); assert_eq!(offset!(-23:59:59).to_string(), "-23:59:59"); + + assert_eq!(format!("{:>10}", offset!(UTC)), " +00:00:00"); + assert_eq!(format!("{:x^14}", offset!(UTC)), "xx+00:00:00xxx"); } #[test] diff --git a/time/Cargo.toml b/time/Cargo.toml index 3e5594d8d..959b4a138 100644 --- a/time/Cargo.toml +++ b/time/Cargo.toml @@ -51,6 +51,7 @@ wasm-bindgen = ["dep:js-sys"] [dependencies] deranged = { workspace = true } itoa = { workspace = true, optional = true } +powerfmt = { workspace = true } quickcheck = { workspace = true, optional = true } rand = { workspace = true, optional = true } serde = { workspace = true, optional = true } diff --git a/time/src/date.rs b/time/src/date.rs index 3f76adb2d..5e62dfabc 100644 --- a/time/src/date.rs +++ b/time/src/date.rs @@ -1,15 +1,18 @@ //! The [`Date`] struct and its associated `impl`s. -use core::fmt; use core::num::NonZeroI32; use core::ops::{Add, Sub}; use core::time::Duration as StdDuration; +use core::{cmp, fmt}; #[cfg(feature = "formatting")] use std::io; use deranged::RangedI32; +use powerfmt::ext::FormatterExt; +use powerfmt::smart_display::{self, FormatterOptions, Metadata, SmartDisplay}; use crate::convert::*; +use crate::ext::DigitCount; #[cfg(feature = "formatting")] use crate::formatting::Formattable; use crate::internal_macros::{ @@ -1303,29 +1306,92 @@ impl Date { } } -impl fmt::Display for Date { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if cfg!(feature = "large-dates") && self.year().abs() >= 10_000 { - write!( - f, - "{:+}-{:02}-{:02}", - self.year(), - self.month() as u8, - self.day() +mod private { + #[non_exhaustive] + #[derive(Debug, Clone, Copy)] + pub struct DateMetadata { + /// The width of the year component, including the sign. + pub(super) year_width: u8, + /// Whether the sign should be displayed. + pub(super) display_sign: bool, + pub(super) year: i32, + pub(super) month: u8, + pub(super) day: u8, + } +} +use private::DateMetadata; + +impl SmartDisplay for Date { + type Metadata = DateMetadata; + + fn metadata(&self, _: FormatterOptions) -> Metadata { + let (year, month, day) = self.to_calendar_date(); + + // There is a minimum of four digits for any year. + let mut year_width = cmp::max(year.unsigned_abs().num_digits(), 4); + let display_sign = if !(0..10_000).contains(&year) { + // An extra character is required for the sign. + year_width += 1; + true + } else { + false + }; + + let formatted_width = year_width as usize + + smart_display::padded_width_of!( + "-", + month as u8 => width(2), + "-", + day => width(2), + ); + + Metadata::new( + formatted_width, + self, + DateMetadata { + year_width, + display_sign, + year, + month: month as u8, + day, + }, + ) + } + + fn fmt_with_metadata( + &self, + f: &mut fmt::Formatter<'_>, + metadata: Metadata, + ) -> fmt::Result { + let DateMetadata { + year_width, + display_sign, + year, + month, + day, + } = *metadata; + let year_width = year_width as usize; + + if display_sign { + f.pad_with_width( + metadata.unpadded_width(), + format_args!("{year:+0year_width$}-{month:02}-{day:02}"), ) } else { - write!( - f, - "{:0width$}-{:02}-{:02}", - self.year(), - self.month() as u8, - self.day(), - width = 4 + (self.year() < 0) as usize + f.pad_with_width( + metadata.unpadded_width(), + format_args!("{year:0year_width$}-{month:02}-{day:02}"), ) } } } +impl fmt::Display for Date { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + SmartDisplay::fmt(self, f) + } +} + impl fmt::Debug for Date { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { fmt::Display::fmt(self, f) diff --git a/time/src/date_time.rs b/time/src/date_time.rs index 53cae5eb4..27f07cec8 100644 --- a/time/src/date_time.rs +++ b/time/src/date_time.rs @@ -17,6 +17,8 @@ use std::io; use std::time::SystemTime; use deranged::RangedI64; +use powerfmt::ext::FormatterExt; +use powerfmt::smart_display::{self, FormatterOptions, Metadata, SmartDisplay}; use crate::convert::*; use crate::date::{MAX_YEAR, MIN_YEAR}; @@ -911,23 +913,60 @@ impl DateTime { // endregion deprecated time getters } -impl fmt::Debug for DateTime { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - ::fmt(self, f) +// region: trait impls +mod private { + use super::*; + + #[non_exhaustive] + #[derive(Debug, Clone, Copy)] + pub struct DateTimeMetadata { + pub(super) maybe_offset: Option, + } +} +pub(crate) use private::DateTimeMetadata; + +impl SmartDisplay for DateTime { + type Metadata = DateTimeMetadata; + + fn metadata(&self, _: FormatterOptions) -> Metadata { + let maybe_offset = maybe_offset_as_offset_opt::(self.offset); + let width = match maybe_offset { + Some(offset) => smart_display::padded_width_of!(self.date, " ", self.time, " ", offset), + None => smart_display::padded_width_of!(self.date, " ", self.time), + }; + Metadata::new(width, self, DateTimeMetadata { maybe_offset }) + } + + fn fmt_with_metadata( + &self, + f: &mut fmt::Formatter<'_>, + metadata: Metadata, + ) -> fmt::Result { + match metadata.maybe_offset { + Some(offset) => f.pad_with_width( + metadata.unpadded_width(), + format_args!("{} {} {offset}", self.date, self.time), + ), + None => f.pad_with_width( + metadata.unpadded_width(), + format_args!("{} {}", self.date, self.time), + ), + } } } impl fmt::Display for DateTime { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} {}", self.date, self.time)?; - if let Some(offset) = maybe_offset_as_offset_opt::(self.offset) { - write!(f, " {offset}")?; - } - Ok(()) + SmartDisplay::fmt(self, f) + } +} + +impl fmt::Debug for DateTime { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) } } -// region: trait impls impl PartialEq for DateTime { fn eq(&self, rhs: &Self) -> bool { if O::HAS_LOGICAL_OFFSET { diff --git a/time/src/ext.rs b/time/src/ext.rs index cbc1ca881..1b89d4155 100644 --- a/time/src/ext.rs +++ b/time/src/ext.rs @@ -290,3 +290,30 @@ impl NumericalStdDuration for f64 { } } // endregion NumericalStdDuration + +// region: DigitCount +/// A trait that indicates the formatted width of the value can be determined. +/// +/// Note that this should not be implemented for any signed integers. This forces the caller to +/// write the sign if desired. +pub(crate) trait DigitCount { + /// The number of digits in the stringified value. + fn num_digits(self) -> u8; +} + +/// A macro to generate implementations of `DigitCount` for unsigned integers. +macro_rules! impl_digit_count { + ($($t:ty),* $(,)?) => { + $(impl DigitCount for $t { + fn num_digits(self) -> u8 { + match self.checked_ilog10() { + Some(n) => (n as u8) + 1, + None => 1, + } + } + })* + }; +} + +impl_digit_count!(u8, u16, u32); +// endregion DigitCount diff --git a/time/src/formatting/mod.rs b/time/src/formatting/mod.rs index a22742236..77a52305b 100644 --- a/time/src/formatting/mod.rs +++ b/time/src/formatting/mod.rs @@ -2,12 +2,12 @@ pub(crate) mod formattable; mod iso8601; - use core::num::NonZeroU8; use std::io; pub use self::formattable::Formattable; use crate::convert::*; +use crate::ext::DigitCount; use crate::format_description::{modifier, Component}; use crate::{error, Date, OffsetDateTime, Time, UtcOffset}; @@ -38,33 +38,6 @@ const WEEKDAY_NAMES: [&[u8]; 7] = [ b"Sunday", ]; -// region: extension trait -/// A trait that indicates the formatted width of the value can be determined. -/// -/// Note that this should not be implemented for any signed integers. This forces the caller to -/// write the sign if desired. -pub(crate) trait DigitCount { - /// The number of digits in the stringified value. - fn num_digits(self) -> u8; -} - -/// A macro to generate implementations of `DigitCount` for unsigned integers. -macro_rules! impl_digit_count { - ($($t:ty),* $(,)?) => { - $(impl DigitCount for $t { - fn num_digits(self) -> u8 { - match self.checked_ilog10() { - Some(n) => (n as u8) + 1, - None => 1, - } - } - })* - }; -} - -impl_digit_count!(u8, u16, u32); -// endregion extension trait - /// Write all bytes to the output, returning the number of bytes written. pub(crate) fn write(output: &mut impl io::Write, bytes: &[u8]) -> io::Result { output.write_all(bytes)?; diff --git a/time/src/month.rs b/time/src/month.rs index ea3480f00..55e53b4b8 100644 --- a/time/src/month.rs +++ b/time/src/month.rs @@ -4,6 +4,8 @@ use core::fmt; use core::num::NonZeroU8; use core::str::FromStr; +use powerfmt::smart_display::{FormatterOptions, Metadata, SmartDisplay}; + use self::Month::*; use crate::error; @@ -164,9 +166,35 @@ impl Month { } } -impl fmt::Display for Month { +mod private { + #[non_exhaustive] + #[derive(Debug, Clone, Copy)] + pub struct MonthMetadata; +} +use private::MonthMetadata; + +impl SmartDisplay for Month { + type Metadata = MonthMetadata; + + fn metadata(&self, _: FormatterOptions) -> Metadata { + match self { + January => Metadata::new(7, self, MonthMetadata), + February => Metadata::new(8, self, MonthMetadata), + March => Metadata::new(5, self, MonthMetadata), + April => Metadata::new(5, self, MonthMetadata), + May => Metadata::new(3, self, MonthMetadata), + June => Metadata::new(4, self, MonthMetadata), + July => Metadata::new(4, self, MonthMetadata), + August => Metadata::new(6, self, MonthMetadata), + September => Metadata::new(9, self, MonthMetadata), + October => Metadata::new(7, self, MonthMetadata), + November => Metadata::new(8, self, MonthMetadata), + December => Metadata::new(8, self, MonthMetadata), + } + } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { + f.pad(match self { January => "January", February => "February", March => "March", @@ -183,6 +211,12 @@ impl fmt::Display for Month { } } +impl fmt::Display for Month { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + SmartDisplay::fmt(self, f) + } +} + impl FromStr for Month { type Err = error::InvalidVariant; diff --git a/time/src/offset_date_time.rs b/time/src/offset_date_time.rs index e88097fe3..79d91a61b 100644 --- a/time/src/offset_date_time.rs +++ b/time/src/offset_date_time.rs @@ -13,7 +13,9 @@ use std::io; #[cfg(feature = "std")] use std::time::SystemTime; -use crate::date_time::offset_kind; +use powerfmt::smart_display::{FormatterOptions, Metadata, SmartDisplay}; + +use crate::date_time::{offset_kind, DateTimeMetadata}; #[cfg(feature = "formatting")] use crate::formatting::Formattable; use crate::internal_macros::{const_try, const_try_opt}; @@ -1034,9 +1036,25 @@ impl OffsetDateTime { } } +impl SmartDisplay for OffsetDateTime { + type Metadata = DateTimeMetadata; + + fn metadata(&self, f: FormatterOptions) -> Metadata { + self.0.metadata(f).reuse() + } + + fn fmt_with_metadata( + &self, + f: &mut fmt::Formatter<'_>, + metadata: Metadata, + ) -> fmt::Result { + self.0.fmt_with_metadata(f, metadata.reuse()) + } +} + impl fmt::Display for OffsetDateTime { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) + SmartDisplay::fmt(self, f) } } diff --git a/time/src/primitive_date_time.rs b/time/src/primitive_date_time.rs index 58119d702..83a94610f 100644 --- a/time/src/primitive_date_time.rs +++ b/time/src/primitive_date_time.rs @@ -6,7 +6,9 @@ use core::time::Duration as StdDuration; #[cfg(feature = "formatting")] use std::io; -use crate::date_time::offset_kind; +use powerfmt::smart_display::{FormatterOptions, Metadata, SmartDisplay}; + +use crate::date_time::{offset_kind, DateTimeMetadata}; #[cfg(feature = "formatting")] use crate::formatting::Formattable; use crate::internal_macros::{const_try, const_try_opt}; @@ -807,9 +809,25 @@ impl PrimitiveDateTime { } } +impl SmartDisplay for PrimitiveDateTime { + type Metadata = DateTimeMetadata; + + fn metadata(&self, f: FormatterOptions) -> Metadata { + self.0.metadata(f).reuse() + } + + fn fmt_with_metadata( + &self, + f: &mut fmt::Formatter<'_>, + metadata: Metadata, + ) -> fmt::Result { + self.0.fmt_with_metadata(f, metadata.reuse()) + } +} + impl fmt::Display for PrimitiveDateTime { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) + SmartDisplay::fmt(self, f) } } diff --git a/time/src/tests.rs b/time/src/tests.rs index e637ba03d..2f9dbfe56 100644 --- a/time/src/tests.rs +++ b/time/src/tests.rs @@ -27,7 +27,7 @@ use std::num::NonZeroU8; -use crate::formatting::DigitCount; +use crate::ext::DigitCount; use crate::parsing::combinator::rfc::iso8601; use crate::parsing::shim::Integer; use crate::{duration, parsing}; diff --git a/time/src/time.rs b/time/src/time.rs index 74ab2a5ed..97659e06b 100644 --- a/time/src/time.rs +++ b/time/src/time.rs @@ -7,6 +7,8 @@ use core::time::Duration as StdDuration; use std::io; use deranged::{RangedU32, RangedU8}; +use powerfmt::ext::FormatterExt; +use powerfmt::smart_display::{self, FormatterOptions, Metadata, SmartDisplay}; use crate::convert::*; #[cfg(feature = "formatting")] @@ -756,9 +758,24 @@ impl Time { } } -impl fmt::Display for Time { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let (value, width) = match self.nanosecond() { +mod private { + #[non_exhaustive] + #[derive(Debug, Clone, Copy)] + pub struct TimeMetadata { + /// How many characters wide the formatted subsecond is. + pub(super) subsecond_width: u8, + /// The value to use when formatting the subsecond. Leading zeroes will be added as + /// necessary. + pub(super) subsecond_value: u32, + } +} +use private::TimeMetadata; + +impl SmartDisplay for Time { + type Metadata = TimeMetadata; + + fn metadata(&self, _: FormatterOptions) -> Metadata { + let (subsecond_value, subsecond_width) = match self.nanosecond() { nanos if nanos % 10 != 0 => (nanos, 9), nanos if (nanos / 10) % 10 != 0 => (nanos / 10, 8), nanos if (nanos / 100) % 10 != 0 => (nanos / 100, 7), @@ -769,12 +786,48 @@ impl fmt::Display for Time { nanos if (nanos / 10_000_000) % 10 != 0 => (nanos / 10_000_000, 2), nanos => (nanos / 100_000_000, 1), }; - write!( - f, - "{}:{:02}:{:02}.{value:0width$}", - self.hour, self.minute, self.second, + + let formatted_width = smart_display::padded_width_of!( + self.hour.get(), + ":", + self.minute.get() => width(2) fill('0'), + ":", + self.second.get() => width(2) fill('0'), + ".", + ) + subsecond_width; + + Metadata::new( + formatted_width, + self, + TimeMetadata { + subsecond_width: subsecond_width as _, + subsecond_value, + }, ) } + + fn fmt_with_metadata( + &self, + f: &mut fmt::Formatter<'_>, + metadata: Metadata, + ) -> fmt::Result { + let subsecond_width = metadata.subsecond_width as usize; + let subsecond_value = metadata.subsecond_value; + + f.pad_with_width( + metadata.unpadded_width(), + format_args!( + "{}:{:02}:{:02}.{subsecond_value:0subsecond_width$}", + self.hour, self.minute, self.second + ), + ) + } +} + +impl fmt::Display for Time { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + SmartDisplay::fmt(self, f) + } } impl fmt::Debug for Time { diff --git a/time/src/utc_offset.rs b/time/src/utc_offset.rs index 86a937d7a..f4e6eaa2b 100644 --- a/time/src/utc_offset.rs +++ b/time/src/utc_offset.rs @@ -6,6 +6,8 @@ use core::ops::Neg; use std::io; use deranged::{RangedI32, RangedI8}; +use powerfmt::ext::FormatterExt; +use powerfmt::smart_display::{self, FormatterOptions, Metadata, SmartDisplay}; use crate::convert::*; use crate::error; @@ -396,16 +398,50 @@ impl UtcOffset { } } +mod private { + #[non_exhaustive] + #[derive(Debug, Clone, Copy)] + pub struct UtcOffsetMetadata; +} +use private::UtcOffsetMetadata; + +impl SmartDisplay for UtcOffset { + type Metadata = UtcOffsetMetadata; + + fn metadata(&self, _: FormatterOptions) -> Metadata { + let sign = if self.is_negative() { '-' } else { '+' }; + let width = smart_display::padded_width_of!( + sign, + self.hours.abs() => width(2), + ":", + self.minutes.abs() => width(2), + ":", + self.seconds.abs() => width(2), + ); + Metadata::new(width, self, UtcOffsetMetadata) + } + + fn fmt_with_metadata( + &self, + f: &mut fmt::Formatter<'_>, + metadata: Metadata, + ) -> fmt::Result { + f.pad_with_width( + metadata.unpadded_width(), + format_args!( + "{}{:02}:{:02}:{:02}", + if self.is_negative() { '-' } else { '+' }, + self.hours.abs(), + self.minutes.abs(), + self.seconds.abs(), + ), + ) + } +} + impl fmt::Display for UtcOffset { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}{:02}:{:02}:{:02}", - if self.is_negative() { '-' } else { '+' }, - self.hours.abs(), - self.minutes.abs(), - self.seconds.abs(), - ) + SmartDisplay::fmt(self, f) } } diff --git a/time/src/weekday.rs b/time/src/weekday.rs index 07642498d..543ecb22c 100644 --- a/time/src/weekday.rs +++ b/time/src/weekday.rs @@ -1,10 +1,11 @@ //! Days of the week. -use core::fmt::{self, Display}; +use core::fmt; use core::str::FromStr; -use Weekday::*; +use powerfmt::smart_display::{FormatterOptions, Metadata, SmartDisplay}; +use self::Weekday::*; use crate::error; /// Days of the week. @@ -160,9 +161,30 @@ impl Weekday { } } -impl Display for Weekday { +mod private { + #[non_exhaustive] + #[derive(Debug, Clone, Copy)] + pub struct WeekdayMetadata; +} +use private::WeekdayMetadata; + +impl SmartDisplay for Weekday { + type Metadata = WeekdayMetadata; + + fn metadata(&self, _: FormatterOptions) -> Metadata<'_, Self> { + match self { + Monday => Metadata::new(6, self, WeekdayMetadata), + Tuesday => Metadata::new(7, self, WeekdayMetadata), + Wednesday => Metadata::new(9, self, WeekdayMetadata), + Thursday => Metadata::new(8, self, WeekdayMetadata), + Friday => Metadata::new(6, self, WeekdayMetadata), + Saturday => Metadata::new(8, self, WeekdayMetadata), + Sunday => Metadata::new(6, self, WeekdayMetadata), + } + } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { + f.pad(match self { Monday => "Monday", Tuesday => "Tuesday", Wednesday => "Wednesday", @@ -174,6 +196,12 @@ impl Display for Weekday { } } +impl fmt::Display for Weekday { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + SmartDisplay::fmt(self, f) + } +} + impl FromStr for Weekday { type Err = error::InvalidVariant;