time/format_description/parse/
format_item.rs

1//! Typed, validated representation of a parsed format description.
2
3use alloc::boxed::Box;
4use alloc::string::String;
5use core::num::NonZero;
6use core::str::{self, FromStr};
7
8use super::{ast, unused, Error, Span, Spanned};
9use crate::internal_macros::bug;
10
11/// Parse an AST iterator into a sequence of format items.
12#[inline]
13pub(super) fn parse<'a>(
14    ast_items: impl Iterator<Item = Result<ast::Item<'a>, Error>>,
15) -> impl Iterator<Item = Result<Item<'a>, Error>> {
16    ast_items.map(|ast_item| ast_item.and_then(Item::from_ast))
17}
18
19/// A description of how to format and parse one part of a type.
20pub(super) enum Item<'a> {
21    /// A literal string.
22    Literal(&'a [u8]),
23    /// Part of a type, along with its modifiers.
24    Component(Component),
25    /// A sequence of optional items.
26    Optional {
27        /// The items themselves.
28        value: Box<[Self]>,
29        /// The span of the full sequence.
30        span: Span,
31    },
32    /// The first matching parse of a sequence of format descriptions.
33    First {
34        /// The sequence of format descriptions.
35        value: Box<[Box<[Self]>]>,
36        /// The span of the full sequence.
37        span: Span,
38    },
39}
40
41impl Item<'_> {
42    /// Parse an AST item into a format item.
43    pub(super) fn from_ast(ast_item: ast::Item<'_>) -> Result<Item<'_>, Error> {
44        Ok(match ast_item {
45            ast::Item::Component {
46                _opening_bracket: _,
47                _leading_whitespace: _,
48                name,
49                modifiers,
50                _trailing_whitespace: _,
51                _closing_bracket: _,
52            } => Item::Component(component_from_ast(&name, &modifiers)?),
53            ast::Item::Literal(Spanned { value, span: _ }) => Item::Literal(value),
54            ast::Item::EscapedBracket {
55                _first: _,
56                _second: _,
57            } => Item::Literal(b"["),
58            ast::Item::Optional {
59                opening_bracket,
60                _leading_whitespace: _,
61                _optional_kw: _,
62                _whitespace: _,
63                nested_format_description,
64                closing_bracket,
65            } => {
66                let items = nested_format_description
67                    .items
68                    .into_vec()
69                    .into_iter()
70                    .map(Item::from_ast)
71                    .collect::<Result<_, _>>()?;
72                Item::Optional {
73                    value: items,
74                    span: opening_bracket.to(closing_bracket),
75                }
76            }
77            ast::Item::First {
78                opening_bracket,
79                _leading_whitespace: _,
80                _first_kw: _,
81                _whitespace: _,
82                nested_format_descriptions,
83                closing_bracket,
84            } => {
85                let items = nested_format_descriptions
86                    .into_vec()
87                    .into_iter()
88                    .map(|nested_format_description| {
89                        nested_format_description
90                            .items
91                            .into_vec()
92                            .into_iter()
93                            .map(Item::from_ast)
94                            .collect()
95                    })
96                    .collect::<Result<_, _>>()?;
97                Item::First {
98                    value: items,
99                    span: opening_bracket.to(closing_bracket),
100                }
101            }
102        })
103    }
104}
105
106impl<'a> TryFrom<Item<'a>> for crate::format_description::BorrowedFormatItem<'a> {
107    type Error = Error;
108
109    #[inline]
110    fn try_from(item: Item<'a>) -> Result<Self, Self::Error> {
111        match item {
112            Item::Literal(literal) => Ok(Self::Literal(literal)),
113            Item::Component(component) => Ok(Self::Component(component.into())),
114            Item::Optional { value: _, span } => Err(Error {
115                _inner: unused(span.error(
116                    "optional items are not supported in runtime-parsed format descriptions",
117                )),
118                public: crate::error::InvalidFormatDescription::NotSupported {
119                    what: "optional item",
120                    context: "runtime-parsed format descriptions",
121                    index: span.start.byte as usize,
122                },
123            }),
124            Item::First { value: _, span } => Err(Error {
125                _inner: unused(span.error(
126                    "'first' items are not supported in runtime-parsed format descriptions",
127                )),
128                public: crate::error::InvalidFormatDescription::NotSupported {
129                    what: "'first' item",
130                    context: "runtime-parsed format descriptions",
131                    index: span.start.byte as usize,
132                },
133            }),
134        }
135    }
136}
137
138impl From<Item<'_>> for crate::format_description::OwnedFormatItem {
139    #[inline]
140    fn from(item: Item<'_>) -> Self {
141        match item {
142            Item::Literal(literal) => Self::Literal(literal.to_vec().into_boxed_slice()),
143            Item::Component(component) => Self::Component(component.into()),
144            Item::Optional { value, span: _ } => Self::Optional(Box::new(value.into())),
145            Item::First { value, span: _ } => {
146                Self::First(value.into_vec().into_iter().map(Into::into).collect())
147            }
148        }
149    }
150}
151
152impl<'a> From<Box<[Item<'a>]>> for crate::format_description::OwnedFormatItem {
153    #[inline]
154    fn from(items: Box<[Item<'a>]>) -> Self {
155        let items = items.into_vec();
156        match <[_; 1]>::try_from(items) {
157            Ok([item]) => item.into(),
158            Err(vec) => Self::Compound(vec.into_iter().map(Into::into).collect()),
159        }
160    }
161}
162
163/// Declare the `Component` struct.
164macro_rules! component_definition {
165    (@if_required required then { $($then:tt)* } $(else { $($else:tt)* })?) => { $($then)* };
166    (@if_required then { $($then:tt)* } $(else { $($else:tt)* })?) => { $($($else)*)? };
167    (@if_from_str from_str then { $($then:tt)* } $(else { $($else:tt)* })?) => { $($then)* };
168    (@if_from_str then { $($then:tt)* } $(else { $($else:tt)* })?) => { $($($else)*)? };
169
170    ($vis:vis enum $name:ident {
171        $($variant:ident = $parse_variant:literal {$(
172            $(#[$required:tt])?
173            $field:ident = $parse_field:literal:
174            Option<$(#[$from_str:tt])? $field_type:ty>
175            => $target_field:ident
176        ),* $(,)?}),* $(,)?
177    }) => {
178        $vis enum $name {
179            $($variant($variant),)*
180        }
181
182        $($vis struct $variant {
183            $($field: Option<$field_type>),*
184        })*
185
186        $(impl $variant {
187            /// Parse the component from the AST, given its modifiers.
188            #[inline]
189            fn with_modifiers(
190                modifiers: &[ast::Modifier<'_>],
191                _component_span: Span,
192            ) -> Result<Self, Error>
193            {
194                // rustc will complain if the modifier is empty.
195                #[allow(unused_mut)]
196                let mut this = Self {
197                    $($field: None),*
198                };
199
200                for modifier in modifiers {
201                    $(#[expect(clippy::string_lit_as_bytes)]
202                    if modifier.key.eq_ignore_ascii_case($parse_field.as_bytes()) {
203                        this.$field = component_definition!(@if_from_str $($from_str)?
204                            then {
205                                parse_from_modifier_value::<$field_type>(&modifier.value)?
206                            } else {
207                                <$field_type>::from_modifier_value(&modifier.value)?
208                            });
209                        continue;
210                    })*
211                    return Err(Error {
212                        _inner: unused(modifier.key.span.error("invalid modifier key")),
213                        public: crate::error::InvalidFormatDescription::InvalidModifier {
214                            value: String::from_utf8_lossy(*modifier.key).into_owned(),
215                            index: modifier.key.span.start.byte as usize,
216                        }
217                    });
218                }
219
220                $(component_definition! { @if_required $($required)? then {
221                    if this.$field.is_none() {
222                        return Err(Error {
223                            _inner: unused(_component_span.error("missing required modifier")),
224                            public:
225                                crate::error::InvalidFormatDescription::MissingRequiredModifier {
226                                    name: $parse_field,
227                                    index: _component_span.start.byte as usize,
228                                }
229                        });
230                    }
231                }})*
232
233                Ok(this)
234            }
235        })*
236
237        impl From<$name> for crate::format_description::Component {
238            #[inline]
239            fn from(component: $name) -> Self {
240                match component {$(
241                    $name::$variant($variant { $($field),* }) => {
242                        $crate::format_description::component::Component::$variant(
243                            $crate::format_description::modifier::$variant {$(
244                                $target_field: component_definition! { @if_required $($required)?
245                                    then {
246                                        match $field {
247                                            Some(value) => value.into(),
248                                            None => bug!("required modifier was not set"),
249                                        }
250                                    } else {
251                                        $field.unwrap_or_default().into()
252                                    }
253                                }
254                            ),*}
255                        )
256                    }
257                )*}
258            }
259        }
260
261        /// Parse a component from the AST, given its name and modifiers.
262        #[inline]
263        fn component_from_ast(
264            name: &Spanned<&[u8]>,
265            modifiers: &[ast::Modifier<'_>],
266        ) -> Result<Component, Error> {
267            $(#[expect(clippy::string_lit_as_bytes)]
268            if name.eq_ignore_ascii_case($parse_variant.as_bytes()) {
269                return Ok(Component::$variant($variant::with_modifiers(&modifiers, name.span)?));
270            })*
271            Err(Error {
272                _inner: unused(name.span.error("invalid component")),
273                public: crate::error::InvalidFormatDescription::InvalidComponentName {
274                    name: String::from_utf8_lossy(name).into_owned(),
275                    index: name.span.start.byte as usize,
276                },
277            })
278        }
279    }
280}
281
282// Keep in alphabetical order.
283component_definition! {
284    pub(super) enum Component {
285        Day = "day" {
286            padding = "padding": Option<Padding> => padding,
287        },
288        End = "end" {},
289        Hour = "hour" {
290            padding = "padding": Option<Padding> => padding,
291            base = "repr": Option<HourBase> => is_12_hour_clock,
292        },
293        Ignore = "ignore" {
294            #[required]
295            count = "count": Option<#[from_str] NonZero<u16>> => count,
296        },
297        Minute = "minute" {
298            padding = "padding": Option<Padding> => padding,
299        },
300        Month = "month" {
301            padding = "padding": Option<Padding> => padding,
302            repr = "repr": Option<MonthRepr> => repr,
303            case_sensitive = "case_sensitive": Option<MonthCaseSensitive> => case_sensitive,
304        },
305        OffsetHour = "offset_hour" {
306            sign_behavior = "sign": Option<SignBehavior> => sign_is_mandatory,
307            padding = "padding": Option<Padding> => padding,
308        },
309        OffsetMinute = "offset_minute" {
310            padding = "padding": Option<Padding> => padding,
311        },
312        OffsetSecond = "offset_second" {
313            padding = "padding": Option<Padding> => padding,
314        },
315        Ordinal = "ordinal" {
316            padding = "padding": Option<Padding> => padding,
317        },
318        Period = "period" {
319            case = "case": Option<PeriodCase> => is_uppercase,
320            case_sensitive = "case_sensitive": Option<PeriodCaseSensitive> => case_sensitive,
321        },
322        Second = "second" {
323            padding = "padding": Option<Padding> => padding,
324        },
325        Subsecond = "subsecond" {
326            digits = "digits": Option<SubsecondDigits> => digits,
327        },
328        UnixTimestamp = "unix_timestamp" {
329            precision = "precision": Option<UnixTimestampPrecision> => precision,
330            sign_behavior = "sign": Option<SignBehavior> => sign_is_mandatory,
331        },
332        Weekday = "weekday" {
333            repr = "repr": Option<WeekdayRepr> => repr,
334            one_indexed = "one_indexed": Option<WeekdayOneIndexed> => one_indexed,
335            case_sensitive = "case_sensitive": Option<WeekdayCaseSensitive> => case_sensitive,
336        },
337        WeekNumber = "week_number" {
338            padding = "padding": Option<Padding> => padding,
339            repr = "repr": Option<WeekNumberRepr> => repr,
340        },
341        Year = "year" {
342            padding = "padding": Option<Padding> => padding,
343            repr = "repr": Option<YearRepr> => repr,
344            range = "range": Option<YearRange> => range,
345            base = "base": Option<YearBase> => iso_week_based,
346            sign_behavior = "sign": Option<SignBehavior> => sign_is_mandatory,
347        },
348    }
349}
350
351/// Get the target type for a given enum.
352macro_rules! target_ty {
353    ($name:ident $type:ty) => {
354        $type
355    };
356    ($name:ident) => {
357        $crate::format_description::modifier::$name
358    };
359}
360
361/// Get the target value for a given enum.
362macro_rules! target_value {
363    ($name:ident $variant:ident $value:expr) => {
364        $value
365    };
366    ($name:ident $variant:ident) => {
367        $crate::format_description::modifier::$name::$variant
368    };
369}
370
371/// Declare the various modifiers.
372///
373/// For the general case, ordinary syntax can be used. Note that you _must_ declare a default
374/// variant. The only significant change is that the string representation of the variant must be
375/// provided after the variant name. For example, `Numerical = b"numerical"` declares a variant
376/// named `Numerical` with the string representation `b"numerical"`. This is the value that will be
377/// used when parsing the modifier. The value is not case sensitive.
378///
379/// If the type in the public API does not have the same name as the type in the internal
380/// representation, then the former must be specified in parenthesis after the internal name. For
381/// example, `HourBase(bool)` has an internal name "HourBase", but is represented as a boolean in
382/// the public API.
383///
384/// By default, the internal variant name is assumed to be the same as the public variant name. If
385/// this is not the case, the qualified path to the variant must be specified in parenthesis after
386/// the internal variant name. For example, `Twelve(true)` has an internal variant name "Twelve",
387/// but is represented as `true` in the public API.
388macro_rules! modifier {
389    ($(
390        enum $name:ident $(($target_ty:ty))? {
391            $(
392                $(#[$attr:meta])?
393                $variant:ident $(($target_value:expr))? = $parse_variant:literal
394            ),* $(,)?
395        }
396    )+) => {$(
397        #[derive(Default)]
398        enum $name {
399            $($(#[$attr])? $variant),*
400        }
401
402        impl $name {
403            /// Parse the modifier from its string representation.
404            #[inline]
405            fn from_modifier_value(value: &Spanned<&[u8]>) -> Result<Option<Self>, Error> {
406                $(if value.eq_ignore_ascii_case($parse_variant) {
407                    return Ok(Some(Self::$variant));
408                })*
409                Err(Error {
410                    _inner: unused(value.span.error("invalid modifier value")),
411                    public: crate::error::InvalidFormatDescription::InvalidModifier {
412                        value: String::from_utf8_lossy(value).into_owned(),
413                        index: value.span.start.byte as usize,
414                    },
415                })
416            }
417        }
418
419        impl From<$name> for target_ty!($name $($target_ty)?) {
420            #[inline]
421            fn from(modifier: $name) -> Self {
422                match modifier {
423                    $($name::$variant => target_value!($name $variant $($target_value)?)),*
424                }
425            }
426        }
427    )+};
428}
429
430// Keep in alphabetical order.
431modifier! {
432    enum HourBase(bool) {
433        Twelve(true) = b"12",
434        #[default]
435        TwentyFour(false) = b"24",
436    }
437
438    enum MonthCaseSensitive(bool) {
439        False(false) = b"false",
440        #[default]
441        True(true) = b"true",
442    }
443
444    enum MonthRepr {
445        #[default]
446        Numerical = b"numerical",
447        Long = b"long",
448        Short = b"short",
449    }
450
451    enum Padding {
452        Space = b"space",
453        #[default]
454        Zero = b"zero",
455        None = b"none",
456    }
457
458    enum PeriodCase(bool) {
459        Lower(false) = b"lower",
460        #[default]
461        Upper(true) = b"upper",
462    }
463
464    enum PeriodCaseSensitive(bool) {
465        False(false) = b"false",
466        #[default]
467        True(true) = b"true",
468    }
469
470    enum SignBehavior(bool) {
471        #[default]
472        Automatic(false) = b"automatic",
473        Mandatory(true) = b"mandatory",
474    }
475
476    enum SubsecondDigits {
477        One = b"1",
478        Two = b"2",
479        Three = b"3",
480        Four = b"4",
481        Five = b"5",
482        Six = b"6",
483        Seven = b"7",
484        Eight = b"8",
485        Nine = b"9",
486        #[default]
487        OneOrMore = b"1+",
488    }
489
490    enum UnixTimestampPrecision {
491        #[default]
492        Second = b"second",
493        Millisecond = b"millisecond",
494        Microsecond = b"microsecond",
495        Nanosecond = b"nanosecond",
496    }
497
498    enum WeekNumberRepr {
499        #[default]
500        Iso = b"iso",
501        Sunday = b"sunday",
502        Monday = b"monday",
503    }
504
505    enum WeekdayCaseSensitive(bool) {
506        False(false) = b"false",
507        #[default]
508        True(true) = b"true",
509    }
510
511    enum WeekdayOneIndexed(bool) {
512        False(false) = b"false",
513        #[default]
514        True(true) = b"true",
515    }
516
517    enum WeekdayRepr {
518        Short = b"short",
519        #[default]
520        Long = b"long",
521        Sunday = b"sunday",
522        Monday = b"monday",
523    }
524
525    enum YearBase(bool) {
526        #[default]
527        Calendar(false) = b"calendar",
528        IsoWeek(true) = b"iso_week",
529    }
530
531    enum YearRepr {
532        #[default]
533        Full = b"full",
534        Century = b"century",
535        LastTwo = b"last_two",
536    }
537
538    enum YearRange {
539        Standard = b"standard",
540        #[default]
541        Extended = b"extended",
542    }
543}
544
545/// Parse a modifier value using `FromStr`. Requires the modifier value to be valid UTF-8.
546#[inline]
547fn parse_from_modifier_value<T: FromStr>(value: &Spanned<&[u8]>) -> Result<Option<T>, Error> {
548    str::from_utf8(value)
549        .ok()
550        .and_then(|val| val.parse::<T>().ok())
551        .map(|val| Some(val))
552        .ok_or_else(|| Error {
553            _inner: unused(value.span.error("invalid modifier value")),
554            public: crate::error::InvalidFormatDescription::InvalidModifier {
555                value: String::from_utf8_lossy(value).into_owned(),
556                index: value.span.start.byte as usize,
557            },
558        })
559}