Skip to main content

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::{Error, Span, Spanned, ast, unused};
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            trailing_input = "trailing_input": Option<TrailingInput> => trailing_input,
290        },
291        Hour = "hour" {
292            padding = "padding": Option<Padding> => padding,
293            base = "repr": Option<HourBase> => is_12_hour_clock,
294        },
295        Ignore = "ignore" {
296            #[required]
297            count = "count": Option<#[from_str] NonZero<u16>> => count,
298        },
299        Minute = "minute" {
300            padding = "padding": Option<Padding> => padding,
301        },
302        Month = "month" {
303            padding = "padding": Option<Padding> => padding,
304            repr = "repr": Option<MonthRepr> => repr,
305            case_sensitive = "case_sensitive": Option<MonthCaseSensitive> => case_sensitive,
306        },
307        OffsetHour = "offset_hour" {
308            sign_behavior = "sign": Option<SignBehavior> => sign_is_mandatory,
309            padding = "padding": Option<Padding> => padding,
310        },
311        OffsetMinute = "offset_minute" {
312            padding = "padding": Option<Padding> => padding,
313        },
314        OffsetSecond = "offset_second" {
315            padding = "padding": Option<Padding> => padding,
316        },
317        Ordinal = "ordinal" {
318            padding = "padding": Option<Padding> => padding,
319        },
320        Period = "period" {
321            case = "case": Option<PeriodCase> => is_uppercase,
322            case_sensitive = "case_sensitive": Option<PeriodCaseSensitive> => case_sensitive,
323        },
324        Second = "second" {
325            padding = "padding": Option<Padding> => padding,
326        },
327        Subsecond = "subsecond" {
328            digits = "digits": Option<SubsecondDigits> => digits,
329        },
330        UnixTimestamp = "unix_timestamp" {
331            precision = "precision": Option<UnixTimestampPrecision> => precision,
332            sign_behavior = "sign": Option<SignBehavior> => sign_is_mandatory,
333        },
334        Weekday = "weekday" {
335            repr = "repr": Option<WeekdayRepr> => repr,
336            one_indexed = "one_indexed": Option<WeekdayOneIndexed> => one_indexed,
337            case_sensitive = "case_sensitive": Option<WeekdayCaseSensitive> => case_sensitive,
338        },
339        WeekNumber = "week_number" {
340            padding = "padding": Option<Padding> => padding,
341            repr = "repr": Option<WeekNumberRepr> => repr,
342        },
343        Year = "year" {
344            padding = "padding": Option<Padding> => padding,
345            repr = "repr": Option<YearRepr> => repr,
346            range = "range": Option<YearRange> => range,
347            base = "base": Option<YearBase> => iso_week_based,
348            sign_behavior = "sign": Option<SignBehavior> => sign_is_mandatory,
349        },
350    }
351}
352
353/// Get the target type for a given enum.
354macro_rules! target_ty {
355    ($name:ident $type:ty) => {
356        $type
357    };
358    ($name:ident) => {
359        $crate::format_description::modifier::$name
360    };
361}
362
363/// Get the target value for a given enum.
364macro_rules! target_value {
365    ($name:ident $variant:ident $value:expr) => {
366        $value
367    };
368    ($name:ident $variant:ident) => {
369        $crate::format_description::modifier::$name::$variant
370    };
371}
372
373/// Declare the various modifiers.
374///
375/// For the general case, ordinary syntax can be used. Note that you _must_ declare a default
376/// variant. The only significant change is that the string representation of the variant must be
377/// provided after the variant name. For example, `Numerical = b"numerical"` declares a variant
378/// named `Numerical` with the string representation `b"numerical"`. This is the value that will be
379/// used when parsing the modifier. The value is not case sensitive.
380///
381/// If the type in the public API does not have the same name as the type in the internal
382/// representation, then the former must be specified in parenthesis after the internal name. For
383/// example, `HourBase(bool)` has an internal name "HourBase", but is represented as a boolean in
384/// the public API.
385///
386/// By default, the internal variant name is assumed to be the same as the public variant name. If
387/// this is not the case, the qualified path to the variant must be specified in parenthesis after
388/// the internal variant name. For example, `Twelve(true)` has an internal variant name "Twelve",
389/// but is represented as `true` in the public API.
390macro_rules! modifier {
391    ($(
392        enum $name:ident $(($target_ty:ty))? {
393            $(
394                $(#[$attr:meta])?
395                $variant:ident $(($target_value:expr))? = $parse_variant:literal
396            ),* $(,)?
397        }
398    )+) => {$(
399        #[derive(Default)]
400        enum $name {
401            $($(#[$attr])? $variant),*
402        }
403
404        impl $name {
405            /// Parse the modifier from its string representation.
406            #[inline]
407            fn from_modifier_value(value: &Spanned<&[u8]>) -> Result<Option<Self>, Error> {
408                $(if value.eq_ignore_ascii_case($parse_variant) {
409                    return Ok(Some(Self::$variant));
410                })*
411                Err(Error {
412                    _inner: unused(value.span.error("invalid modifier value")),
413                    public: crate::error::InvalidFormatDescription::InvalidModifier {
414                        value: String::from_utf8_lossy(value).into_owned(),
415                        index: value.span.start.byte as usize,
416                    },
417                })
418            }
419        }
420
421        impl From<$name> for target_ty!($name $($target_ty)?) {
422            #[inline]
423            fn from(modifier: $name) -> Self {
424                match modifier {
425                    $($name::$variant => target_value!($name $variant $($target_value)?)),*
426                }
427            }
428        }
429    )+};
430}
431
432// Keep in alphabetical order.
433modifier! {
434    enum HourBase(bool) {
435        Twelve(true) = b"12",
436        #[default]
437        TwentyFour(false) = b"24",
438    }
439
440    enum MonthCaseSensitive(bool) {
441        False(false) = b"false",
442        #[default]
443        True(true) = b"true",
444    }
445
446    enum MonthRepr {
447        #[default]
448        Numerical = b"numerical",
449        Long = b"long",
450        Short = b"short",
451    }
452
453    enum Padding {
454        Space = b"space",
455        #[default]
456        Zero = b"zero",
457        None = b"none",
458    }
459
460    enum PeriodCase(bool) {
461        Lower(false) = b"lower",
462        #[default]
463        Upper(true) = b"upper",
464    }
465
466    enum PeriodCaseSensitive(bool) {
467        False(false) = b"false",
468        #[default]
469        True(true) = b"true",
470    }
471
472    enum SignBehavior(bool) {
473        #[default]
474        Automatic(false) = b"automatic",
475        Mandatory(true) = b"mandatory",
476    }
477
478    enum SubsecondDigits {
479        One = b"1",
480        Two = b"2",
481        Three = b"3",
482        Four = b"4",
483        Five = b"5",
484        Six = b"6",
485        Seven = b"7",
486        Eight = b"8",
487        Nine = b"9",
488        #[default]
489        OneOrMore = b"1+",
490    }
491
492    enum TrailingInput {
493        #[default]
494        Prohibit = b"prohibit",
495        Discard = b"discard",
496    }
497
498    enum UnixTimestampPrecision {
499        #[default]
500        Second = b"second",
501        Millisecond = b"millisecond",
502        Microsecond = b"microsecond",
503        Nanosecond = b"nanosecond",
504    }
505
506    enum WeekNumberRepr {
507        #[default]
508        Iso = b"iso",
509        Sunday = b"sunday",
510        Monday = b"monday",
511    }
512
513    enum WeekdayCaseSensitive(bool) {
514        False(false) = b"false",
515        #[default]
516        True(true) = b"true",
517    }
518
519    enum WeekdayOneIndexed(bool) {
520        False(false) = b"false",
521        #[default]
522        True(true) = b"true",
523    }
524
525    enum WeekdayRepr {
526        Short = b"short",
527        #[default]
528        Long = b"long",
529        Sunday = b"sunday",
530        Monday = b"monday",
531    }
532
533    enum YearBase(bool) {
534        #[default]
535        Calendar(false) = b"calendar",
536        IsoWeek(true) = b"iso_week",
537    }
538
539    enum YearRepr {
540        #[default]
541        Full = b"full",
542        Century = b"century",
543        LastTwo = b"last_two",
544    }
545
546    enum YearRange {
547        Standard = b"standard",
548        #[default]
549        Extended = b"extended",
550    }
551}
552
553/// Parse a modifier value using `FromStr`. Requires the modifier value to be valid UTF-8.
554#[inline]
555fn parse_from_modifier_value<T>(value: &Spanned<&[u8]>) -> Result<Option<T>, Error>
556where
557    T: FromStr,
558{
559    str::from_utf8(value)
560        .ok()
561        .and_then(|val| val.parse::<T>().ok())
562        .map(|val| Some(val))
563        .ok_or_else(|| Error {
564            _inner: unused(value.span.error("invalid modifier value")),
565            public: crate::error::InvalidFormatDescription::InvalidModifier {
566                value: String::from_utf8_lossy(value).into_owned(),
567                index: value.span.start.byte as usize,
568            },
569        })
570}