Skip to main content

time/format_description/parse/
format_item.rs

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