diff --git a/src/format/formatting.rs b/src/format/formatting.rs index e27b76406..4843d131c 100644 --- a/src/format/formatting.rs +++ b/src/format/formatting.rs @@ -127,89 +127,58 @@ impl<'a, I: Iterator + Clone, B: Borrow>> DelayedFormat { fn format_numeric(&self, w: &mut impl Write, spec: &Numeric, pad: Pad) -> fmt::Result { use self::Numeric::*; - fn write_one(w: &mut impl Write, v: u8) -> fmt::Result { - w.write_char((b'0' + v) as char) - } - - fn write_two(w: &mut impl Write, v: u8, pad: Pad) -> fmt::Result { - let ones = b'0' + v % 10; - match (v / 10, pad) { - (0, Pad::None) => {} - (0, Pad::Space) => w.write_char(' ')?, - (tens, _) => w.write_char((b'0' + tens) as char)?, + // unpack padding width if provided + let (spec, mut pad_width) = spec.unwrap_padding(); + + let (value, default_pad_width, is_year) = match (spec, self.date, self.time) { + (Year, Some(d), _) => (d.year() as i64, 4, true), + (YearDiv100, Some(d), _) => (d.year().div_euclid(100) as i64, 2, false), + (YearMod100, Some(d), _) => (d.year().rem_euclid(100) as i64, 2, false), + (IsoYear, Some(d), _) => (d.iso_week().year() as i64, 4, true), + (IsoYearDiv100, Some(d), _) => (d.iso_week().year().div_euclid(100) as i64, 2, false), + (IsoYearMod100, Some(d), _) => (d.iso_week().year().rem_euclid(100) as i64, 2, false), + (Quarter, Some(d), _) => (d.quarter() as i64, 1, false), + (Month, Some(d), _) => (d.month() as i64, 2, false), + (Day, Some(d), _) => (d.day() as i64, 2, false), + (WeekFromSun, Some(d), _) => (d.weeks_from(Weekday::Sun) as i64, 2, false), + (WeekFromMon, Some(d), _) => (d.weeks_from(Weekday::Mon) as i64, 2, false), + (IsoWeek, Some(d), _) => (d.iso_week().week() as i64, 2, false), + (NumDaysFromSun, Some(d), _) => (d.weekday().num_days_from_sunday() as i64, 1, false), + (WeekdayFromMon, Some(d), _) => (d.weekday().number_from_monday() as i64, 1, false), + (Ordinal, Some(d), _) => (d.ordinal() as i64, 3, false), + (Hour, _, Some(t)) => (t.hour() as i64, 2, false), + (Hour12, _, Some(t)) => (t.hour12().1 as i64, 2, false), + (Minute, _, Some(t)) => (t.minute() as i64, 2, false), + (Second, _, Some(t)) => { + ((t.second() + t.nanosecond() / 1_000_000_000) as i64, 2, false) } - w.write_char(ones as char) - } - - #[inline] - fn write_year(w: &mut impl Write, year: i32, pad: Pad) -> fmt::Result { - if (1000..=9999).contains(&year) { - // fast path - write_hundreds(w, (year / 100) as u8)?; - write_hundreds(w, (year % 100) as u8) - } else { - write_n(w, 4, year as i64, pad, !(0..10_000).contains(&year)) + (Nanosecond, _, Some(t)) => ((t.nanosecond() % 1_000_000_000) as i64, 9, false), + (Timestamp, Some(d), Some(t)) => { + let offset = self.off.as_ref().map(|(_, o)| i64::from(o.local_minus_utc())); + (d.and_time(t).and_utc().timestamp() - offset.unwrap_or(0), 9, false) } - } + (Internal(_), _, _) => return Ok(()), // for future expansion + (Padded { .. }, _, _) => return Err(fmt::Error), // should be unwrapped above + _ => return Err(fmt::Error), // insufficient arguments for given format + }; - fn write_n( - w: &mut impl Write, - n: usize, - v: i64, - pad: Pad, - always_sign: bool, - ) -> fmt::Result { - if always_sign { - match pad { - Pad::None => write!(w, "{:+}", v), - Pad::Zero => write!(w, "{:+01$}", v, n + 1), - Pad::Space => write!(w, "{:+1$}", v, n + 1), - } - } else { - match pad { - Pad::None => write!(w, "{}", v), - Pad::Zero => write!(w, "{:01$}", v, n), - Pad::Space => write!(w, "{:1$}", v, n), - } - } + if pad_width == 0 { + pad_width = default_pad_width; } - match (spec, self.date, self.time) { - (Year, Some(d), _) => write_year(w, d.year(), pad), - (YearDiv100, Some(d), _) => write_two(w, d.year().div_euclid(100) as u8, pad), - (YearMod100, Some(d), _) => write_two(w, d.year().rem_euclid(100) as u8, pad), - (IsoYear, Some(d), _) => write_year(w, d.iso_week().year(), pad), - (IsoYearDiv100, Some(d), _) => { - write_two(w, d.iso_week().year().div_euclid(100) as u8, pad) - } - (IsoYearMod100, Some(d), _) => { - write_two(w, d.iso_week().year().rem_euclid(100) as u8, pad) - } - (Quarter, Some(d), _) => write_one(w, d.quarter() as u8), - (Month, Some(d), _) => write_two(w, d.month() as u8, pad), - (Day, Some(d), _) => write_two(w, d.day() as u8, pad), - (WeekFromSun, Some(d), _) => write_two(w, d.weeks_from(Weekday::Sun) as u8, pad), - (WeekFromMon, Some(d), _) => write_two(w, d.weeks_from(Weekday::Mon) as u8, pad), - (IsoWeek, Some(d), _) => write_two(w, d.iso_week().week() as u8, pad), - (NumDaysFromSun, Some(d), _) => write_one(w, d.weekday().num_days_from_sunday() as u8), - (WeekdayFromMon, Some(d), _) => write_one(w, d.weekday().number_from_monday() as u8), - (Ordinal, Some(d), _) => write_n(w, 3, d.ordinal() as i64, pad, false), - (Hour, _, Some(t)) => write_two(w, t.hour() as u8, pad), - (Hour12, _, Some(t)) => write_two(w, t.hour12().1 as u8, pad), - (Minute, _, Some(t)) => write_two(w, t.minute() as u8, pad), - (Second, _, Some(t)) => { - write_two(w, (t.second() + t.nanosecond() / 1_000_000_000) as u8, pad) - } - (Nanosecond, _, Some(t)) => { - write_n(w, 9, (t.nanosecond() % 1_000_000_000) as i64, pad, false) + let always_sign = is_year && !(0..10_000).contains(&value); + if always_sign { + match pad { + Pad::None => write!(w, "{:+}", value), + Pad::Zero => write!(w, "{:+01$}", value, pad_width + 1), + Pad::Space => write!(w, "{:+1$}", value, pad_width + 1), } - (Timestamp, Some(d), Some(t)) => { - let offset = self.off.as_ref().map(|(_, o)| i64::from(o.local_minus_utc())); - let timestamp = d.and_time(t).and_utc().timestamp() - offset.unwrap_or(0); - write_n(w, 9, timestamp, pad, false) + } else { + match pad { + Pad::None => write!(w, "{}", value), + Pad::Zero => write!(w, "{:01$}", value, pad_width), + Pad::Space => write!(w, "{:1$}", value, pad_width), } - (Internal(_), _, _) => Ok(()), // for future expansion - _ => Err(fmt::Error), // insufficient arguments for given format } } diff --git a/src/format/mod.rs b/src/format/mod.rs index 241be7a1d..072a82aa0 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -33,6 +33,8 @@ #[cfg(all(feature = "alloc", not(feature = "std"), not(test)))] use alloc::boxed::Box; +#[cfg(feature = "alloc")] +use alloc::sync::Arc; use core::fmt; use core::str::FromStr; #[cfg(feature = "std")] @@ -149,6 +151,15 @@ pub enum Numeric { /// For formatting, it assumes UTC upon the absence of time zone offset. Timestamp, + #[cfg(feature = "alloc")] + /// An extension to carry the width of the padding. + Padded { + /// The numeric to be padded. + numeric: Arc, + /// The width of the padding. + width: usize, + }, + /// Internal uses only. /// /// This item exists so that one can add additional internal-only formatting @@ -156,6 +167,55 @@ pub enum Numeric { Internal(InternalNumeric), } +#[cfg(feature = "alloc")] +impl Numeric { + /// Adds the with of the padding to the numeric + /// + /// Should be removed if the padding width is added to the `Pad` enum. + pub fn with_padding(self, width: usize) -> Self { + if width != 0 { + // update padding + match self { + Numeric::Padded { numeric, .. } => Numeric::Padded { numeric, width }, + numeric => Numeric::Padded { numeric: Arc::new(numeric), width }, + } + } else { + // remove padding + match self { + Numeric::Padded { numeric, .. } => numeric.as_ref().clone(), + numeric => numeric, + } + } + } + + /// Gets the numeric and padding width from the numeric + /// + /// Should be removed if the padding width is added to the `Pad` enum. + pub fn unwrap_padding(&self) -> (&Self, usize) { + match self { + Numeric::Padded { numeric, width } => (numeric.as_ref(), *width), + numeric => (numeric, 0), + } + } +} + +#[cfg(not(feature = "alloc"))] +impl Numeric { + /// Adds the with of the padding to the numeric + /// + /// Should be removed if the padding width is added to the `Pad` enum. + pub fn with_padding(self, _width: usize) -> Self { + self + } + + /// Gets the numeric and padding width from the numeric + /// + /// Should be removed if the padding width is added to the `Pad` enum. + pub fn unwrap_padding(&self) -> (&Self, usize) { + (self, 0) + } +} + /// An opaque type representing numeric item types for internal uses only. #[derive(Clone, Eq, Hash, PartialEq)] pub struct InternalNumeric { @@ -555,3 +615,46 @@ impl FromStr for Month { } } } + +#[cfg(test)] +mod tests { + use crate::format::*; + + #[test] + #[cfg(feature = "alloc")] + fn test_numeric_with_padding() { + // No padding + assert_eq!(Numeric::Year.with_padding(0), Numeric::Year); + + // Add padding + assert_eq!( + Numeric::Year.with_padding(5), + Numeric::Padded { numeric: Arc::new(Numeric::Year), width: 5 } + ); + + // Update padding + assert_eq!( + Numeric::Year.with_padding(5).with_padding(10), + Numeric::Padded { numeric: Arc::new(Numeric::Year), width: 10 } + ); + + // Remove padding + assert_eq!(Numeric::Year.with_padding(5).with_padding(0), Numeric::Year); + } + + #[test] + #[cfg(not(feature = "alloc"))] + fn test_numeric_with_padding_disabled() { + // No padding + assert_eq!(Numeric::Year.with_padding(0), Numeric::Year); + + // Add padding + assert_eq!(Numeric::Year.with_padding(5), Numeric::Year); + + // Update padding + assert_eq!(Numeric::Year.with_padding(5).with_padding(10), Numeric::Year); + + // Remove padding + assert_eq!(Numeric::Year.with_padding(5).with_padding(0), Numeric::Year); + } +} diff --git a/src/format/parse.rs b/src/format/parse.rs index 40b5b0524..e67d01499 100644 --- a/src/format/parse.rs +++ b/src/format/parse.rs @@ -7,6 +7,8 @@ use core::borrow::Borrow; use core::str; +#[cfg(feature = "alloc")] +use super::ParseErrorKind::BadFormat; use super::scan; use super::{BAD_FORMAT, INVALID, OUT_OF_RANGE, TOO_LONG, TOO_SHORT}; use super::{Fixed, InternalFixed, InternalInternal, Item, Numeric, Pad, Parsed}; @@ -359,7 +361,9 @@ where Nanosecond => (9, false, Parsed::set_nanosecond), Timestamp => (usize::MAX, false, Parsed::set_timestamp), - // for the future expansion + #[cfg(feature = "alloc")] + Padded { .. } => return Err(ParseError(BadFormat)), + Internal(ref int) => match int._dummy {}, }; @@ -1580,6 +1584,88 @@ mod tests { ); } + #[test] + #[rustfmt::skip] + #[cfg(feature = "alloc")] + fn test_parse_padded() { + use crate::format::InternalInternal::*; + use crate::format::Item::{Literal, Space}; + use crate::format::Numeric::*; + use crate::format::ParseErrorKind::BadFormat; + + check( + "2000-01-02 03:04:05Z", + &[ + nums(Year.with_padding(5)) + ], + Err(ParseError(BadFormat)), + ); + + check( + "2000-01-02 03:04:05Z", + &[ + nums(Year.with_padding(5)), Literal("-"), num(Month), Literal("-"), num(Day), Space(" "), + num(Hour), Literal(":"), num(Minute), Literal(":"), num(Second), + internal_fixed(TimezoneOffsetPermissive) + ], + Err(ParseError(BadFormat)), + ); + + check( + "2000-01-02 03:04:05Z", + &[ + num(Year), Literal("-"), num(Month), Literal("-"), nums(Day.with_padding(5)), Space(" "), + num(Hour), Literal(":"), num(Minute), Literal(":"), num(Second), + internal_fixed(TimezoneOffsetPermissive) + ], + Err(ParseError(BadFormat)), + ); + } + + #[test] + #[rustfmt::skip] + #[cfg(not(feature = "alloc"))] + fn test_parse_padded_disabled() { + use crate::format::InternalInternal::*; + use crate::format::Item::{Literal, Space}; + use crate::format::Numeric::*; + use crate::format::ParseErrorKind::TooLong; + + check( + "2000-01-02 03:04:05Z", + &[ + nums(Year.with_padding(5)) + ], + Err(ParseError(TooLong)), + ); + + check( + "2000-01-02 03:04:05Z", + &[ + nums(Year.with_padding(5)), Literal("-"), num(Month), Literal("-"), num(Day), Space(" "), + num(Hour), Literal(":"), num(Minute), Literal(":"), num(Second), + internal_fixed(TimezoneOffsetPermissive) + ], + parsed!( + year: 2000, month: 1, day: 2, hour_div_12: 0, hour_mod_12: 3, minute: 4, second: 5, + offset: 0 + ), + ); + + check( + "2000-01-02 03:04:05Z", + &[ + num(Year), Literal("-"), num(Month), Literal("-"), nums(Day.with_padding(5)), Space(" "), + num(Hour), Literal(":"), num(Minute), Literal(":"), num(Second), + internal_fixed(TimezoneOffsetPermissive) + ], + parsed!( + year: 2000, month: 1, day: 2, hour_div_12: 0, hour_mod_12: 3, minute: 4, second: 5, + offset: 0 + ), + ); + } + #[track_caller] fn parses(s: &str, items: &[Item]) { let mut parsed = Parsed::new(); diff --git a/src/format/strftime.rs b/src/format/strftime.rs index f0478b43a..dca00b44a 100644 --- a/src/format/strftime.rs +++ b/src/format/strftime.rs @@ -93,8 +93,10 @@ This is not allowed for other specifiers and will result in the `BAD_FORMAT` err Modifier | Description -------- | ----------- `%-?` | Suppresses any padding including spaces and zeroes. (e.g. `%j` = `012`, `%-j` = `12`) -`%_?` | Uses spaces as a padding. (e.g. `%j` = `012`, `%_j` = ` 12`) -`%0?` | Uses zeroes as a padding. (e.g. `%e` = ` 9`, `%0e` = `09`) +`%_?` | Uses spaces as a padding. (e.g. `%j` = `012`, `%_j` = ` 12`) +`%_X?` | Uses spaces as a padding. (e.g. `%j` = `012`, `%_4j` = ` 12`) +`%0?` | Uses zeroes as a padding. (e.g. `%e` = ` 9`, `%0e` = `09`) +`%0X?` | Uses zeroes as a padding. (e.g. `%e` = ` 9`, `%03e` = `009`) Notes: @@ -468,18 +470,25 @@ impl<'a> StrftimeItems<'a> { }; } - let spec = next!(); - let pad_override = match spec { - '-' => Some(Pad::None), - '0' => Some(Pad::Zero), - '_' => Some(Pad::Space), - _ => None, + let (padding, is_alternate, mut spec) = match next!() { + '-' => (Some(Pad::None), false, next!()), + '0' => (Some(Pad::Zero), false, next!()), + '_' => (Some(Pad::Space), false, next!()), + '#' => (None, true, next!()), + spec => (None, false, spec), }; - let is_alternate = spec == '#'; - let spec = if pad_override.is_some() || is_alternate { next!() } else { spec }; if is_alternate && !HAVE_ALTERNATES.contains(spec) { return Some((remainder, Item::Error)); } + let mut padding_width: usize = 0; + if padding.is_some() { + // try parse padding width + while let Some(digit) = spec.to_digit(10) { + padding_width *= 10; + padding_width += digit as usize; + spec = next!(); + } + }; macro_rules! queue { [$head:expr, $($tail:expr),+ $(,)*] => ({ @@ -628,11 +637,12 @@ impl<'a> StrftimeItems<'a> { // Adjust `item` if we have any padding modifier. // Not allowed on non-numeric items or on specifiers composed out of multiple // formatting items. - if let Some(new_pad) = pad_override { + if let Some(new_pad) = padding { match item { - Item::Numeric(ref kind, _pad) if self.queue.is_empty() => { - Some((remainder, Item::Numeric(kind.clone(), new_pad))) - } + Item::Numeric(kind, _pad) if self.queue.is_empty() => Some(( + remainder, + Item::Numeric(kind.with_padding(padding_width), new_pad), + )), _ => Some((remainder, Item::Error)), } } else { @@ -833,12 +843,20 @@ mod tests { assert_eq!(parse_and_collect("%:j"), [Item::Error]); assert_eq!(parse_and_collect("%-j"), [num(Ordinal)]); assert_eq!(parse_and_collect("%0j"), [num0(Ordinal)]); + assert_eq!(parse_and_collect("%05j"), [num0(Ordinal.with_padding(5))]); + assert_eq!(parse_and_collect("%010j"), [num0(Ordinal.with_padding(10))]); assert_eq!(parse_and_collect("%_j"), [nums(Ordinal)]); + assert_eq!(parse_and_collect("%_5j"), [nums(Ordinal.with_padding(5))]); + assert_eq!(parse_and_collect("%_10j"), [nums(Ordinal.with_padding(10))]); assert_eq!(parse_and_collect("%.e"), [Item::Error]); assert_eq!(parse_and_collect("%:e"), [Item::Error]); assert_eq!(parse_and_collect("%-e"), [num(Day)]); assert_eq!(parse_and_collect("%0e"), [num0(Day)]); + assert_eq!(parse_and_collect("%05e"), [num0(Day.with_padding(5))]); + assert_eq!(parse_and_collect("%010e"), [num0(Day.with_padding(10))]); assert_eq!(parse_and_collect("%_e"), [nums(Day)]); + assert_eq!(parse_and_collect("%_5e"), [nums(Day.with_padding(5))]); + assert_eq!(parse_and_collect("%_10e"), [nums(Day.with_padding(10))]); assert_eq!(parse_and_collect("%z"), [fixed(Fixed::TimezoneOffset)]); assert_eq!(parse_and_collect("%:z"), [fixed(Fixed::TimezoneOffsetColon)]); assert_eq!(parse_and_collect("%Z"), [fixed(Fixed::TimezoneName)]); @@ -851,6 +869,50 @@ mod tests { assert_eq!(parse_and_collect("%#m"), [Item::Error]); } + #[cfg(feature = "alloc")] + fn test_padding( + dt: DateTime, + format_string: &str, + default_pad_width: usize, + expected_base: &str, + ) { + let format = format!("%-{format_string}"); + let expected = expected_base; + assert_eq!(dt.format(&format).to_string(), expected, "with format '{}'", format); + + let format = format!("%-5{format_string}"); + let expected = expected_base; + assert_eq!(dt.format(&format).to_string(), expected, "with format '{}'", format); + + let format = format!("%-10{format_string}"); + let expected = expected_base; + assert_eq!(dt.format(&format).to_string(), expected, "with format '{}'", format); + + let format = format!("%_{format_string}"); + let expected = format!("{:>1$}", expected_base, default_pad_width); + assert_eq!(dt.format(&format).to_string(), expected, "with format '{}'", format); + + let format = format!("%_5{format_string}"); + let expected = format!("{:>1$}", expected_base, 5); + assert_eq!(dt.format(&format).to_string(), expected, "with format '{}'", format); + + let format = format!("%_10{format_string}"); + let expected = format!("{:>1$}", expected_base, 10); + assert_eq!(dt.format(&format).to_string(), expected, "with format '{}'", format); + + let format = format!("%0{format_string}"); + let expected = format!("{:0>1$}", expected_base, default_pad_width); + assert_eq!(dt.format(&format).to_string(), expected, "with format '{}'", format); + + let format = format!("%05{format_string}"); + let expected = format!("{:0>1$}", expected_base, 5); + assert_eq!(dt.format(&format).to_string(), expected, "with format '{}'", format); + + let format = format!("%010{format_string}"); + let expected = format!("{:0>1$}", expected_base, 10); + assert_eq!(dt.format(&format).to_string(), expected, "with format '{}'", format); + } + #[test] #[cfg(feature = "alloc")] fn test_strftime_docs() { @@ -891,6 +953,22 @@ mod tests { assert_eq!(dt.format("%F").to_string(), "2001-07-08"); assert_eq!(dt.format("%v").to_string(), " 8-Jul-2001"); + test_padding(dt, "Y", 4, "2001"); + test_padding(dt, "C", 2, "20"); + test_padding(dt, "y", 2, "1"); + test_padding(dt, "q", 1, "3"); + test_padding(dt, "m", 2, "7"); + test_padding(dt, "d", 2, "8"); + test_padding(dt, "e", 2, "8"); + test_padding(dt, "w", 1, "0"); + test_padding(dt, "u", 1, "7"); + test_padding(dt, "U", 2, "27"); + test_padding(dt, "W", 2, "27"); + test_padding(dt, "G", 4, "2001"); + test_padding(dt, "g", 2, "1"); + test_padding(dt, "V", 2, "27"); + test_padding(dt, "j", 3, "189"); + // time specifiers assert_eq!(dt.format("%H").to_string(), "00"); assert_eq!(dt.format("%k").to_string(), " 0"); @@ -916,6 +994,15 @@ mod tests { assert_eq!(dt.format("%X").to_string(), "00:34:60"); assert_eq!(dt.format("%r").to_string(), "12:34:60 AM"); + test_padding(dt, "H", 2, "0"); + test_padding(dt, "k", 2, "0"); + test_padding(dt, "I", 2, "12"); + test_padding(dt, "l", 2, "12"); + test_padding(dt, "l", 2, "12"); + test_padding(dt, "M", 2, "34"); + test_padding(dt, "S", 2, "60"); + test_padding(dt, "f", 9, "26490708"); + // time zone specifiers //assert_eq!(dt.format("%Z").to_string(), "ACST"); assert_eq!(dt.format("%z").to_string(), "+0930"); @@ -1112,7 +1199,16 @@ mod tests { } #[test] - #[cfg(all(feature = "unstable-locales", target_pointer_width = "64"))] + #[cfg(all(feature = "unstable-locales", feature = "alloc", target_pointer_width = "64"))] + fn test_type_sizes() { + use core::mem::size_of; + assert_eq!(size_of::(), 32); + assert_eq!(size_of::(), 56); + assert_eq!(size_of::(), 2); + } + + #[test] + #[cfg(all(feature = "unstable-locales", not(feature = "alloc"), target_pointer_width = "64"))] fn test_type_sizes() { use core::mem::size_of; assert_eq!(size_of::(), 24); @@ -1121,7 +1217,16 @@ mod tests { } #[test] - #[cfg(all(feature = "unstable-locales", target_pointer_width = "32"))] + #[cfg(all(feature = "unstable-locales", feature = "alloc", target_pointer_width = "32"))] + fn test_type_sizes() { + use core::mem::size_of; + assert_eq!(size_of::(), 16); + assert_eq!(size_of::(), 28); + assert_eq!(size_of::(), 2); + } + + #[test] + #[cfg(all(feature = "unstable-locales", not(feature = "alloc"), target_pointer_width = "32"))] fn test_type_sizes() { use core::mem::size_of; assert_eq!(size_of::(), 12);