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            Item::Literal(literal) => Ok(Self::Literal(literal)),
113            Item::Component(component) => Ok(Self::Component(component.into())),
114            Item::Optional { value: _, span } => Err(Error {
115                _inner: unused(span.error(
116                    "optional items are not supported in runtime-parsed format descriptions",
117                )),
118                public: crate::error::InvalidFormatDescription::NotSupported {
119                    what: "optional item",
120                    context: "runtime-parsed format descriptions",
121                    index: span.start.byte as usize,
122                },
123            }),
124            Item::First { value: _, span } => Err(Error {
125                _inner: unused(span.error(
126                    "'first' items are not supported in runtime-parsed format descriptions",
127                )),
128                public: crate::error::InvalidFormatDescription::NotSupported {
129                    what: "'first' item",
130                    context: "runtime-parsed format descriptions",
131                    index: span.start.byte as usize,
132                },
133            }),
134        }
135    }
136}
137
138impl From<Item<'_>> for crate::format_description::OwnedFormatItem {
139    #[inline]
140    fn from(item: Item<'_>) -> Self {
141        match item {
142            Item::Literal(literal) => Self::Literal(literal.to_vec().into_boxed_slice()),
143            Item::Component(component) => Self::Component(component.into()),
144            Item::Optional { value, span: _ } => Self::Optional(Box::new(value.into())),
145            Item::First { value, span: _ } => {
146                Self::First(value.into_vec().into_iter().map(Into::into).collect())
147            }
148        }
149    }
150}
151
152impl<'a> From<Box<[Item<'a>]>> for crate::format_description::OwnedFormatItem {
153    #[inline]
154    fn from(items: Box<[Item<'a>]>) -> Self {
155        let items = items.into_vec();
156        match <[_; 1]>::try_from(items) {
157            Ok([item]) => item.into(),
158            Err(vec) => Self::Compound(vec.into_iter().map(Into::into).collect()),
159        }
160    }
161}
162
163/// Declare the `Component` struct.
164macro_rules! component_definition {
165    (@if_required required then { $($then:tt)* } $(else { $($else:tt)* })?) => { $($then)* };
166    (@if_required then { $($then:tt)* } $(else { $($else:tt)* })?) => { $($($else)*)? };
167    (@if_from_str from_str then { $($then:tt)* } $(else { $($else:tt)* })?) => { $($then)* };
168    (@if_from_str then { $($then:tt)* } $(else { $($else:tt)* })?) => { $($($else)*)? };
169
170    ($vis:vis enum $name:ident {
171        $($variant:ident = $parse_variant:literal {$(
172            $(#[$required:tt])?
173            $field:ident = $parse_field:literal:
174            Option<$(#[$from_str:tt])? $field_type:ty>
175            => $target_field:ident
176        ),* $(,)?}),* $(,)?
177    }) => {
178        $vis enum $name {
179            $($variant($variant),)*
180        }
181
182        $($vis struct $variant {
183            $($field: Option<$field_type>),*
184        })*
185
186        $(impl $variant {
187            /// Parse the component from the AST, given its modifiers.
188            #[inline]
189            fn with_modifiers(
190                modifiers: &[ast::Modifier<'_>],
191                _component_span: Span,
192            ) -> Result<Self, Error>
193            {
194                // rustc will complain if the modifier is empty.
195                #[allow(unused_mut)]
196                let mut this = Self {
197                    $($field: None),*
198                };
199
200                for modifier in modifiers {
201                    $(#[expect(clippy::string_lit_as_bytes)]
202                    if modifier.key.eq_ignore_ascii_case($parse_field.as_bytes()) {
203                        this.$field = component_definition!(@if_from_str $($from_str)?
204                            then {
205                                parse_from_modifier_value::<$field_type>(&modifier.value)?
206                            } else {
207                                <$field_type>::from_modifier_value(&modifier.value)?
208                            });
209                        continue;
210                    })*
211                    return Err(Error {
212                        _inner: unused(modifier.key.span.error("invalid modifier key")),
213                        public: crate::error::InvalidFormatDescription::InvalidModifier {
214                            value: String::from_utf8_lossy(*modifier.key).into_owned(),
215                            index: modifier.key.span.start.byte as usize,
216                        }
217                    });
218                }
219
220                $(component_definition! { @if_required $($required)? then {
221                    if this.$field.is_none() {
222                        return Err(Error {
223                            _inner: unused(_component_span.error("missing required modifier")),
224                            public:
225                                crate::error::InvalidFormatDescription::MissingRequiredModifier {
226                                    name: $parse_field,
227                                    index: _component_span.start.byte as usize,
228                                }
229                        });
230                    }
231                }})*
232
233                Ok(this)
234            }
235        })*
236
237        impl From<$name> for crate::format_description::Component {
238            #[inline]
239            fn from(component: $name) -> Self {
240                match component {$(
241                    $name::$variant($variant { $($field),* }) => {
242                        $crate::format_description::component::Component::$variant(
243                            $crate::format_description::modifier::$variant {$(
244                                $target_field: component_definition! { @if_required $($required)?
245                                    then {
246                                        match $field {
247                                            Some(value) => value.into(),
248                                            None => bug!("required modifier was not set"),
249                                        }
250                                    } else {
251                                        $field.unwrap_or_default().into()
252                                    }
253                                }
254                            ),*}
255                        )
256                    }
257                )*}
258            }
259        }
260
261        /// Parse a component from the AST, given its name and modifiers.
262        #[inline]
263        fn component_from_ast(
264            name: &Spanned<&[u8]>,
265            modifiers: &[ast::Modifier<'_>],
266        ) -> Result<Component, Error> {
267            $(#[expect(clippy::string_lit_as_bytes)]
268            if name.eq_ignore_ascii_case($parse_variant.as_bytes()) {
269                return Ok(Component::$variant($variant::with_modifiers(&modifiers, name.span)?));
270            })*
271            Err(Error {
272                _inner: unused(name.span.error("invalid component")),
273                public: crate::error::InvalidFormatDescription::InvalidComponentName {
274                    name: String::from_utf8_lossy(name).into_owned(),
275                    index: name.span.start.byte as usize,
276                },
277            })
278        }
279    }
280}
281
282// Keep in alphabetical order.
283pub(super) enum Component {
    Day(Day),
    End(End),
    Hour(Hour),
    Ignore(Ignore),
    Minute(Minute),
    Month(Month),
    OffsetHour(OffsetHour),
    OffsetMinute(OffsetMinute),
    OffsetSecond(OffsetSecond),
    Ordinal(Ordinal),
    Period(Period),
    Second(Second),
    Subsecond(Subsecond),
    UnixTimestamp(UnixTimestamp),
    Weekday(Weekday),
    WeekNumber(WeekNumber),
    Year(Year),
}
pub(super) struct Day {
    padding: Option<Padding>,
}
pub(super) struct End {
    trailing_input: Option<TrailingInput>,
}
pub(super) struct Hour {
    padding: Option<Padding>,
    base: Option<HourBase>,
}
pub(super) struct Ignore {
    count: Option<NonZero<u16>>,
}
pub(super) struct Minute {
    padding: Option<Padding>,
}
pub(super) struct Month {
    padding: Option<Padding>,
    repr: Option<MonthRepr>,
    case_sensitive: Option<MonthCaseSensitive>,
}
pub(super) struct OffsetHour {
    sign_behavior: Option<SignBehavior>,
    padding: Option<Padding>,
}
pub(super) struct OffsetMinute {
    padding: Option<Padding>,
}
pub(super) struct OffsetSecond {
    padding: Option<Padding>,
}
pub(super) struct Ordinal {
    padding: Option<Padding>,
}
pub(super) struct Period {
    case: Option<PeriodCase>,
    case_sensitive: Option<PeriodCaseSensitive>,
}
pub(super) struct Second {
    padding: Option<Padding>,
}
pub(super) struct Subsecond {
    digits: Option<SubsecondDigits>,
}
pub(super) struct UnixTimestamp {
    precision: Option<UnixTimestampPrecision>,
    sign_behavior: Option<SignBehavior>,
}
pub(super) struct Weekday {
    repr: Option<WeekdayRepr>,
    one_indexed: Option<WeekdayOneIndexed>,
    case_sensitive: Option<WeekdayCaseSensitive>,
}
pub(super) struct WeekNumber {
    padding: Option<Padding>,
    repr: Option<WeekNumberRepr>,
}
pub(super) struct Year {
    padding: Option<Padding>,
    repr: Option<YearRepr>,
    range: Option<YearRange>,
    base: Option<YearBase>,
    sign_behavior: Option<SignBehavior>,
}
impl Day {
    /// Parse the component from the AST, given its modifiers.
    #[inline]
    fn with_modifiers(modifiers: &[ast::Modifier<'_>], _component_span: Span)
        -> Result<Self, Error> {
        #[allow(unused_mut)]
        let mut this = Self { padding: None };
        for modifier in modifiers {

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("padding".as_bytes()) {
                this.padding =
                    <Padding>::from_modifier_value(&modifier.value)?;
                continue;
            }
            return Err(Error {
                        _inner: unused(modifier.key.span.error("invalid modifier key")),
                        public: crate::error::InvalidFormatDescription::InvalidModifier {
                            value: String::from_utf8_lossy(*modifier.key).into_owned(),
                            index: modifier.key.span.start.byte as usize,
                        },
                    });
        }
        Ok(this)
    }
}
impl End {
    /// Parse the component from the AST, given its modifiers.
    #[inline]
    fn with_modifiers(modifiers: &[ast::Modifier<'_>], _component_span: Span)
        -> Result<Self, Error> {
        #[allow(unused_mut)]
        let mut this = Self { trailing_input: None };
        for modifier in modifiers {

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("trailing_input".as_bytes())
                {
                this.trailing_input =
                    <TrailingInput>::from_modifier_value(&modifier.value)?;
                continue;
            }
            return Err(Error {
                        _inner: unused(modifier.key.span.error("invalid modifier key")),
                        public: crate::error::InvalidFormatDescription::InvalidModifier {
                            value: String::from_utf8_lossy(*modifier.key).into_owned(),
                            index: modifier.key.span.start.byte as usize,
                        },
                    });
        }
        Ok(this)
    }
}
impl Hour {
    /// Parse the component from the AST, given its modifiers.
    #[inline]
    fn with_modifiers(modifiers: &[ast::Modifier<'_>], _component_span: Span)
        -> Result<Self, Error> {
        #[allow(unused_mut)]
        let mut this = Self { padding: None, base: None };
        for modifier in modifiers {

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("padding".as_bytes()) {
                this.padding =
                    <Padding>::from_modifier_value(&modifier.value)?;
                continue;
            }

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("repr".as_bytes()) {
                this.base = <HourBase>::from_modifier_value(&modifier.value)?;
                continue;
            }
            return Err(Error {
                        _inner: unused(modifier.key.span.error("invalid modifier key")),
                        public: crate::error::InvalidFormatDescription::InvalidModifier {
                            value: String::from_utf8_lossy(*modifier.key).into_owned(),
                            index: modifier.key.span.start.byte as usize,
                        },
                    });
        }
        Ok(this)
    }
}
impl Ignore {
    /// Parse the component from the AST, given its modifiers.
    #[inline]
    fn with_modifiers(modifiers: &[ast::Modifier<'_>], _component_span: Span)
        -> Result<Self, Error> {
        #[allow(unused_mut)]
        let mut this = Self { count: None };
        for modifier in modifiers {

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("count".as_bytes()) {
                this.count =
                    parse_from_modifier_value::<NonZero<u16>>(&modifier.value)?;
                continue;
            }
            return Err(Error {
                        _inner: unused(modifier.key.span.error("invalid modifier key")),
                        public: crate::error::InvalidFormatDescription::InvalidModifier {
                            value: String::from_utf8_lossy(*modifier.key).into_owned(),
                            index: modifier.key.span.start.byte as usize,
                        },
                    });
        }
        if this.count.is_none() {
            return Err(Error {
                        _inner: unused(_component_span.error("missing required modifier")),
                        public: crate::error::InvalidFormatDescription::MissingRequiredModifier {
                            name: "count",
                            index: _component_span.start.byte as usize,
                        },
                    });
        }
        Ok(this)
    }
}
impl Minute {
    /// Parse the component from the AST, given its modifiers.
    #[inline]
    fn with_modifiers(modifiers: &[ast::Modifier<'_>], _component_span: Span)
        -> Result<Self, Error> {
        #[allow(unused_mut)]
        let mut this = Self { padding: None };
        for modifier in modifiers {

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("padding".as_bytes()) {
                this.padding =
                    <Padding>::from_modifier_value(&modifier.value)?;
                continue;
            }
            return Err(Error {
                        _inner: unused(modifier.key.span.error("invalid modifier key")),
                        public: crate::error::InvalidFormatDescription::InvalidModifier {
                            value: String::from_utf8_lossy(*modifier.key).into_owned(),
                            index: modifier.key.span.start.byte as usize,
                        },
                    });
        }
        Ok(this)
    }
}
impl Month {
    /// Parse the component from the AST, given its modifiers.
    #[inline]
    fn with_modifiers(modifiers: &[ast::Modifier<'_>], _component_span: Span)
        -> Result<Self, Error> {
        #[allow(unused_mut)]
        let mut this =
            Self { padding: None, repr: None, case_sensitive: None };
        for modifier in modifiers {

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("padding".as_bytes()) {
                this.padding =
                    <Padding>::from_modifier_value(&modifier.value)?;
                continue;
            }

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("repr".as_bytes()) {
                this.repr =
                    <MonthRepr>::from_modifier_value(&modifier.value)?;
                continue;
            }

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("case_sensitive".as_bytes())
                {
                this.case_sensitive =
                    <MonthCaseSensitive>::from_modifier_value(&modifier.value)?;
                continue;
            }
            return Err(Error {
                        _inner: unused(modifier.key.span.error("invalid modifier key")),
                        public: crate::error::InvalidFormatDescription::InvalidModifier {
                            value: String::from_utf8_lossy(*modifier.key).into_owned(),
                            index: modifier.key.span.start.byte as usize,
                        },
                    });
        }
        Ok(this)
    }
}
impl OffsetHour {
    /// Parse the component from the AST, given its modifiers.
    #[inline]
    fn with_modifiers(modifiers: &[ast::Modifier<'_>], _component_span: Span)
        -> Result<Self, Error> {
        #[allow(unused_mut)]
        let mut this = Self { sign_behavior: None, padding: None };
        for modifier in modifiers {

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("sign".as_bytes()) {
                this.sign_behavior =
                    <SignBehavior>::from_modifier_value(&modifier.value)?;
                continue;
            }

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("padding".as_bytes()) {
                this.padding =
                    <Padding>::from_modifier_value(&modifier.value)?;
                continue;
            }
            return Err(Error {
                        _inner: unused(modifier.key.span.error("invalid modifier key")),
                        public: crate::error::InvalidFormatDescription::InvalidModifier {
                            value: String::from_utf8_lossy(*modifier.key).into_owned(),
                            index: modifier.key.span.start.byte as usize,
                        },
                    });
        }
        Ok(this)
    }
}
impl OffsetMinute {
    /// Parse the component from the AST, given its modifiers.
    #[inline]
    fn with_modifiers(modifiers: &[ast::Modifier<'_>], _component_span: Span)
        -> Result<Self, Error> {
        #[allow(unused_mut)]
        let mut this = Self { padding: None };
        for modifier in modifiers {

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("padding".as_bytes()) {
                this.padding =
                    <Padding>::from_modifier_value(&modifier.value)?;
                continue;
            }
            return Err(Error {
                        _inner: unused(modifier.key.span.error("invalid modifier key")),
                        public: crate::error::InvalidFormatDescription::InvalidModifier {
                            value: String::from_utf8_lossy(*modifier.key).into_owned(),
                            index: modifier.key.span.start.byte as usize,
                        },
                    });
        }
        Ok(this)
    }
}
impl OffsetSecond {
    /// Parse the component from the AST, given its modifiers.
    #[inline]
    fn with_modifiers(modifiers: &[ast::Modifier<'_>], _component_span: Span)
        -> Result<Self, Error> {
        #[allow(unused_mut)]
        let mut this = Self { padding: None };
        for modifier in modifiers {

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("padding".as_bytes()) {
                this.padding =
                    <Padding>::from_modifier_value(&modifier.value)?;
                continue;
            }
            return Err(Error {
                        _inner: unused(modifier.key.span.error("invalid modifier key")),
                        public: crate::error::InvalidFormatDescription::InvalidModifier {
                            value: String::from_utf8_lossy(*modifier.key).into_owned(),
                            index: modifier.key.span.start.byte as usize,
                        },
                    });
        }
        Ok(this)
    }
}
impl Ordinal {
    /// Parse the component from the AST, given its modifiers.
    #[inline]
    fn with_modifiers(modifiers: &[ast::Modifier<'_>], _component_span: Span)
        -> Result<Self, Error> {
        #[allow(unused_mut)]
        let mut this = Self { padding: None };
        for modifier in modifiers {

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("padding".as_bytes()) {
                this.padding =
                    <Padding>::from_modifier_value(&modifier.value)?;
                continue;
            }
            return Err(Error {
                        _inner: unused(modifier.key.span.error("invalid modifier key")),
                        public: crate::error::InvalidFormatDescription::InvalidModifier {
                            value: String::from_utf8_lossy(*modifier.key).into_owned(),
                            index: modifier.key.span.start.byte as usize,
                        },
                    });
        }
        Ok(this)
    }
}
impl Period {
    /// Parse the component from the AST, given its modifiers.
    #[inline]
    fn with_modifiers(modifiers: &[ast::Modifier<'_>], _component_span: Span)
        -> Result<Self, Error> {
        #[allow(unused_mut)]
        let mut this = Self { case: None, case_sensitive: None };
        for modifier in modifiers {

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("case".as_bytes()) {
                this.case =
                    <PeriodCase>::from_modifier_value(&modifier.value)?;
                continue;
            }

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("case_sensitive".as_bytes())
                {
                this.case_sensitive =
                    <PeriodCaseSensitive>::from_modifier_value(&modifier.value)?;
                continue;
            }
            return Err(Error {
                        _inner: unused(modifier.key.span.error("invalid modifier key")),
                        public: crate::error::InvalidFormatDescription::InvalidModifier {
                            value: String::from_utf8_lossy(*modifier.key).into_owned(),
                            index: modifier.key.span.start.byte as usize,
                        },
                    });
        }
        Ok(this)
    }
}
impl Second {
    /// Parse the component from the AST, given its modifiers.
    #[inline]
    fn with_modifiers(modifiers: &[ast::Modifier<'_>], _component_span: Span)
        -> Result<Self, Error> {
        #[allow(unused_mut)]
        let mut this = Self { padding: None };
        for modifier in modifiers {

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("padding".as_bytes()) {
                this.padding =
                    <Padding>::from_modifier_value(&modifier.value)?;
                continue;
            }
            return Err(Error {
                        _inner: unused(modifier.key.span.error("invalid modifier key")),
                        public: crate::error::InvalidFormatDescription::InvalidModifier {
                            value: String::from_utf8_lossy(*modifier.key).into_owned(),
                            index: modifier.key.span.start.byte as usize,
                        },
                    });
        }
        Ok(this)
    }
}
impl Subsecond {
    /// Parse the component from the AST, given its modifiers.
    #[inline]
    fn with_modifiers(modifiers: &[ast::Modifier<'_>], _component_span: Span)
        -> Result<Self, Error> {
        #[allow(unused_mut)]
        let mut this = Self { digits: None };
        for modifier in modifiers {

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("digits".as_bytes()) {
                this.digits =
                    <SubsecondDigits>::from_modifier_value(&modifier.value)?;
                continue;
            }
            return Err(Error {
                        _inner: unused(modifier.key.span.error("invalid modifier key")),
                        public: crate::error::InvalidFormatDescription::InvalidModifier {
                            value: String::from_utf8_lossy(*modifier.key).into_owned(),
                            index: modifier.key.span.start.byte as usize,
                        },
                    });
        }
        Ok(this)
    }
}
impl UnixTimestamp {
    /// Parse the component from the AST, given its modifiers.
    #[inline]
    fn with_modifiers(modifiers: &[ast::Modifier<'_>], _component_span: Span)
        -> Result<Self, Error> {
        #[allow(unused_mut)]
        let mut this = Self { precision: None, sign_behavior: None };
        for modifier in modifiers {

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("precision".as_bytes()) {
                this.precision =
                    <UnixTimestampPrecision>::from_modifier_value(&modifier.value)?;
                continue;
            }

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("sign".as_bytes()) {
                this.sign_behavior =
                    <SignBehavior>::from_modifier_value(&modifier.value)?;
                continue;
            }
            return Err(Error {
                        _inner: unused(modifier.key.span.error("invalid modifier key")),
                        public: crate::error::InvalidFormatDescription::InvalidModifier {
                            value: String::from_utf8_lossy(*modifier.key).into_owned(),
                            index: modifier.key.span.start.byte as usize,
                        },
                    });
        }
        Ok(this)
    }
}
impl Weekday {
    /// Parse the component from the AST, given its modifiers.
    #[inline]
    fn with_modifiers(modifiers: &[ast::Modifier<'_>], _component_span: Span)
        -> Result<Self, Error> {
        #[allow(unused_mut)]
        let mut this =
            Self { repr: None, one_indexed: None, case_sensitive: None };
        for modifier in modifiers {

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("repr".as_bytes()) {
                this.repr =
                    <WeekdayRepr>::from_modifier_value(&modifier.value)?;
                continue;
            }

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("one_indexed".as_bytes()) {
                this.one_indexed =
                    <WeekdayOneIndexed>::from_modifier_value(&modifier.value)?;
                continue;
            }

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("case_sensitive".as_bytes())
                {
                this.case_sensitive =
                    <WeekdayCaseSensitive>::from_modifier_value(&modifier.value)?;
                continue;
            }
            return Err(Error {
                        _inner: unused(modifier.key.span.error("invalid modifier key")),
                        public: crate::error::InvalidFormatDescription::InvalidModifier {
                            value: String::from_utf8_lossy(*modifier.key).into_owned(),
                            index: modifier.key.span.start.byte as usize,
                        },
                    });
        }
        Ok(this)
    }
}
impl WeekNumber {
    /// Parse the component from the AST, given its modifiers.
    #[inline]
    fn with_modifiers(modifiers: &[ast::Modifier<'_>], _component_span: Span)
        -> Result<Self, Error> {
        #[allow(unused_mut)]
        let mut this = Self { padding: None, repr: None };
        for modifier in modifiers {

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("padding".as_bytes()) {
                this.padding =
                    <Padding>::from_modifier_value(&modifier.value)?;
                continue;
            }

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("repr".as_bytes()) {
                this.repr =
                    <WeekNumberRepr>::from_modifier_value(&modifier.value)?;
                continue;
            }
            return Err(Error {
                        _inner: unused(modifier.key.span.error("invalid modifier key")),
                        public: crate::error::InvalidFormatDescription::InvalidModifier {
                            value: String::from_utf8_lossy(*modifier.key).into_owned(),
                            index: modifier.key.span.start.byte as usize,
                        },
                    });
        }
        Ok(this)
    }
}
impl Year {
    /// Parse the component from the AST, given its modifiers.
    #[inline]
    fn with_modifiers(modifiers: &[ast::Modifier<'_>], _component_span: Span)
        -> Result<Self, Error> {
        #[allow(unused_mut)]
        let mut this =
            Self {
                padding: None,
                repr: None,
                range: None,
                base: None,
                sign_behavior: None,
            };
        for modifier in modifiers {

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("padding".as_bytes()) {
                this.padding =
                    <Padding>::from_modifier_value(&modifier.value)?;
                continue;
            }

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("repr".as_bytes()) {
                this.repr = <YearRepr>::from_modifier_value(&modifier.value)?;
                continue;
            }

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("range".as_bytes()) {
                this.range =
                    <YearRange>::from_modifier_value(&modifier.value)?;
                continue;
            }

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("base".as_bytes()) {
                this.base = <YearBase>::from_modifier_value(&modifier.value)?;
                continue;
            }

            #[expect(clippy :: string_lit_as_bytes)]
            if modifier.key.eq_ignore_ascii_case("sign".as_bytes()) {
                this.sign_behavior =
                    <SignBehavior>::from_modifier_value(&modifier.value)?;
                continue;
            }
            return Err(Error {
                        _inner: unused(modifier.key.span.error("invalid modifier key")),
                        public: crate::error::InvalidFormatDescription::InvalidModifier {
                            value: String::from_utf8_lossy(*modifier.key).into_owned(),
                            index: modifier.key.span.start.byte as usize,
                        },
                    });
        }
        Ok(this)
    }
}
impl From<Component> for crate::format_description::Component {
    #[inline]
    fn from(component: Component) -> Self {
        match component {
            Component::Day(Day { padding }) => {
                crate::format_description::component::Component::Day(crate::format_description::modifier::Day {
                        padding: padding.unwrap_or_default().into(),
                    })
            }
            Component::End(End { trailing_input }) => {
                crate::format_description::component::Component::End(crate::format_description::modifier::End {
                        trailing_input: trailing_input.unwrap_or_default().into(),
                    })
            }
            Component::Hour(Hour { padding, base }) => {
                crate::format_description::component::Component::Hour(crate::format_description::modifier::Hour {
                        padding: padding.unwrap_or_default().into(),
                        is_12_hour_clock: base.unwrap_or_default().into(),
                    })
            }
            Component::Ignore(Ignore { count }) => {
                crate::format_description::component::Component::Ignore(crate::format_description::modifier::Ignore {
                        count: match count {
                            Some(value) => value.into(),
                            None => {
                                ::core::panicking::panic_fmt(format_args!("internal error: required modifier was not set"));
                            }
                        },
                    })
            }
            Component::Minute(Minute { padding }) => {
                crate::format_description::component::Component::Minute(crate::format_description::modifier::Minute {
                        padding: padding.unwrap_or_default().into(),
                    })
            }
            Component::Month(Month { padding, repr, case_sensitive }) => {
                crate::format_description::component::Component::Month(crate::format_description::modifier::Month {
                        padding: padding.unwrap_or_default().into(),
                        repr: repr.unwrap_or_default().into(),
                        case_sensitive: case_sensitive.unwrap_or_default().into(),
                    })
            }
            Component::OffsetHour(OffsetHour { sign_behavior, padding }) => {
                crate::format_description::component::Component::OffsetHour(crate::format_description::modifier::OffsetHour {
                        sign_is_mandatory: sign_behavior.unwrap_or_default().into(),
                        padding: padding.unwrap_or_default().into(),
                    })
            }
            Component::OffsetMinute(OffsetMinute { padding }) => {
                crate::format_description::component::Component::OffsetMinute(crate::format_description::modifier::OffsetMinute {
                        padding: padding.unwrap_or_default().into(),
                    })
            }
            Component::OffsetSecond(OffsetSecond { padding }) => {
                crate::format_description::component::Component::OffsetSecond(crate::format_description::modifier::OffsetSecond {
                        padding: padding.unwrap_or_default().into(),
                    })
            }
            Component::Ordinal(Ordinal { padding }) => {
                crate::format_description::component::Component::Ordinal(crate::format_description::modifier::Ordinal {
                        padding: padding.unwrap_or_default().into(),
                    })
            }
            Component::Period(Period { case, case_sensitive }) => {
                crate::format_description::component::Component::Period(crate::format_description::modifier::Period {
                        is_uppercase: case.unwrap_or_default().into(),
                        case_sensitive: case_sensitive.unwrap_or_default().into(),
                    })
            }
            Component::Second(Second { padding }) => {
                crate::format_description::component::Component::Second(crate::format_description::modifier::Second {
                        padding: padding.unwrap_or_default().into(),
                    })
            }
            Component::Subsecond(Subsecond { digits }) => {
                crate::format_description::component::Component::Subsecond(crate::format_description::modifier::Subsecond {
                        digits: digits.unwrap_or_default().into(),
                    })
            }
            Component::UnixTimestamp(UnixTimestamp { precision, sign_behavior
                }) => {
                crate::format_description::component::Component::UnixTimestamp(crate::format_description::modifier::UnixTimestamp {
                        precision: precision.unwrap_or_default().into(),
                        sign_is_mandatory: sign_behavior.unwrap_or_default().into(),
                    })
            }
            Component::Weekday(Weekday { repr, one_indexed, case_sensitive })
                => {
                crate::format_description::component::Component::Weekday(crate::format_description::modifier::Weekday {
                        repr: repr.unwrap_or_default().into(),
                        one_indexed: one_indexed.unwrap_or_default().into(),
                        case_sensitive: case_sensitive.unwrap_or_default().into(),
                    })
            }
            Component::WeekNumber(WeekNumber { padding, repr }) => {
                crate::format_description::component::Component::WeekNumber(crate::format_description::modifier::WeekNumber {
                        padding: padding.unwrap_or_default().into(),
                        repr: repr.unwrap_or_default().into(),
                    })
            }
            Component::Year(Year { padding, repr, range, base, sign_behavior
                }) => {
                crate::format_description::component::Component::Year(crate::format_description::modifier::Year {
                        padding: padding.unwrap_or_default().into(),
                        repr: repr.unwrap_or_default().into(),
                        range: range.unwrap_or_default().into(),
                        iso_week_based: base.unwrap_or_default().into(),
                        sign_is_mandatory: sign_behavior.unwrap_or_default().into(),
                    })
            }
        }
    }
}
/// Parse a component from the AST, given its name and modifiers.
#[inline]
fn component_from_ast(name: &Spanned<&[u8]>, modifiers: &[ast::Modifier<'_>])
    -> Result<Component, Error> {

    #[expect(clippy :: string_lit_as_bytes)]
    if name.eq_ignore_ascii_case("day".as_bytes()) {
        return Ok(Component::Day(Day::with_modifiers(&modifiers,
                            name.span)?));
    }

    #[expect(clippy :: string_lit_as_bytes)]
    if name.eq_ignore_ascii_case("end".as_bytes()) {
        return Ok(Component::End(End::with_modifiers(&modifiers,
                            name.span)?));
    }

    #[expect(clippy :: string_lit_as_bytes)]
    if name.eq_ignore_ascii_case("hour".as_bytes()) {
        return Ok(Component::Hour(Hour::with_modifiers(&modifiers,
                            name.span)?));
    }

    #[expect(clippy :: string_lit_as_bytes)]
    if name.eq_ignore_ascii_case("ignore".as_bytes()) {
        return Ok(Component::Ignore(Ignore::with_modifiers(&modifiers,
                            name.span)?));
    }

    #[expect(clippy :: string_lit_as_bytes)]
    if name.eq_ignore_ascii_case("minute".as_bytes()) {
        return Ok(Component::Minute(Minute::with_modifiers(&modifiers,
                            name.span)?));
    }

    #[expect(clippy :: string_lit_as_bytes)]
    if name.eq_ignore_ascii_case("month".as_bytes()) {
        return Ok(Component::Month(Month::with_modifiers(&modifiers,
                            name.span)?));
    }

    #[expect(clippy :: string_lit_as_bytes)]
    if name.eq_ignore_ascii_case("offset_hour".as_bytes()) {
        return Ok(Component::OffsetHour(OffsetHour::with_modifiers(&modifiers,
                            name.span)?));
    }

    #[expect(clippy :: string_lit_as_bytes)]
    if name.eq_ignore_ascii_case("offset_minute".as_bytes()) {
        return Ok(Component::OffsetMinute(OffsetMinute::with_modifiers(&modifiers,
                            name.span)?));
    }

    #[expect(clippy :: string_lit_as_bytes)]
    if name.eq_ignore_ascii_case("offset_second".as_bytes()) {
        return Ok(Component::OffsetSecond(OffsetSecond::with_modifiers(&modifiers,
                            name.span)?));
    }

    #[expect(clippy :: string_lit_as_bytes)]
    if name.eq_ignore_ascii_case("ordinal".as_bytes()) {
        return Ok(Component::Ordinal(Ordinal::with_modifiers(&modifiers,
                            name.span)?));
    }

    #[expect(clippy :: string_lit_as_bytes)]
    if name.eq_ignore_ascii_case("period".as_bytes()) {
        return Ok(Component::Period(Period::with_modifiers(&modifiers,
                            name.span)?));
    }

    #[expect(clippy :: string_lit_as_bytes)]
    if name.eq_ignore_ascii_case("second".as_bytes()) {
        return Ok(Component::Second(Second::with_modifiers(&modifiers,
                            name.span)?));
    }

    #[expect(clippy :: string_lit_as_bytes)]
    if name.eq_ignore_ascii_case("subsecond".as_bytes()) {
        return Ok(Component::Subsecond(Subsecond::with_modifiers(&modifiers,
                            name.span)?));
    }

    #[expect(clippy :: string_lit_as_bytes)]
    if name.eq_ignore_ascii_case("unix_timestamp".as_bytes()) {
        return Ok(Component::UnixTimestamp(UnixTimestamp::with_modifiers(&modifiers,
                            name.span)?));
    }

    #[expect(clippy :: string_lit_as_bytes)]
    if name.eq_ignore_ascii_case("weekday".as_bytes()) {
        return Ok(Component::Weekday(Weekday::with_modifiers(&modifiers,
                            name.span)?));
    }

    #[expect(clippy :: string_lit_as_bytes)]
    if name.eq_ignore_ascii_case("week_number".as_bytes()) {
        return Ok(Component::WeekNumber(WeekNumber::with_modifiers(&modifiers,
                            name.span)?));
    }

    #[expect(clippy :: string_lit_as_bytes)]
    if name.eq_ignore_ascii_case("year".as_bytes()) {
        return Ok(Component::Year(Year::with_modifiers(&modifiers,
                            name.span)?));
    }
    Err(Error {
            _inner: unused(name.span.error("invalid component")),
            public: crate::error::InvalidFormatDescription::InvalidComponentName {
                name: String::from_utf8_lossy(name).into_owned(),
                index: name.span.start.byte as usize,
            },
        })
}component_definition! {
284    pub(super) enum Component {
285        Day = "day" {
286            padding = "padding": Option<Padding> => padding,
287        },
288        End = "end" {
289            trailing_input = "trailing_input": Option<TrailingInput> => trailing_input,
290        },
291        Hour = "hour" {
292            padding = "padding": Option<Padding> => padding,
293            base = "repr": Option<HourBase> => is_12_hour_clock,
294        },
295        Ignore = "ignore" {
296            #[required]
297            count = "count": Option<#[from_str] NonZero<u16>> => count,
298        },
299        Minute = "minute" {
300            padding = "padding": Option<Padding> => padding,
301        },
302        Month = "month" {
303            padding = "padding": Option<Padding> => padding,
304            repr = "repr": Option<MonthRepr> => repr,
305            case_sensitive = "case_sensitive": Option<MonthCaseSensitive> => case_sensitive,
306        },
307        OffsetHour = "offset_hour" {
308            sign_behavior = "sign": Option<SignBehavior> => sign_is_mandatory,
309            padding = "padding": Option<Padding> => padding,
310        },
311        OffsetMinute = "offset_minute" {
312            padding = "padding": Option<Padding> => padding,
313        },
314        OffsetSecond = "offset_second" {
315            padding = "padding": Option<Padding> => padding,
316        },
317        Ordinal = "ordinal" {
318            padding = "padding": Option<Padding> => padding,
319        },
320        Period = "period" {
321            case = "case": Option<PeriodCase> => is_uppercase,
322            case_sensitive = "case_sensitive": Option<PeriodCaseSensitive> => case_sensitive,
323        },
324        Second = "second" {
325            padding = "padding": Option<Padding> => padding,
326        },
327        Subsecond = "subsecond" {
328            digits = "digits": Option<SubsecondDigits> => digits,
329        },
330        UnixTimestamp = "unix_timestamp" {
331            precision = "precision": Option<UnixTimestampPrecision> => precision,
332            sign_behavior = "sign": Option<SignBehavior> => sign_is_mandatory,
333        },
334        Weekday = "weekday" {
335            repr = "repr": Option<WeekdayRepr> => repr,
336            one_indexed = "one_indexed": Option<WeekdayOneIndexed> => one_indexed,
337            case_sensitive = "case_sensitive": Option<WeekdayCaseSensitive> => case_sensitive,
338        },
339        WeekNumber = "week_number" {
340            padding = "padding": Option<Padding> => padding,
341            repr = "repr": Option<WeekNumberRepr> => repr,
342        },
343        Year = "year" {
344            padding = "padding": Option<Padding> => padding,
345            repr = "repr": Option<YearRepr> => repr,
346            range = "range": Option<YearRange> => range,
347            base = "base": Option<YearBase> => iso_week_based,
348            sign_behavior = "sign": Option<SignBehavior> => sign_is_mandatory,
349        },
350    }
351}
352
353/// Get the target type for a given enum.
354macro_rules! target_ty {
355    ($name:ident $type:ty) => {
356        $type
357    };
358    ($name:ident) => {
359        $crate::format_description::modifier::$name
360    };
361}
362
363/// Get the target value for a given enum.
364macro_rules! target_value {
365    ($name:ident $variant:ident $value:expr) => {
366        $value
367    };
368    ($name:ident $variant:ident) => {
369        $crate::format_description::modifier::$name::$variant
370    };
371}
372
373/// Declare the various modifiers.
374///
375/// For the general case, ordinary syntax can be used. Note that you _must_ declare a default
376/// variant. The only significant change is that the string representation of the variant must be
377/// provided after the variant name. For example, `Numerical = b"numerical"` declares a variant
378/// named `Numerical` with the string representation `b"numerical"`. This is the value that will be
379/// used when parsing the modifier. The value is not case sensitive.
380///
381/// If the type in the public API does not have the same name as the type in the internal
382/// representation, then the former must be specified in parenthesis after the internal name. For
383/// example, `HourBase(bool)` has an internal name "HourBase", but is represented as a boolean in
384/// the public API.
385///
386/// By default, the internal variant name is assumed to be the same as the public variant name. If
387/// this is not the case, the qualified path to the variant must be specified in parenthesis after
388/// the internal variant name. For example, `Twelve(true)` has an internal variant name "Twelve",
389/// but is represented as `true` in the public API.
390macro_rules! modifier {
391    ($(
392        enum $name:ident $(($target_ty:ty))? {
393            $(
394                $(#[$attr:meta])?
395                $variant:ident $(($target_value:expr))? = $parse_variant:literal
396            ),* $(,)?
397        }
398    )+) => {$(
399        #[derive(Default)]
400        enum $name {
401            $($(#[$attr])? $variant),*
402        }
403
404        impl $name {
405            /// Parse the modifier from its string representation.
406            #[inline]
407            fn from_modifier_value(value: &Spanned<&[u8]>) -> Result<Option<Self>, Error> {
408                $(if value.eq_ignore_ascii_case($parse_variant) {
409                    return Ok(Some(Self::$variant));
410                })*
411                Err(Error {
412                    _inner: unused(value.span.error("invalid modifier value")),
413                    public: crate::error::InvalidFormatDescription::InvalidModifier {
414                        value: String::from_utf8_lossy(value).into_owned(),
415                        index: value.span.start.byte as usize,
416                    },
417                })
418            }
419        }
420
421        impl From<$name> for target_ty!($name $($target_ty)?) {
422            #[inline]
423            fn from(modifier: $name) -> Self {
424                match modifier {
425                    $($name::$variant => target_value!($name $variant $($target_value)?)),*
426                }
427            }
428        }
429    )+};
430}
431
432// Keep in alphabetical order.
433#[automatically_derived]
impl ::core::default::Default for YearRange {
    #[inline]
    fn default() -> YearRange { Self::Extended }
}
impl YearRange {
    /// Parse the modifier from its string representation.
    #[inline]
    fn from_modifier_value(value: &Spanned<&[u8]>)
        -> Result<Option<Self>, Error> {
        if value.eq_ignore_ascii_case(b"standard") {
            return Ok(Some(Self::Standard));
        }
        if value.eq_ignore_ascii_case(b"extended") {
            return Ok(Some(Self::Extended));
        }
        Err(Error {
                _inner: unused(value.span.error("invalid modifier value")),
                public: crate::error::InvalidFormatDescription::InvalidModifier {
                    value: String::from_utf8_lossy(value).into_owned(),
                    index: value.span.start.byte as usize,
                },
            })
    }
}
impl From<YearRange> for crate::format_description::modifier::YearRange {
    #[inline]
    fn from(modifier: YearRange) -> Self {
        match modifier {
            YearRange::Standard =>
                crate::format_description::modifier::YearRange::Standard,
            YearRange::Extended =>
                crate::format_description::modifier::YearRange::Extended,
        }
    }
}modifier! {
434    enum HourBase(bool) {
435        Twelve(true) = b"12",
436        #[default]
437        TwentyFour(false) = b"24",
438    }
439
440    enum MonthCaseSensitive(bool) {
441        False(false) = b"false",
442        #[default]
443        True(true) = b"true",
444    }
445
446    enum MonthRepr {
447        #[default]
448        Numerical = b"numerical",
449        Long = b"long",
450        Short = b"short",
451    }
452
453    enum Padding {
454        Space = b"space",
455        #[default]
456        Zero = b"zero",
457        None = b"none",
458    }
459
460    enum PeriodCase(bool) {
461        Lower(false) = b"lower",
462        #[default]
463        Upper(true) = b"upper",
464    }
465
466    enum PeriodCaseSensitive(bool) {
467        False(false) = b"false",
468        #[default]
469        True(true) = b"true",
470    }
471
472    enum SignBehavior(bool) {
473        #[default]
474        Automatic(false) = b"automatic",
475        Mandatory(true) = b"mandatory",
476    }
477
478    enum SubsecondDigits {
479        One = b"1",
480        Two = b"2",
481        Three = b"3",
482        Four = b"4",
483        Five = b"5",
484        Six = b"6",
485        Seven = b"7",
486        Eight = b"8",
487        Nine = b"9",
488        #[default]
489        OneOrMore = b"1+",
490    }
491
492    enum TrailingInput {
493        #[default]
494        Prohibit = b"prohibit",
495        Discard = b"discard",
496    }
497
498    enum UnixTimestampPrecision {
499        #[default]
500        Second = b"second",
501        Millisecond = b"millisecond",
502        Microsecond = b"microsecond",
503        Nanosecond = b"nanosecond",
504    }
505
506    enum WeekNumberRepr {
507        #[default]
508        Iso = b"iso",
509        Sunday = b"sunday",
510        Monday = b"monday",
511    }
512
513    enum WeekdayCaseSensitive(bool) {
514        False(false) = b"false",
515        #[default]
516        True(true) = b"true",
517    }
518
519    enum WeekdayOneIndexed(bool) {
520        False(false) = b"false",
521        #[default]
522        True(true) = b"true",
523    }
524
525    enum WeekdayRepr {
526        Short = b"short",
527        #[default]
528        Long = b"long",
529        Sunday = b"sunday",
530        Monday = b"monday",
531    }
532
533    enum YearBase(bool) {
534        #[default]
535        Calendar(false) = b"calendar",
536        IsoWeek(true) = b"iso_week",
537    }
538
539    enum YearRepr {
540        #[default]
541        Full = b"full",
542        Century = b"century",
543        LastTwo = b"last_two",
544    }
545
546    enum YearRange {
547        Standard = b"standard",
548        #[default]
549        Extended = b"extended",
550    }
551}
552
553/// Parse a modifier value using `FromStr`. Requires the modifier value to be valid UTF-8.
554#[inline]
555fn parse_from_modifier_value<T>(value: &Spanned<&[u8]>) -> Result<Option<T>, Error>
556where
557    T: FromStr,
558{
559    str::from_utf8(value)
560        .ok()
561        .and_then(|val| val.parse::<T>().ok())
562        .map(|val| Some(val))
563        .ok_or_else(|| Error {
564            _inner: unused(value.span.error("invalid modifier value")),
565            public: crate::error::InvalidFormatDescription::InvalidModifier {
566                value: String::from_utf8_lossy(value).into_owned(),
567                index: value.span.start.byte as usize,
568            },
569        })
570}