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