Skip to main content

time/format_description/parse/
format_item.rs

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