Skip to main content

time/format_description/parse/
format_item.rs

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