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::{
9    Error, FormatDescriptionVersion, Location, OptionExt as _, Span, Spanned, SpannedValue as _,
10    ast, unused,
11};
12use crate::error::InvalidFormatDescription;
13use crate::internal_macros::bug;
14
15macro_rules! parse_modifiers {
16    ($modifiers:expr, struct {}) => {{
17        struct Parsed {}
18        drop($modifiers);
19        Ok(Parsed {})
20    }};
21    ($modifiers:expr, struct { $($field:ident : $modifier:ident),* $(,)? }) => {
22        'block: {
23            struct Parsed {
24                $($field: Option<Spanned<<$modifier as ModifierValue>::Type>>),*
25            }
26
27            let mut parsed = Parsed {
28                $($field: None),*
29            };
30
31            for modifier in $modifiers {
32                $(if modifier.key.eq_ignore_ascii_case(stringify!($field).as_bytes()) {
33                    if parsed.$field.is_some() {
34                        break 'block Err(Error {
35                            _inner: unused(modifier.key.span.error("duplicate modifier key")),
36                            public: InvalidFormatDescription::DuplicateModifier {
37                                name: stringify!($field),
38                                index: modifier.key.span.start.byte as usize,
39                            }
40                        });
41                    }
42                    match <$modifier>::from_modifier_value(&modifier.value) {
43                        Ok(value) => {
44                            parsed.$field = Some(
45                                <<$modifier as ModifierValue>::Type>::from(value)
46                                    .spanned(modifier.value.span)
47                            )
48                        },
49                        Err(err) => break 'block Err(err),
50                    }
51                    continue;
52                })*
53                break 'block Err(Error {
54                    _inner: unused(modifier.key.span.error("invalid modifier key")),
55                    public: InvalidFormatDescription::InvalidModifier {
56                        value: String::from_utf8_lossy(*modifier.key).into_owned(),
57                        index: modifier.key.span.start.byte as usize,
58                    }
59                });
60            }
61
62            Ok(parsed)
63        }
64    };
65}
66
67/// Parse an AST iterator into a sequence of format items.
68#[inline]
69pub(super) fn parse<'a>(
70    ast_items: impl Iterator<Item = Result<ast::Item<'a>, Error>>,
71) -> impl Iterator<Item = Result<Item<'a>, Error>> {
72    ast_items.map(|ast_item| ast_item.and_then(Item::from_ast))
73}
74
75/// A description of how to format and parse one part of a type.
76pub(super) enum Item<'a> {
77    /// A literal string.
78    Literal(&'a [u8]),
79    /// Part of a type, along with its modifiers.
80    Component(AstComponent),
81    /// A sequence of optional items.
82    Optional {
83        /// The items themselves.
84        value: Box<[Self]>,
85        /// Whether the value should be formatted.
86        format: Spanned<bool>,
87        /// The span of the full sequence.
88        span: Span,
89    },
90    /// The first matching parse of a sequence of format descriptions.
91    First {
92        /// The sequence of format descriptions.
93        value: Box<[Box<[Self]>]>,
94        /// The span of the full sequence.
95        span: Span,
96    },
97}
98
99impl<'a> Item<'a> {
100    /// Parse an AST item into a format item.
101    pub(super) fn from_ast(ast_item: ast::Item<'a>) -> Result<Self, Error> {
102        Ok(match ast_item {
103            ast::Item::Literal(Spanned { value, span: _ }) => Item::Literal(value),
104            ast::Item::Component {
105                version,
106                opening_bracket,
107                _leading_whitespace: _,
108                name,
109                modifiers,
110                nested_format_descriptions,
111                _trailing_whitespace: _,
112                closing_bracket,
113            } => {
114                // Perform additional syntactic checks that are required, even though not
115                // semantically relevant.
116
117                if let Some(first_nested_fd) = nested_format_descriptions.first()
118                    && first_nested_fd.leading_whitespace.is_none()
119                {
120                    return Err(Error {
121                        _inner: unused(
122                            opening_bracket.to(closing_bracket).error(
123                                "missing leading whitespace before nested format description",
124                            ),
125                        ),
126                        public: InvalidFormatDescription::Expected {
127                            what: "whitespace before nested format description",
128                            index: first_nested_fd.opening_bracket.byte as usize,
129                        },
130                    });
131                }
132
133                // Parse the actual component, starting with those that require nested format
134                // descriptions.
135
136                if name.eq_ignore_ascii_case(b"optional") {
137                    Self::optional_from_parts(
138                        opening_bracket,
139                        &modifiers,
140                        nested_format_descriptions,
141                        closing_bracket,
142                    )?
143                } else if name.eq_ignore_ascii_case(b"first") {
144                    let _modifiers = parse_modifiers!(modifiers, struct {})?;
145
146                    if version.is_at_least_v3() && nested_format_descriptions.is_empty() {
147                        return Err(Error {
148                            _inner: unused(opening_bracket.to(closing_bracket).error(
149                                "the `first` component requires at least one nested format \
150                                 description",
151                            )),
152                            public: InvalidFormatDescription::Expected {
153                                what: "at least one nested format description",
154                                index: closing_bracket.byte as usize,
155                            },
156                        });
157                    }
158
159                    let items = nested_format_descriptions
160                        .into_iter()
161                        .map(|nested_format_description| {
162                            nested_format_description
163                                .items
164                                .into_iter()
165                                .map(Item::from_ast)
166                                .collect()
167                        })
168                        .collect::<Result<_, _>>()?;
169
170                    Item::First {
171                        value: items,
172                        span: opening_bracket.to(closing_bracket),
173                    }
174                } else {
175                    // Ensure no nested format descriptions are present.
176                    if !nested_format_descriptions.is_empty() {
177                        return Err(Error {
178                            _inner: unused(opening_bracket.to(closing_bracket).error(
179                                "this component does not support nested format descriptions",
180                            )),
181                            public: InvalidFormatDescription::NotSupported {
182                                what: "nested format descriptions",
183                                context: "on this component",
184                                index: opening_bracket.byte as usize,
185                            },
186                        });
187                    }
188
189                    let mut component = component_from_ast(version, &name, &modifiers)?;
190                    // v3 format descriptions default to `range:standard` rather than
191                    // `range:extended` for v1 and v2.
192                    if version.is_at_least_v3()
193                        && let AstComponent::Year(y) = &mut component
194                        && y.range.value.is_none()
195                    {
196                        y.range = Some(YearRange::Standard).spanned(Span::DUMMY);
197                    }
198                    Item::Component(component)
199                }
200            }
201        })
202    }
203
204    fn optional_from_parts(
205        opening_bracket: Location,
206        modifiers: &[ast::Modifier<'_>],
207        nested_format_descriptions: Box<[ast::NestedFormatDescription<'a>]>,
208        closing_bracket: Location,
209    ) -> Result<Self, Error> {
210        let modifiers = parse_modifiers!(modifiers, struct {
211            format: OptionalFormat,
212        })?;
213
214        let [nested_format_description] = if let Some(second_fd) = nested_format_descriptions.get(1)
215        {
216            return Err(Error {
217                _inner: unused(
218                    second_fd
219                        .opening_bracket
220                        .to(second_fd.closing_bracket)
221                        .error(
222                            "the `optional` component only allows a single nested format \
223                             description",
224                        ),
225                ),
226                public: InvalidFormatDescription::NotSupported {
227                    what: "more than one nested format description",
228                    context: "`optional` components",
229                    index: second_fd.opening_bracket.byte as usize,
230                },
231            });
232        } else if let Ok(nested_format_description) =
233            <Box<[_; 1]>>::try_from(nested_format_descriptions)
234        {
235            *nested_format_description
236        } else {
237            return Err(Error {
238                _inner: unused(
239                    opening_bracket
240                        .to(closing_bracket)
241                        .error("missing nested format description for `optional` component"),
242                ),
243                public: InvalidFormatDescription::Expected {
244                    what: "nested format description",
245                    index: closing_bracket.byte as usize,
246                },
247            });
248        };
249
250        let format = modifiers.format.transpose().map(|val| val.unwrap_or(true));
251        let items = nested_format_description
252            .items
253            .into_iter()
254            .map(Item::from_ast)
255            .collect::<Result<_, _>>()?;
256
257        Ok(Item::Optional {
258            value: items,
259            format,
260            span: opening_bracket.to(closing_bracket),
261        })
262    }
263}
264
265impl<'a> TryFrom<Item<'a>> for crate::format_description::BorrowedFormatItem<'a> {
266    type Error = Error;
267
268    #[inline]
269    fn try_from(item: Item<'a>) -> Result<Self, Self::Error> {
270        match item {
271            #[expect(deprecated)]
272            Item::Literal(literal) => Ok(Self::Literal(literal)),
273            Item::Component(component) => Ok(Self::Component(component.try_into()?)),
274            Item::Optional {
275                value: _,
276                format: _,
277                span,
278            } => Err(Error {
279                _inner: unused(span.error(
280                    "optional items are not supported in runtime-parsed format descriptions",
281                )),
282                public: InvalidFormatDescription::NotSupported {
283                    what: "optional item",
284                    context: "runtime-parsed format descriptions",
285                    index: span.start.byte as usize,
286                },
287            }),
288            Item::First { value: _, span } => Err(Error {
289                _inner: unused(span.error(
290                    "'first' items are not supported in runtime-parsed format descriptions",
291                )),
292                public: InvalidFormatDescription::NotSupported {
293                    what: "'first' item",
294                    context: "runtime-parsed format descriptions",
295                    index: span.start.byte as usize,
296                },
297            }),
298        }
299    }
300}
301
302impl TryFrom<Item<'_>> for crate::format_description::OwnedFormatItem {
303    type Error = Error;
304
305    #[inline]
306    fn try_from(item: Item<'_>) -> Result<Self, Self::Error> {
307        match item {
308            #[expect(deprecated)]
309            Item::Literal(literal) => Ok(Self::Literal(literal.to_vec().into_boxed_slice())),
310            Item::Component(component) => Ok(Self::Component(component.try_into()?)),
311            Item::Optional {
312                value,
313                format,
314                span: _,
315            } => {
316                if !*format {
317                    return Err(Error {
318                        _inner: unused(format.span.error(
319                            "v1 and v2 format descriptions do not support optional items that are \
320                             not formatted",
321                        )),
322                        public: InvalidFormatDescription::NotSupported {
323                            what: "optional item with `format:false`",
324                            context: "v1 and v2 format descriptions",
325                            index: format.span.start.byte as usize,
326                        },
327                    });
328                }
329                Ok(Self::Optional(Box::new(value.try_into()?)))
330            }
331            Item::First { value, span: _ } => Ok(Self::First(
332                value
333                    .into_vec()
334                    .into_iter()
335                    .map(Self::try_from)
336                    .collect::<Result<_, _>>()?,
337            )),
338        }
339    }
340}
341
342impl<'a> TryFrom<Box<[Item<'a>]>> for crate::format_description::OwnedFormatItem {
343    type Error = Error;
344
345    #[inline]
346    fn try_from(items: Box<[Item<'a>]>) -> Result<Self, Self::Error> {
347        let items = items.into_vec();
348        match <[_; 1]>::try_from(items) {
349            Ok([item]) => item.try_into(),
350            Err(vec) => Ok(Self::Compound(
351                vec.into_iter()
352                    .map(Self::try_from)
353                    .collect::<Result<_, _>>()?,
354            )),
355        }
356    }
357}
358
359impl<'a> TryFrom<Item<'a>> for crate::format_description::__private::FormatDescriptionV3Inner<'a> {
360    type Error = Error;
361
362    #[inline]
363    fn try_from(item: Item<'a>) -> Result<Self, Self::Error> {
364        match item {
365            // Safety: The only way to get a literal for a version 3 format description originates
366            // with `&str`, which is guaranteed to be UTF-8.
367            Item::Literal(literal) => Ok(Self::BorrowedLiteral(unsafe {
368                str::from_utf8_unchecked(literal)
369            })),
370            Item::Component(component) => Ok(component.try_into()?),
371            Item::Optional {
372                value,
373                format,
374                span: _,
375            } => Ok(Self::OwnedOptional {
376                format: *format,
377                item: Box::new(value.try_into()?),
378            }),
379            Item::First { value, span: _ } => Ok(Self::OwnedFirst(
380                value
381                    .into_vec()
382                    .into_iter()
383                    .map(Self::try_from)
384                    .collect::<Result<_, _>>()?,
385            )),
386        }
387    }
388}
389
390impl<'a> TryFrom<Box<[Item<'a>]>>
391    for crate::format_description::__private::FormatDescriptionV3Inner<'a>
392{
393    type Error = Error;
394
395    #[inline]
396    fn try_from(items: Box<[Item<'a>]>) -> Result<Self, Self::Error> {
397        let items = items.into_vec();
398        match <[_; 1]>::try_from(items) {
399            Ok([item]) => item.try_into(),
400            Err(vec) => Ok(Self::OwnedCompound(
401                vec.into_iter()
402                    .map(Self::try_from)
403                    .collect::<Result<_, _>>()?,
404            )),
405        }
406    }
407}
408
409/// Declare the `Component` struct.
410macro_rules! component_definition {
411    (@if_required required then { $($then:tt)* } $(else { $($else:tt)* })?) => { $($then)* };
412    (@if_required then { $($then:tt)* } $(else { $($else:tt)* })?) => { $($($else)*)? };
413    (@if_from_str from_str then { $($then:tt)* } $(else { $($else:tt)* })?) => { $($then)* };
414    (@if_from_str then { $($then:tt)* } $(else { $($else:tt)* })?) => { $($($else)*)? };
415
416    ($vis:vis enum $name:ident {$(
417        $variant:ident = $parse_variant:literal {$(
418            $(#[$required:tt])?
419            $field:ident = $parse_field:literal:
420            Option<$(#[$from_str:tt])? $field_type:ty>
421        ),* $(,)?}
422    ),* $(,)?}) => {
423        $vis enum $name {
424            $($variant($variant),)*
425        }
426
427        $($vis struct $variant {
428            $($field: Spanned<Option<$field_type>>),*
429        })*
430
431        $(impl $variant {
432            /// Parse the component from the AST, given its modifiers.
433            #[inline]
434            fn with_modifiers(
435                version: FormatDescriptionVersion,
436                modifiers: &[ast::Modifier<'_>],
437                _component_span: Span,
438            ) -> Result<Self, Error>
439            {
440                // rustc will complain if the modifier is empty.
441                #[allow(unused_mut)]
442                let mut this = Self {
443                    $($field: None.spanned(Span::DUMMY)),*
444                };
445
446                for modifier in modifiers {
447                    $(#[expect(clippy::string_lit_as_bytes)]
448                    if modifier.key.eq_ignore_ascii_case($parse_field.as_bytes()) {
449                        if version.is_at_least_v3() && this.$field.is_some() {
450                            return Err(Error {
451                                _inner: unused(modifier.key.span.error("duplicate modifier key")),
452                                public: InvalidFormatDescription::DuplicateModifier {
453                                    name: stringify!($field),
454                                    index: modifier.key.span.start.byte as usize,
455                                }
456                            });
457                        }
458                        this.$field = Some(
459                            component_definition!(@if_from_str $($from_str)?
460                                then {
461                                    parse_from_modifier_value::<$field_type>(&modifier.value)?
462                                } else {
463                                    <$field_type>::from_modifier_value(&modifier.value)?
464                                }
465                            )
466                        ).spanned(modifier.key_value_span());
467                        continue;
468                    })*
469                    return Err(Error {
470                        _inner: unused(modifier.key.span.error("invalid modifier key")),
471                        public: InvalidFormatDescription::InvalidModifier {
472                            value: String::from_utf8_lossy(*modifier.key).into_owned(),
473                            index: modifier.key.span.start.byte as usize,
474                        }
475                    });
476                }
477
478                $(component_definition! { @if_required $($required)? then {
479                    if this.$field.is_none() {
480                        return Err(Error {
481                            _inner: unused(_component_span.error("missing required modifier")),
482                            public:
483                                InvalidFormatDescription::MissingRequiredModifier {
484                                    name: $parse_field,
485                                    index: _component_span.start.byte as usize,
486                                }
487                        });
488                    }
489                }})*
490
491                Ok(this)
492            }
493        })*
494
495        /// Parse a component from the AST, given its name and modifiers.
496        #[inline]
497        fn component_from_ast(
498            version: FormatDescriptionVersion,
499            name: &Spanned<&[u8]>,
500            modifiers: &[ast::Modifier<'_>],
501        ) -> Result<AstComponent, Error> {
502            $(#[expect(clippy::string_lit_as_bytes)]
503            if name.eq_ignore_ascii_case($parse_variant.as_bytes()) {
504                return Ok(AstComponent::$variant(
505                    $variant::with_modifiers(version, &modifiers, name.span)?
506                ));
507            })*
508            Err(Error {
509                _inner: unused(name.span.error("invalid component")),
510                public: InvalidFormatDescription::InvalidComponentName {
511                    name: String::from_utf8_lossy(name).into_owned(),
512                    index: name.span.start.byte as usize,
513                },
514            })
515        }
516    }
517}
518
519// Keep in alphabetical order.
520component_definition! {
521    pub(super) enum AstComponent {
522        Day = "day" {
523            padding = "padding": Option<Padding>,
524        },
525        End = "end" {
526            trailing_input = "trailing_input": Option<TrailingInput>,
527        },
528        Hour = "hour" {
529            padding = "padding": Option<Padding>,
530            base = "repr": Option<HourBase>,
531        },
532        Ignore = "ignore" {
533            #[required]
534            count = "count": Option<#[from_str] NonZero<u16>>,
535        },
536        Minute = "minute" {
537            padding = "padding": Option<Padding>,
538        },
539        Month = "month" {
540            padding = "padding": Option<Padding>,
541            repr = "repr": Option<MonthRepr>,
542            case_sensitive = "case_sensitive": Option<MonthCaseSensitive>,
543        },
544        OffsetHour = "offset_hour" {
545            sign_behavior = "sign": Option<SignBehavior>,
546            padding = "padding": Option<Padding>,
547        },
548        OffsetMinute = "offset_minute" {
549            padding = "padding": Option<Padding>,
550        },
551        OffsetSecond = "offset_second" {
552            padding = "padding": Option<Padding>,
553        },
554        Ordinal = "ordinal" {
555            padding = "padding": Option<Padding>,
556        },
557        Period = "period" {
558            case = "case": Option<PeriodCase>,
559            case_sensitive = "case_sensitive": Option<PeriodCaseSensitive>,
560        },
561        Second = "second" {
562            padding = "padding": Option<Padding>,
563        },
564        Subsecond = "subsecond" {
565            digits = "digits": Option<SubsecondDigits>,
566        },
567        UnixTimestamp = "unix_timestamp" {
568            precision = "precision": Option<UnixTimestampPrecision>,
569            sign_behavior = "sign": Option<SignBehavior>,
570        },
571        Weekday = "weekday" {
572            repr = "repr": Option<WeekdayRepr>,
573            one_indexed = "one_indexed": Option<WeekdayOneIndexed>,
574            case_sensitive = "case_sensitive": Option<WeekdayCaseSensitive>,
575        },
576        WeekNumber = "week_number" {
577            padding = "padding": Option<Padding>,
578            repr = "repr": Option<WeekNumberRepr>,
579        },
580        Year = "year" {
581            padding = "padding": Option<Padding>,
582            repr = "repr": Option<YearRepr>,
583            range = "range": Option<YearRange>,
584            base = "base": Option<YearBase>,
585            sign_behavior = "sign": Option<SignBehavior>,
586        },
587    }
588}
589
590macro_rules! impl_from_ast_component_for {
591    ($([$reject_nonsensical:literal] $ty:ty),+ $(,)?) => {$(
592        impl TryFrom<AstComponent> for $ty {
593            type Error = Error;
594
595            #[inline]
596            fn try_from(component: AstComponent) -> Result<Self, Self::Error> {
597                macro_rules! reject_modifier {
598                    ($modifier:ident, $modifier_str:literal, $context:literal) => {
599                        if $reject_nonsensical && $modifier.value.is_some() {
600                            return Err(Error {
601                                _inner: unused($modifier.span.error(concat!(
602                                    "the '",
603                                    $modifier_str,
604                                    "' modifier is not valid ",
605                                    $context
606                                ))),
607                                public: InvalidFormatDescription::InvalidModifierCombination {
608                                    modifier: $modifier_str,
609                                    context: $context,
610                                    index: $modifier.span.start.byte as usize,
611                                },
612                            });
613                        }
614                    };
615                }
616
617                use crate::format_description::modifier;
618                Ok(match component {
619                    AstComponent::Day(Day { padding }) => Self::Day(modifier::Day {
620                        padding: padding.unwrap_or_default().into(),
621                    }),
622                    AstComponent::End(End { trailing_input }) => Self::End(modifier::End {
623                        trailing_input: trailing_input.unwrap_or_default().into(),
624                    }),
625                    AstComponent::Hour(Hour { padding, base }) => match base.unwrap_or_default() {
626                        HourBase::Twelve => Self::Hour12(modifier::Hour12 {
627                            padding: padding.unwrap_or_default().into(),
628                        }),
629                        HourBase::TwentyFour => Self::Hour24(modifier::Hour24 {
630                            padding: padding.unwrap_or_default().into(),
631                        }),
632                    },
633                    AstComponent::Ignore(Ignore { count }) => Self::Ignore(modifier::Ignore {
634                        count: match *count {
635                            Some(value) => value,
636                            None => bug!("required modifier was not set"),
637                        },
638                    }),
639                    AstComponent::Minute(Minute { padding }) => Self::Minute(modifier::Minute {
640                        padding: padding.unwrap_or_default().into(),
641                    }),
642                    AstComponent::Month(Month {
643                        padding,
644                        repr,
645                        case_sensitive,
646                    }) => match repr.unwrap_or_default() {
647                        MonthRepr::Numerical => {
648                            reject_modifier!(
649                                case_sensitive,
650                                "case_sensitive",
651                                "for numerical month"
652                            );
653                            Self::MonthNumerical(modifier::MonthNumerical {
654                                padding: padding.unwrap_or_default().into(),
655                            })
656                        },
657                        MonthRepr::Long => {
658                            reject_modifier!(padding, "padding", "for long month");
659                            Self::MonthLong(modifier::MonthLong {
660                                case_sensitive: case_sensitive.unwrap_or_default().into(),
661                            })
662                        },
663                        MonthRepr::Short => {
664                            reject_modifier!(padding, "padding", "for short month");
665                            Self::MonthShort(modifier::MonthShort {
666                                case_sensitive: case_sensitive.unwrap_or_default().into(),
667                            })
668                        },
669                    },
670                    AstComponent::OffsetHour(OffsetHour {
671                        sign_behavior,
672                        padding,
673                    }) => Self::OffsetHour(modifier::OffsetHour {
674                        sign_is_mandatory: sign_behavior.unwrap_or_default().into(),
675                        padding: padding.unwrap_or_default().into(),
676                    }),
677                    AstComponent::OffsetMinute(OffsetMinute { padding }) => {
678                        Self::OffsetMinute(modifier::OffsetMinute {
679                            padding: padding.unwrap_or_default().into(),
680                        })
681                    }
682                    AstComponent::OffsetSecond(OffsetSecond { padding }) => {
683                        Self::OffsetSecond(modifier::OffsetSecond {
684                            padding: padding.unwrap_or_default().into(),
685                        })
686                    }
687                    AstComponent::Ordinal(Ordinal { padding }) => Self::Ordinal(modifier::Ordinal {
688                        padding: padding.unwrap_or_default().into(),
689                    }),
690                    AstComponent::Period(Period {
691                        case,
692                        case_sensitive,
693                    }) => Self::Period(modifier::Period {
694                        is_uppercase: case.unwrap_or_default().into(),
695                        case_sensitive: case_sensitive.unwrap_or_default().into(),
696                    }),
697                    AstComponent::Second(Second { padding }) => Self::Second(modifier::Second {
698                        padding: padding.unwrap_or_default().into(),
699                    }),
700                    AstComponent::Subsecond(Subsecond { digits }) => {
701                        Self::Subsecond(modifier::Subsecond {
702                            digits: digits.unwrap_or_default().into(),
703                        })
704                    },
705                    AstComponent::UnixTimestamp(UnixTimestamp {
706                        precision,
707                        sign_behavior,
708                    }) => match precision.unwrap_or_default() {
709                        UnixTimestampPrecision::Second => {
710                            Self::UnixTimestampSecond(modifier::UnixTimestampSecond {
711                                sign_is_mandatory: sign_behavior.unwrap_or_default().into(),
712                            })
713                        }
714                        UnixTimestampPrecision::Millisecond => {
715                            Self::UnixTimestampMillisecond(modifier::UnixTimestampMillisecond {
716                                sign_is_mandatory: sign_behavior.unwrap_or_default().into(),
717                            })
718                        }
719                        UnixTimestampPrecision::Microsecond => {
720                            Self::UnixTimestampMicrosecond(modifier::UnixTimestampMicrosecond {
721                                sign_is_mandatory: sign_behavior.unwrap_or_default().into(),
722                            })
723                        }
724                        UnixTimestampPrecision::Nanosecond => {
725                            Self::UnixTimestampNanosecond(modifier::UnixTimestampNanosecond {
726                                sign_is_mandatory: sign_behavior.unwrap_or_default().into(),
727                            })
728                        }
729                    },
730                    AstComponent::Weekday(Weekday {
731                        repr,
732                        one_indexed,
733                        case_sensitive,
734                    }) => match repr.unwrap_or_default() {
735                        WeekdayRepr::Short => {
736                            reject_modifier!(one_indexed, "one_indexed", "for short weekday");
737                            Self::WeekdayShort(modifier::WeekdayShort {
738                                case_sensitive: case_sensitive.unwrap_or_default().into(),
739                            })
740                        },
741                        WeekdayRepr::Long => {
742                            reject_modifier!(one_indexed, "one_indexed", "for long weekday");
743                            Self::WeekdayLong(modifier::WeekdayLong {
744                                case_sensitive: case_sensitive.unwrap_or_default().into(),
745                            })
746                        },
747                        WeekdayRepr::Sunday => {
748                            reject_modifier!(
749                                case_sensitive,
750                                "case_sensitive",
751                                "for numerical weekday"
752                            );
753                            Self::WeekdaySunday(modifier::WeekdaySunday {
754                                one_indexed: one_indexed.unwrap_or_default().into(),
755                            })
756                        },
757                        WeekdayRepr::Monday => {
758                            reject_modifier!(
759                                case_sensitive,
760                                "case_sensitive",
761                                "for numerical weekday"
762                            );
763                            Self::WeekdayMonday(modifier::WeekdayMonday {
764                                one_indexed: one_indexed.unwrap_or_default().into(),
765                            })
766                        },
767                    },
768                    AstComponent::WeekNumber(WeekNumber { padding, repr }) => {
769                        match repr.unwrap_or_default() {
770                            WeekNumberRepr::Iso => {
771                                Self::WeekNumberIso(modifier::WeekNumberIso {
772                                    padding: padding.unwrap_or_default().into(),
773                                })
774                            },
775                            WeekNumberRepr::Sunday => {
776                                Self::WeekNumberSunday(modifier::WeekNumberSunday {
777                                    padding: padding.unwrap_or_default().into(),
778                                })
779                            },
780                            WeekNumberRepr::Monday => {
781                                Self::WeekNumberMonday(modifier::WeekNumberMonday {
782                                    padding: padding.unwrap_or_default().into(),
783                                })
784                            },
785                        }
786                    }
787                    AstComponent::Year(Year {
788                        padding,
789                        repr,
790                        range,
791                        base,
792                        sign_behavior,
793                    }) => {
794                        #[cfg(not(feature = "large-dates"))]
795                        reject_modifier!(
796                            range,
797                            "range",
798                            "when the `large-dates` feature is not enabled"
799                        );
800
801                        match (
802                            base.unwrap_or_default(),
803                            repr.unwrap_or_default(),
804                            range.unwrap_or_default(),
805                        ) {
806                            #[cfg(feature = "large-dates")]
807                            (YearBase::Calendar, YearRepr::Full, YearRange::Extended) => {
808                                Self::CalendarYearFullExtendedRange(
809                                    modifier::CalendarYearFullExtendedRange {
810                                        padding: padding.unwrap_or_default().into(),
811                                        sign_is_mandatory: sign_behavior.unwrap_or_default().into(),
812                                    },
813                                )
814                            }
815                            (YearBase::Calendar, YearRepr::Full, _) => {
816                                Self::CalendarYearFullStandardRange(
817                                    modifier::CalendarYearFullStandardRange {
818                                        padding: padding.unwrap_or_default().into(),
819                                        sign_is_mandatory: sign_behavior.unwrap_or_default().into(),
820                                    },
821                                )
822                            }
823                            #[cfg(feature = "large-dates")]
824                            (YearBase::Calendar, YearRepr::Century, YearRange::Extended) => {
825                                Self::CalendarYearCenturyExtendedRange(
826                                    modifier::CalendarYearCenturyExtendedRange {
827                                        padding: padding.unwrap_or_default().into(),
828                                        sign_is_mandatory: sign_behavior.unwrap_or_default().into(),
829                                    },
830                                )
831                            }
832                            (YearBase::Calendar, YearRepr::Century, _) => {
833                                Self::CalendarYearCenturyStandardRange(
834                                    modifier::CalendarYearCenturyStandardRange {
835                                        padding: padding.unwrap_or_default().into(),
836                                        sign_is_mandatory: sign_behavior.unwrap_or_default().into(),
837                                    },
838                                )
839                            }
840                            #[cfg(feature = "large-dates")]
841                            (YearBase::IsoWeek, YearRepr::Full, YearRange::Extended) => {
842                                Self::IsoYearFullExtendedRange(modifier::IsoYearFullExtendedRange {
843                                    padding: padding.unwrap_or_default().into(),
844                                    sign_is_mandatory: sign_behavior.unwrap_or_default().into(),
845                                })
846                            }
847                            (YearBase::IsoWeek, YearRepr::Full, _) => {
848                                Self::IsoYearFullStandardRange(modifier::IsoYearFullStandardRange {
849                                    padding: padding.unwrap_or_default().into(),
850                                    sign_is_mandatory: sign_behavior.unwrap_or_default().into(),
851                                })
852                            }
853                            #[cfg(feature = "large-dates")]
854                            (YearBase::IsoWeek, YearRepr::Century, YearRange::Extended) => {
855                                Self::IsoYearCenturyExtendedRange(
856                                    modifier::IsoYearCenturyExtendedRange {
857                                        padding: padding.unwrap_or_default().into(),
858                                        sign_is_mandatory: sign_behavior.unwrap_or_default().into(),
859                                    },
860                                )
861                            }
862                            (YearBase::IsoWeek, YearRepr::Century, _) => {
863                                Self::IsoYearCenturyStandardRange(
864                                    modifier::IsoYearCenturyStandardRange {
865                                        padding: padding.unwrap_or_default().into(),
866                                        sign_is_mandatory: sign_behavior.unwrap_or_default().into(),
867                                    },
868                                )
869                            }
870                            (YearBase::Calendar, YearRepr::LastTwo, _) => {
871                                #[cfg(feature = "large-dates")]
872                                reject_modifier!(range, "range", "when `repr:last_two` is used");
873                                reject_modifier!(
874                                    sign_behavior,
875                                    "sign",
876                                    "when `repr:last_two` is used"
877                                );
878                                Self::CalendarYearLastTwo(modifier::CalendarYearLastTwo {
879                                    padding: padding.unwrap_or_default().into(),
880                                })
881                            }
882                            (YearBase::IsoWeek, YearRepr::LastTwo, _) => {
883                                #[cfg(feature = "large-dates")]
884                                reject_modifier!(range, "range", "when `repr:last_two` is used");
885                                reject_modifier!(
886                                    sign_behavior,
887                                    "sign",
888                                    "when `repr:last_two` is used"
889                                );
890                                Self::IsoYearLastTwo(modifier::IsoYearLastTwo {
891                                    padding: padding.unwrap_or_default().into(),
892                                })
893                            }
894                        }
895                    }
896                })
897            }
898        })+
899    }
900}
901
902impl_from_ast_component_for!(
903    [false] crate::format_description::Component,
904    [true] crate::format_description::__private::FormatDescriptionV3Inner<'_>,
905);
906
907/// Get the target type for a given enum.
908macro_rules! target_ty {
909    ($name:ident $type:ty) => {
910        $type
911    };
912    ($name:ident) => {
913        $crate::format_description::modifier::$name
914    };
915}
916
917/// Get the target value for a given enum.
918macro_rules! target_value {
919    ($name:ident $variant:ident $value:expr) => {
920        $value
921    };
922    ($name:ident $variant:ident) => {
923        $crate::format_description::modifier::$name::$variant
924    };
925}
926
927trait ModifierValue {
928    type Type;
929}
930
931/// Declare the various modifiers.
932///
933/// For the general case, ordinary syntax can be used. Note that you _must_ declare a default
934/// variant. The only significant change is that the string representation of the variant must be
935/// provided after the variant name. For example, `Numerical = b"numerical"` declares a variant
936/// named `Numerical` with the string representation `b"numerical"`. This is the value that will be
937/// used when parsing the modifier. The value is not case sensitive.
938///
939/// If the type in the public API does not have the same name as the type in the internal
940/// representation, then the former must be specified in parenthesis after the internal name. For
941/// example, `HourBase(bool)` has an internal name "HourBase", but is represented as a boolean in
942/// the public API.
943///
944/// By default, the internal variant name is assumed to be the same as the public variant name. If
945/// this is not the case, the qualified path to the variant must be specified in parenthesis after
946/// the internal variant name. For example, `Twelve(true)` has an internal variant name "Twelve",
947/// but is represented as `true` in the public API.
948macro_rules! modifier {
949    ($(
950        $(#[expect($expect_inner:meta)])?
951        enum $name:ident $(($target_ty:ty))? {
952            $(
953                $(#[$attr:meta])?
954                $variant:ident $(($target_value:expr))? = $parse_variant:literal
955            ),* $(,)?
956        }
957    )+) => {$(
958        #[derive(Default, Clone, Copy)]
959        enum $name {
960            $($(#[$attr])? $variant),*
961        }
962
963        impl $name {
964            /// Parse the modifier from its string representation.
965            #[inline]
966            fn from_modifier_value(value: &Spanned<&[u8]>) -> Result<Self, Error> {
967                $(if value.eq_ignore_ascii_case($parse_variant) {
968                    return Ok(Self::$variant);
969                })*
970                Err(Error {
971                    _inner: unused(value.span.error("invalid modifier value")),
972                    public: InvalidFormatDescription::InvalidModifier {
973                        value: String::from_utf8_lossy(value).into_owned(),
974                        index: value.span.start.byte as usize,
975                    },
976                })
977            }
978        }
979
980        $(#[expect($expect_inner)])?
981        impl ModifierValue for $name {
982            type Type = target_ty!($name $($target_ty)?);
983        }
984
985        $(#[expect($expect_inner)])?
986        impl From<$name> for <$name as ModifierValue>::Type {
987            #[inline]
988            fn from(modifier: $name) -> Self {
989                match modifier {
990                    $($name::$variant => target_value!($name $variant $($target_value)?)),*
991                }
992            }
993        }
994    )+};
995}
996
997// Keep in alphabetical order.
998modifier! {
999    enum HourBase(bool) {
1000        Twelve(true) = b"12",
1001        #[default]
1002        TwentyFour(false) = b"24",
1003    }
1004
1005    enum MonthCaseSensitive(bool) {
1006        False(false) = b"false",
1007        #[default]
1008        True(true) = b"true",
1009    }
1010
1011    #[expect(deprecated)]
1012    enum MonthRepr {
1013        #[default]
1014        Numerical = b"numerical",
1015        Long = b"long",
1016        Short = b"short",
1017    }
1018
1019    enum OptionalFormat(bool) {
1020        False(false) = b"false",
1021        #[default]
1022        True(true) = b"true",
1023    }
1024
1025    enum Padding {
1026        Space = b"space",
1027        #[default]
1028        Zero = b"zero",
1029        None = b"none",
1030    }
1031
1032    enum PeriodCase(bool) {
1033        Lower(false) = b"lower",
1034        #[default]
1035        Upper(true) = b"upper",
1036    }
1037
1038    enum PeriodCaseSensitive(bool) {
1039        False(false) = b"false",
1040        #[default]
1041        True(true) = b"true",
1042    }
1043
1044    enum SignBehavior(bool) {
1045        #[default]
1046        Automatic(false) = b"automatic",
1047        Mandatory(true) = b"mandatory",
1048    }
1049
1050    enum SubsecondDigits {
1051        One = b"1",
1052        Two = b"2",
1053        Three = b"3",
1054        Four = b"4",
1055        Five = b"5",
1056        Six = b"6",
1057        Seven = b"7",
1058        Eight = b"8",
1059        Nine = b"9",
1060        #[default]
1061        OneOrMore = b"1+",
1062    }
1063
1064    enum TrailingInput {
1065        #[default]
1066        Prohibit = b"prohibit",
1067        Discard = b"discard",
1068    }
1069
1070    #[expect(deprecated)]
1071    enum UnixTimestampPrecision {
1072        #[default]
1073        Second = b"second",
1074        Millisecond = b"millisecond",
1075        Microsecond = b"microsecond",
1076        Nanosecond = b"nanosecond",
1077    }
1078
1079    #[expect(deprecated)]
1080    enum WeekNumberRepr {
1081        #[default]
1082        Iso = b"iso",
1083        Sunday = b"sunday",
1084        Monday = b"monday",
1085    }
1086
1087    enum WeekdayCaseSensitive(bool) {
1088        False(false) = b"false",
1089        #[default]
1090        True(true) = b"true",
1091    }
1092
1093    enum WeekdayOneIndexed(bool) {
1094        False(false) = b"false",
1095        #[default]
1096        True(true) = b"true",
1097    }
1098
1099    #[expect(deprecated)]
1100    enum WeekdayRepr {
1101        Short = b"short",
1102        #[default]
1103        Long = b"long",
1104        Sunday = b"sunday",
1105        Monday = b"monday",
1106    }
1107
1108    enum YearBase(bool) {
1109        #[default]
1110        Calendar(false) = b"calendar",
1111        IsoWeek(true) = b"iso_week",
1112    }
1113
1114    #[expect(deprecated)]
1115    enum YearRepr {
1116        #[default]
1117        Full = b"full",
1118        Century = b"century",
1119        LastTwo = b"last_two",
1120    }
1121
1122    // For v1 and v2 format descriptions, the default is `extended`. For v3 format descriptions,
1123    // the default is `standard`. For backwards compatibility, the default here needs to stay
1124    // `extended`.
1125    #[expect(deprecated)]
1126    enum YearRange {
1127        Standard = b"standard",
1128        #[default]
1129        Extended = b"extended",
1130    }
1131}
1132
1133/// Parse a modifier value using `FromStr`. Requires the modifier value to be valid UTF-8.
1134#[inline]
1135fn parse_from_modifier_value<T>(value: &Spanned<&[u8]>) -> Result<T, Error>
1136where
1137    T: FromStr,
1138{
1139    str::from_utf8(value)
1140        .ok()
1141        .and_then(|val| val.parse::<T>().ok())
1142        .ok_or_else(|| Error {
1143            _inner: unused(value.span.error("invalid modifier value")),
1144            public: InvalidFormatDescription::InvalidModifier {
1145                value: String::from_utf8_lossy(value).into_owned(),
1146                index: value.span.start.byte as usize,
1147            },
1148        })
1149}