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