From cf16454611ea454c25b0f2b855e057118abc1509 Mon Sep 17 00:00:00 2001 From: Jacob Pratt Date: Sun, 27 Aug 2023 02:36:20 -0400 Subject: [PATCH] Implement `[end]` component --- tests/error.rs | 13 ++++++++ tests/formatting.rs | 7 ++++ tests/macros.rs | 4 +++ tests/main.rs | 10 +++--- tests/meta.rs | 2 +- tests/parse_format_description.rs | 4 +++ tests/parsing.rs | 33 +++++++++++++++++++ .../src/format_description/format_item.rs | 2 ++ .../format_description/public/component.rs | 1 + .../src/format_description/public/modifier.rs | 12 ++++--- time/src/error/parse_from_description.rs | 5 +++ time/src/format_description/component.rs | 3 ++ time/src/format_description/modifier.rs | 9 +++++ .../format_description/parse/format_item.rs | 3 ++ time/src/formatting/mod.rs | 1 + time/src/parsing/component.rs | 12 +++++++ time/src/parsing/parsed.rs | 22 +++++++------ 17 files changed, 124 insertions(+), 19 deletions(-) diff --git a/tests/error.rs b/tests/error.rs index df1bf7fb38..b57e0b70a7 100644 --- a/tests/error.rs +++ b/tests/error.rs @@ -44,6 +44,15 @@ fn unexpected_trailing_characters() -> Parse { Time::parse("a", format_description!("")).unwrap_err() } +fn unexpected_trailing_characters_from_description() -> ParseFromDescription { + match Time::parse("0", format_description!("[end]")) { + Err(Parse::ParseFromDescription( + err @ ParseFromDescription::UnexpectedTrailingCharacters { .. }, + )) => err, + _ => panic!("unexpected result"), + } +} + fn invalid_format_description() -> InvalidFormatDescription { format_description::parse("[").unwrap_err() } @@ -95,6 +104,10 @@ fn display() { ParseFromDescription::InvalidComponent("a"), Parse::from(ParseFromDescription::InvalidComponent("a")) ); + assert_display_eq!( + unexpected_trailing_characters_from_description(), + Parse::from(unexpected_trailing_characters_from_description()) + ); assert_display_eq!( component_range(), Parse::from(TryFromParsed::from(component_range())) diff --git a/tests/formatting.rs b/tests/formatting.rs index 73a3ec571d..0b0bcffa14 100644 --- a/tests/formatting.rs +++ b/tests/formatting.rs @@ -739,6 +739,13 @@ fn ignore() -> time::Result<()> { Ok(()) } +#[test] +fn end() -> time::Result<()> { + assert_eq!(Time::MIDNIGHT.format(fd!("[end]"))?, ""); + + Ok(()) +} + #[test] fn unix_timestamp() -> time::Result<()> { let dt = datetime!(2009-02-13 23:31:30.123456789 UTC); diff --git a/tests/macros.rs b/tests/macros.rs index 681b54c735..fb346927b3 100644 --- a/tests/macros.rs +++ b/tests/macros.rs @@ -309,6 +309,10 @@ fn format_description_coverage() { } )))] ); + assert_eq!( + format_description!("[end]"), + &[FormatItem::Component(Component::End(modifier!(End)))] + ); } #[test] diff --git a/tests/main.rs b/tests/main.rs index 14b2066011..3714042a69 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -109,11 +109,13 @@ require_all_features! { /// Construct a non-exhaustive modifier. macro_rules! modifier { - ($name:ident { - $($field:ident $(: $value:expr)?),+ $(,)? - }) => {{ + ($name:ident $({ + $($field:ident $(: $value:expr)?),* $(,)? + })?) => {{ + // Needed for when there are no fields. + #[allow(unused_mut)] let mut value = ::time::format_description::modifier::$name::default(); - $(value.$field = modifier!(@value $field $($value)?);)+ + $($(value.$field = modifier!(@value $field $($value)?);)*)? value }}; diff --git a/tests/meta.rs b/tests/meta.rs index 31caf6b4d2..28438fd82b 100644 --- a/tests/meta.rs +++ b/tests/meta.rs @@ -155,7 +155,7 @@ fn size() { assert_size!(error::Format, 24, 24); assert_size!(error::InvalidFormatDescription, 48, 48); assert_size!(error::Parse, 48, 48); - assert_size!(error::ParseFromDescription, 16, 24); + assert_size!(error::ParseFromDescription, 24, 24); assert_size!(error::TryFromParsed, 48, 48); assert_size!(Component, 6, 6); // TODO Size is 4 starting with rustc 1.71. assert_size!(FormatItem<'_>, 24, 24); diff --git a/tests/parse_format_description.rs b/tests/parse_format_description.rs index 9e3334ee59..42b54563ef 100644 --- a/tests/parse_format_description.rs +++ b/tests/parse_format_description.rs @@ -192,6 +192,10 @@ fn simple_component() { } )))]) ); + assert_eq!( + format_description::parse("[end]"), + Ok(vec![FormatItem::Component(Component::End(modifier!(End)))]) + ); assert_eq!( format_description::parse("[hour]"), Ok(vec![FormatItem::Component(Component::Hour(modifier!( diff --git a/tests/parsing.rs b/tests/parsing.rs index 08cc11c1af..98840b7f9e 100644 --- a/tests/parsing.rs +++ b/tests/parsing.rs @@ -1645,3 +1645,36 @@ fn issue_601() { assert_eq!(date, datetime!(2009-02-13 23:31:30.123 +00:00:00)); } + +#[test] +fn end() -> time::Result<()> { + let mut parsed = Parsed::new(); + let remaining_input = parsed.parse_item( + b"", + &FormatItem::Component(Component::End(modifier::End::default())), + ); + assert_eq!(remaining_input, Ok(b"".as_slice())); + + assert_eq!( + Time::parse("00:00", &fd::parse("[hour]:[minute][end]")?), + Ok(time!(0:00)) + ); + assert_eq!( + Time::parse( + "00:00:00", + &fd::parse_owned::<2>("[hour]:[minute][optional [[end]]]:[second]")? + ), + Ok(time!(0:00)) + ); + assert!(matches!( + Time::parse( + "00:00:00", + &fd::parse_owned::<2>("[hour]:[minute][end]:[second]")? + ), + Err(error::Parse::ParseFromDescription( + error::ParseFromDescription::UnexpectedTrailingCharacters { .. } + )) + )); + + Ok(()) +} diff --git a/time-macros/src/format_description/format_item.rs b/time-macros/src/format_description/format_item.rs index 6a8cf555ee..711686683c 100644 --- a/time-macros/src/format_description/format_item.rs +++ b/time-macros/src/format_description/format_item.rs @@ -143,6 +143,7 @@ macro_rules! component_definition { _component_span: Span, ) -> Result { + #[allow(unused_mut)] let mut this = Self { $($field: None),* }; @@ -212,6 +213,7 @@ component_definition! { Day = "day" { padding = "padding": Option => padding, }, + End = "end" {}, Hour = "hour" { padding = "padding": Option => padding, base = "repr": Option => is_12_hour_clock, diff --git a/time-macros/src/format_description/public/component.rs b/time-macros/src/format_description/public/component.rs index 4737c6ce5c..94c73f0fb4 100644 --- a/time-macros/src/format_description/public/component.rs +++ b/time-macros/src/format_description/public/component.rs @@ -46,4 +46,5 @@ declare_component! { OffsetSecond Ignore UnixTimestamp + End } diff --git a/time-macros/src/format_description/public/modifier.rs b/time-macros/src/format_description/public/modifier.rs index e39c6bf552..63bfaa7065 100644 --- a/time-macros/src/format_description/public/modifier.rs +++ b/time-macros/src/format_description/public/modifier.rs @@ -10,18 +10,18 @@ macro_rules! to_tokens { $struct_vis:vis struct $struct_name:ident {$( $(#[$field_attr:meta])* $field_vis:vis $field_name:ident : $field_ty:ty - ),+ $(,)?} + ),* $(,)?} ) => { $(#[$struct_attr])* $struct_vis struct $struct_name {$( $(#[$field_attr])* $field_vis $field_name: $field_ty - ),+} + ),*} impl ToTokenTree for $struct_name { fn into_token_tree(self) -> TokenTree { let mut tokens = TokenStream::new(); - let Self {$($field_name),+} = self; + let Self {$($field_name),*} = self; quote_append! { tokens let mut value = ::time::format_description::modifier::$struct_name::default(); @@ -30,7 +30,7 @@ macro_rules! to_tokens { quote_append!(tokens value.$field_name =); $field_name.append_to(&mut tokens); quote_append!(tokens ;); - )+ + )* quote_append!(tokens value); proc_macro::TokenTree::Group(proc_macro::Group::new( @@ -245,3 +245,7 @@ to_tokens! { pub(crate) sign_is_mandatory: bool, } } + +to_tokens! { + pub(crate) struct End {} +} diff --git a/time/src/error/parse_from_description.rs b/time/src/error/parse_from_description.rs index 772f10d173..b5ec1f95f8 100644 --- a/time/src/error/parse_from_description.rs +++ b/time/src/error/parse_from_description.rs @@ -13,6 +13,8 @@ pub enum ParseFromDescription { InvalidLiteral, /// A dynamic component was not valid. InvalidComponent(&'static str), + #[non_exhaustive] + UnexpectedTrailingCharacters, } impl fmt::Display for ParseFromDescription { @@ -22,6 +24,9 @@ impl fmt::Display for ParseFromDescription { Self::InvalidComponent(name) => { write!(f, "the '{name}' component could not be parsed") } + Self::UnexpectedTrailingCharacters => { + f.write_str("unexpected trailing characters; the end of input was expected") + } } } } diff --git a/time/src/format_description/component.rs b/time/src/format_description/component.rs index 6725590815..9119c0905c 100644 --- a/time/src/format_description/component.rs +++ b/time/src/format_description/component.rs @@ -38,4 +38,7 @@ pub enum Component { Ignore(modifier::Ignore), /// A Unix timestamp. UnixTimestamp(modifier::UnixTimestamp), + /// The end of input. Parsing this component will fail if there is any input remaining. This + /// component neither affects formatting nor consumes any input when parsing. + End(modifier::End), } diff --git a/time/src/format_description/modifier.rs b/time/src/format_description/modifier.rs index cdac1ae974..b145e60cc8 100644 --- a/time/src/format_description/modifier.rs +++ b/time/src/format_description/modifier.rs @@ -279,6 +279,13 @@ pub struct UnixTimestamp { pub sign_is_mandatory: bool, } +/// The end of input. +/// +/// There is currently not customization for this modifier. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct End; + /// Generate the provided code if and only if `pub` is present. macro_rules! if_pub { (pub $(#[$attr:meta])*; $($x:tt)*) => { @@ -406,4 +413,6 @@ impl_const_default! { precision: UnixTimestampPrecision::Second, sign_is_mandatory: false, }; + /// Creates a modifier used to represent the end of input. + @pub End => End; } diff --git a/time/src/format_description/parse/format_item.rs b/time/src/format_description/parse/format_item.rs index c0e64d92a2..c54a274ab0 100644 --- a/time/src/format_description/parse/format_item.rs +++ b/time/src/format_description/parse/format_item.rs @@ -190,6 +190,8 @@ macro_rules! component_definition { _component_span: Span, ) -> Result { + // rustc will complain if the modifier is empty. + #[allow(unused_mut)] let mut this = Self { $($field: None),* }; @@ -280,6 +282,7 @@ component_definition! { Day = "day" { padding = "padding": Option => padding, }, + End = "end" {}, Hour = "hour" { padding = "padding": Option => padding, base = "repr": Option => is_12_hour_clock, diff --git a/time/src/formatting/mod.rs b/time/src/formatting/mod.rs index cfea7e8c3d..b6da6b74c5 100644 --- a/time/src/formatting/mod.rs +++ b/time/src/formatting/mod.rs @@ -197,6 +197,7 @@ pub(crate) fn format_component( (UnixTimestamp(modifier), Some(date), Some(time), Some(offset)) => { fmt_unix_timestamp(output, date, time, offset, modifier)? } + (End(modifier::End {}), ..) => 0, _ => return Err(error::Format::InsufficientTypeInformation), }) } diff --git a/time/src/parsing/component.rs b/time/src/parsing/component.rs index 23035893e4..582c80e0e9 100644 --- a/time/src/parsing/component.rs +++ b/time/src/parsing/component.rs @@ -331,3 +331,15 @@ pub(crate) fn parse_unix_timestamp( _ => Some(ParsedItem(input, nano_timestamp as _)), } } + +/// Parse the `end` component, which represents the end of input. If any input is remaining, `None` +/// is returned. +pub(crate) const fn parse_end(input: &[u8], end: modifier::End) -> Option> { + let modifier::End {} = end; + + if input.is_empty() { + Some(ParsedItem(input, ())) + } else { + None + } +} diff --git a/time/src/parsing/parsed.rs b/time/src/parsing/parsed.rs index 1ef0ede09b..c382fd3a6d 100644 --- a/time/src/parsing/parsed.rs +++ b/time/src/parsing/parsed.rs @@ -11,12 +11,11 @@ use crate::convert::{Day, Hour, Minute, Nanosecond, Second}; use crate::date::{MAX_YEAR, MIN_YEAR}; use crate::date_time::{maybe_offset_from_offset, offset_kind, DateTime, MaybeOffset}; use crate::error::TryFromParsed::InsufficientInformation; -use crate::format_description::modifier::{WeekNumberRepr, YearRepr}; #[cfg(feature = "alloc")] use crate::format_description::OwnedFormatItem; -use crate::format_description::{Component, FormatItem}; +use crate::format_description::{modifier, Component, FormatItem}; use crate::parsing::component::{ - parse_day, parse_hour, parse_ignore, parse_minute, parse_month, parse_offset_hour, + parse_day, parse_end, parse_hour, parse_ignore, parse_minute, parse_month, parse_offset_hour, parse_offset_minute, parse_offset_second, parse_ordinal, parse_period, parse_second, parse_subsecond, parse_unix_timestamp, parse_week_number, parse_weekday, parse_year, Period, }; @@ -276,11 +275,11 @@ impl Parsed { let ParsedItem(remaining, value) = parse_week_number(input, modifiers).ok_or(InvalidComponent("week number"))?; match modifiers.repr { - WeekNumberRepr::Iso => { + modifier::WeekNumberRepr::Iso => { NonZeroU8::new(value).and_then(|value| self.set_iso_week_number(value)) } - WeekNumberRepr::Sunday => self.set_sunday_week_number(value), - WeekNumberRepr::Monday => self.set_monday_week_number(value), + modifier::WeekNumberRepr::Sunday => self.set_sunday_week_number(value), + modifier::WeekNumberRepr::Monday => self.set_monday_week_number(value), } .ok_or(InvalidComponent("week number"))?; Ok(remaining) @@ -289,10 +288,10 @@ impl Parsed { let ParsedItem(remaining, value) = parse_year(input, modifiers).ok_or(InvalidComponent("year"))?; match (modifiers.iso_week_based, modifiers.repr) { - (false, YearRepr::Full) => self.set_year(value), - (false, YearRepr::LastTwo) => self.set_year_last_two(value as _), - (true, YearRepr::Full) => self.set_iso_year(value), - (true, YearRepr::LastTwo) => self.set_iso_year_last_two(value as _), + (false, modifier::YearRepr::Full) => self.set_year(value), + (false, modifier::YearRepr::LastTwo) => self.set_year_last_two(value as _), + (true, modifier::YearRepr::Full) => self.set_iso_year(value), + (true, modifier::YearRepr::LastTwo) => self.set_iso_year_last_two(value as _), } .ok_or(InvalidComponent("year"))?; Ok(remaining) @@ -349,6 +348,9 @@ impl Parsed { parsed.consume_value(|value| self.set_unix_timestamp_nanos(value)) }) .ok_or(InvalidComponent("unix_timestamp")), + Component::End(modifiers) => parse_end(input, modifiers) + .map(ParsedItem::<()>::into_inner) + .ok_or(error::ParseFromDescription::UnexpectedTrailingCharacters), } } }