1mod assert;
4mod check;
5mod utils;
6mod week;
7mod write;
8
9use core::ffi::c_int;
10use core::fmt;
11use core::num::IntErrorKind;
12use core::str;
13
14use crate::Error;
15use assert::{assert_sorted, assert_sorted_elem_0, assert_to_ascii_uppercase};
16use check::CheckedTime;
17use utils::{Cursor, SizeLimiter};
18use week::{iso_8601_year_and_week_number, week_number, WeekStart};
19use write::Write;
20
21pub(crate) use write::FmtWrite;
22#[cfg(feature = "std")]
23pub(crate) use write::IoWrite;
24
25const DAYS: [&str; 7] = [
27 "Sunday",
28 "Monday",
29 "Tuesday",
30 "Wednesday",
31 "Thursday",
32 "Friday",
33 "Saturday",
34];
35
36const DAYS_UPPER: [&str; 7] = [
38 "SUNDAY",
39 "MONDAY",
40 "TUESDAY",
41 "WEDNESDAY",
42 "THURSDAY",
43 "FRIDAY",
44 "SATURDAY",
45];
46
47const MONTHS: [&str; 12] = [
49 "January",
50 "February",
51 "March",
52 "April",
53 "May",
54 "June",
55 "July",
56 "August",
57 "September",
58 "October",
59 "November",
60 "December",
61];
62
63const MONTHS_UPPER: [&str; 12] = [
65 "JANUARY",
66 "FEBRUARY",
67 "MARCH",
68 "APRIL",
69 "MAY",
70 "JUNE",
71 "JULY",
72 "AUGUST",
73 "SEPTEMBER",
74 "OCTOBER",
75 "NOVEMBER",
76 "DECEMBER",
77];
78
79const _: () = {
81 assert_to_ascii_uppercase(&DAYS, &DAYS_UPPER);
82 assert_to_ascii_uppercase(&MONTHS, &MONTHS_UPPER);
83};
84
85#[repr(u8)]
87#[derive(Debug, Copy, Clone, Eq, PartialEq)]
88enum Flag {
89 LeftPadding = 1 << 0,
91 ChangeCase = 1 << 1,
93 UpperCase = 1 << 2,
95}
96
97#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
99struct Flags(u8);
100
101impl Flags {
102 #[must_use]
104 fn contains(self, flag: Flag) -> bool {
105 let flag = flag as u8;
106 (self.0 & flag) == flag
107 }
108
109 fn set(&mut self, flag: Flag) {
111 self.0 |= flag as u8;
112 }
113
114 #[must_use]
116 fn has_change_or_upper_case(self) -> bool {
117 let flags = Flag::ChangeCase as u8 | Flag::UpperCase as u8;
118 self.0 & flags != 0
119 }
120}
121
122#[derive(Debug, Copy, Clone, Eq, PartialEq)]
124enum Padding {
125 Left,
127 Spaces,
129 Zeros,
131}
132
133#[derive(Debug, Copy, Clone, Eq, PartialEq)]
135enum Spec {
136 Year4Digits,
139 YearDiv100,
142 YearRem100,
145 Month,
147 MonthName,
149 MonthNameAbbr,
152 MonthDayZero,
154 MonthDaySpace,
156 YearDay,
158 Hour24hZero,
161 Hour24hSpace,
164 Hour12hZero,
167 Hour12hSpace,
170 MeridianLower,
172 MeridianUpper,
174 Minute,
176 Second,
178 MilliSecond,
181 FractionalSecond,
184 TimeZoneOffsetHourMinute,
187 TimeZoneOffsetHourMinuteColon,
190 TimeZoneOffsetHourMinuteSecondColon,
193 TimeZoneOffsetColonMinimal,
196 TimeZoneName,
198 WeekDayName,
200 WeekDayNameAbbr,
203 WeekDayFrom1,
205 WeekDayFrom0,
207 YearIso8601,
209 YearIso8601Rem100,
211 WeekNumberIso8601,
213 WeekNumberFromSunday,
216 WeekNumberFromMonday,
219 SecondsSinceEpoch,
222 Newline,
224 Tabulation,
226 Percent,
228 CombinationDateTime,
230 CombinationDate,
232 CombinationIso8601,
234 CombinationVmsDate,
236 CombinationTime12h,
238 CombinationHourMinute24h,
240 CombinationTime24h,
242}
243
244#[derive(Debug)]
246struct UtcOffset {
247 hour: f64,
249 minute: u32,
251 second: u32,
253}
254
255impl UtcOffset {
256 fn new(hour: f64, minute: u32, second: u32) -> Self {
258 Self {
259 hour,
260 minute,
261 second,
262 }
263 }
264}
265
266#[derive(Debug)]
268struct Piece {
269 width: Option<usize>,
271 padding: Padding,
273 flags: Flags,
275 spec: Spec,
277}
278
279impl Piece {
280 fn new(width: Option<usize>, padding: Padding, flags: Flags, spec: Spec) -> Self {
282 Self {
283 width,
284 padding,
285 flags,
286 spec,
287 }
288 }
289
290 fn format_num_zeros(
292 &self,
293 f: &mut SizeLimiter<'_>,
294 value: impl fmt::Display,
295 default_width: usize,
296 ) -> Result<(), Error> {
297 if self.flags.contains(Flag::LeftPadding) {
298 write!(f, "{value}")
299 } else if self.padding == Padding::Spaces {
300 let width = self.pad_width(f, b' ', default_width)?;
301 write!(f, "{value: >width$}")
302 } else {
303 let width = self.pad_width(f, b'0', default_width)?;
304 write!(f, "{value:0width$}")
305 }
306 }
307
308 fn format_num_spaces(
310 &self,
311 f: &mut SizeLimiter<'_>,
312 value: impl fmt::Display,
313 default_width: usize,
314 ) -> Result<(), Error> {
315 if self.flags.contains(Flag::LeftPadding) {
316 write!(f, "{value}")
317 } else if self.padding == Padding::Zeros {
318 let width = self.pad_width(f, b'0', default_width)?;
319 write!(f, "{value:0width$}")
320 } else {
321 let width = self.pad_width(f, b' ', default_width)?;
322 write!(f, "{value: >width$}")
323 }
324 }
325
326 fn pad_width(&self, f: &mut SizeLimiter<'_>, pad: u8, default: usize) -> Result<usize, Error> {
330 let width = self.width.unwrap_or(default);
331 f.pad(pad, width.saturating_sub(u16::MAX.into()))?;
332 Ok(width.min(u16::MAX.into()))
333 }
334
335 fn format_nanoseconds(
337 &self,
338 f: &mut SizeLimiter<'_>,
339 nanoseconds: u32,
340 default_width: usize,
341 ) -> Result<(), Error> {
342 let width = self.width.unwrap_or(default_width);
343
344 match u32::try_from(width) {
345 Ok(w) if w <= 9 => {
346 let value = nanoseconds / 10_u32.pow(9 - w);
347 write!(f, "{value:0width$}")
348 }
349 Ok(_) | Err(_) => {
350 write!(f, "{nanoseconds:09}")?;
351 f.pad(b'0', width - 9)
352 }
353 }
354 }
355
356 fn format_string(&self, f: &mut SizeLimiter<'_>, s: &str) -> Result<(), Error> {
358 if !self.flags.contains(Flag::LeftPadding) {
359 self.write_padding(f, s.len())?;
360 }
361
362 write!(f, "{s}")
363 }
364
365 fn write_padding(&self, f: &mut SizeLimiter<'_>, min_width: usize) -> Result<(), Error> {
367 let Some(width) = self.width else {
368 return Ok(());
369 };
370
371 let n = width.saturating_sub(min_width);
372
373 let pad = match self.padding {
374 Padding::Zeros => b'0',
375 Padding::Left | Padding::Spaces => b' ',
376 };
377
378 f.pad(pad, n)?;
379
380 Ok(())
381 }
382
383 fn compute_offset_parts(&self, time: &impl CheckedTime) -> UtcOffset {
385 let utc_offset = time.utc_offset();
386 let utc_offset_abs = utc_offset.unsigned_abs();
387
388 let sign = if utc_offset < 0 || time.is_utc() && self.flags.contains(Flag::LeftPadding) {
390 -1.0
391 } else {
392 1.0
393 };
394
395 let hour = sign * f64::from(utc_offset_abs / 3600);
397 let minute = (utc_offset_abs / 60) % 60;
398 let second = utc_offset_abs % 60;
399
400 UtcOffset::new(hour, minute, second)
401 }
402
403 fn write_hour_sign(f: &mut SizeLimiter<'_>, hour: f64) -> Result<(), Error> {
405 if hour.is_sign_negative() {
406 write!(f, "-")?;
407 } else {
408 write!(f, "+")?;
409 }
410
411 Ok(())
412 }
413
414 fn write_offset_hour(&self, f: &mut SizeLimiter<'_>, hour: f64, w: usize) -> Result<(), Error> {
416 let mut pad = self.width.unwrap_or(0).saturating_sub(w);
417
418 if hour < 10.0 {
419 pad += 1;
420 }
421
422 if self.padding == Padding::Spaces {
423 f.pad(b' ', pad)?;
424 Self::write_hour_sign(f, hour)?;
425 } else {
426 Self::write_hour_sign(f, hour)?;
427 f.pad(b'0', pad)?;
428 }
429
430 write!(f, "{:.0}", hour.abs())
431 }
432
433 fn write_offset_hh(
435 &self,
436 f: &mut SizeLimiter<'_>,
437 utc_offset: &UtcOffset,
438 ) -> Result<(), Error> {
439 self.write_offset_hour(f, utc_offset.hour, "+hh".len())
440 }
441
442 fn write_offset_hhmm(
444 &self,
445 f: &mut SizeLimiter<'_>,
446 utc_offset: &UtcOffset,
447 ) -> Result<(), Error> {
448 let UtcOffset { hour, minute, .. } = *utc_offset;
449
450 self.write_offset_hour(f, hour, "+hhmm".len())?;
451 write!(f, "{minute:02}")
452 }
453
454 fn write_offset_hh_mm(
456 &self,
457 f: &mut SizeLimiter<'_>,
458 utc_offset: &UtcOffset,
459 ) -> Result<(), Error> {
460 let UtcOffset { hour, minute, .. } = *utc_offset;
461
462 self.write_offset_hour(f, hour, "+hh:mm".len())?;
463 write!(f, ":{minute:02}")
464 }
465
466 fn write_offset_hh_mm_ss(
468 &self,
469 f: &mut SizeLimiter<'_>,
470 utc_offset: &UtcOffset,
471 ) -> Result<(), Error> {
472 let UtcOffset {
473 hour,
474 minute,
475 second,
476 } = *utc_offset;
477
478 self.write_offset_hour(f, hour, "+hh:mm:ss".len())?;
479 write!(f, ":{minute:02}:{second:02}")
480 }
481
482 #[expect(clippy::too_many_lines, reason = "must handle all enum cases")]
484 fn fmt(&self, f: &mut SizeLimiter<'_>, time: &impl CheckedTime) -> Result<(), Error> {
485 match self.spec {
486 Spec::Year4Digits => {
487 let year = time.year();
488 let default_width = if year < 0 { 5 } else { 4 };
489 self.format_num_zeros(f, year, default_width)
490 }
491 Spec::YearDiv100 => self.format_num_zeros(f, time.year().div_euclid(100), 2),
492 Spec::YearRem100 => self.format_num_zeros(f, time.year().rem_euclid(100), 2),
493 Spec::Month => self.format_num_zeros(f, time.month()?, 2),
494 #[expect(
495 clippy::indexing_slicing,
496 reason = "month is validated to be in range 1..=12"
497 )]
498 Spec::MonthName => {
499 let index = usize::from(time.month()? - 1);
500 if self.flags.has_change_or_upper_case() {
501 self.format_string(f, MONTHS_UPPER[index])
502 } else {
503 self.format_string(f, MONTHS[index])
504 }
505 }
506 #[expect(clippy::string_slice, reason = "month names values are all ASCII")]
507 #[expect(
508 clippy::indexing_slicing,
509 reason = "month is validated to be in range 1..=12"
510 )]
511 Spec::MonthNameAbbr => {
512 let index = usize::from(time.month()? - 1);
513 if self.flags.has_change_or_upper_case() {
514 self.format_string(f, &MONTHS_UPPER[index][..3])
515 } else {
516 self.format_string(f, &MONTHS[index][..3])
517 }
518 }
519 Spec::MonthDayZero => self.format_num_zeros(f, time.day()?, 2),
520 Spec::MonthDaySpace => self.format_num_spaces(f, time.day()?, 2),
521 Spec::YearDay => self.format_num_zeros(f, time.day_of_year()?, 3),
522 Spec::Hour24hZero => self.format_num_zeros(f, time.hour()?, 2),
523 Spec::Hour24hSpace => self.format_num_spaces(f, time.hour()?, 2),
524 Spec::Hour12hZero => {
525 let hour = time.hour()? % 12;
526 let hour = if hour == 0 { 12 } else { hour };
527 self.format_num_zeros(f, hour, 2)
528 }
529 Spec::Hour12hSpace => {
530 let hour = time.hour()? % 12;
531 let hour = if hour == 0 { 12 } else { hour };
532 self.format_num_spaces(f, hour, 2)
533 }
534 Spec::MeridianLower => {
535 let (am, pm) = if self.flags.has_change_or_upper_case() {
536 ("AM", "PM")
537 } else {
538 ("am", "pm")
539 };
540 let meridian = if time.hour()? < 12 { am } else { pm };
541 self.format_string(f, meridian)
542 }
543 Spec::MeridianUpper => {
544 let (am, pm) = if self.flags.contains(Flag::ChangeCase) {
545 ("am", "pm")
546 } else {
547 ("AM", "PM")
548 };
549 let meridian = if time.hour()? < 12 { am } else { pm };
550 self.format_string(f, meridian)
551 }
552 Spec::Minute => self.format_num_zeros(f, time.minute()?, 2),
553 Spec::Second => self.format_num_zeros(f, time.second()?, 2),
554 Spec::MilliSecond => self.format_nanoseconds(f, time.nanoseconds()?, 3),
555 Spec::FractionalSecond => self.format_nanoseconds(f, time.nanoseconds()?, 9),
556 Spec::TimeZoneOffsetHourMinute => {
557 self.write_offset_hhmm(f, &self.compute_offset_parts(time))
558 }
559 Spec::TimeZoneOffsetHourMinuteColon => {
560 self.write_offset_hh_mm(f, &self.compute_offset_parts(time))
561 }
562 Spec::TimeZoneOffsetHourMinuteSecondColon => {
563 self.write_offset_hh_mm_ss(f, &self.compute_offset_parts(time))
564 }
565 Spec::TimeZoneOffsetColonMinimal => {
566 let utc_offset = self.compute_offset_parts(time);
567
568 if utc_offset.second != 0 {
569 self.write_offset_hh_mm_ss(f, &utc_offset)
570 } else if utc_offset.minute != 0 {
571 self.write_offset_hh_mm(f, &utc_offset)
572 } else {
573 self.write_offset_hh(f, &utc_offset)
574 }
575 }
576 Spec::TimeZoneName => {
577 let tz_name = time.time_zone()?;
578 if !tz_name.is_empty() {
579 if !self.flags.contains(Flag::LeftPadding) {
580 self.write_padding(f, tz_name.len())?;
581 }
582
583 let convert: fn(&u8) -> u8 = if self.flags.contains(Flag::ChangeCase) {
585 u8::to_ascii_lowercase
586 } else if self.flags.contains(Flag::UpperCase) {
587 u8::to_ascii_uppercase
588 } else {
589 |&x| x
590 };
591
592 for x in tz_name.as_bytes() {
593 f.write_all(&[convert(x)])?;
594 }
595 }
596 Ok(())
597 }
598 #[expect(
599 clippy::indexing_slicing,
600 reason = "day of week is validated to be in range 0..=6"
601 )]
602 Spec::WeekDayName => {
603 let index = usize::from(time.day_of_week()?);
604 if self.flags.has_change_or_upper_case() {
605 self.format_string(f, DAYS_UPPER[index])
606 } else {
607 self.format_string(f, DAYS[index])
608 }
609 }
610 #[expect(clippy::string_slice, reason = "day names values are all ASCII")]
611 #[expect(
612 clippy::indexing_slicing,
613 reason = "day of week is validated to be in range 0..=6"
614 )]
615 Spec::WeekDayNameAbbr => {
616 let index = usize::from(time.day_of_week()?);
617 if self.flags.has_change_or_upper_case() {
618 self.format_string(f, &DAYS_UPPER[index][..3])
619 } else {
620 self.format_string(f, &DAYS[index][..3])
621 }
622 }
623 Spec::WeekDayFrom1 => {
624 let day_of_week = time.day_of_week()?;
625 let day_of_week = if day_of_week == 0 { 7 } else { day_of_week };
626 self.format_num_zeros(f, day_of_week, 1)
627 }
628 Spec::WeekDayFrom0 => self.format_num_zeros(f, time.day_of_week()?, 1),
629 Spec::YearIso8601 => {
630 let (iso_year, _) = iso_8601_year_and_week_number(
631 time.year().into(),
632 time.day_of_week()?.into(),
633 time.day_of_year()?.into(),
634 );
635 let default_width = if iso_year < 0 { 5 } else { 4 };
636 self.format_num_zeros(f, iso_year, default_width)
637 }
638 Spec::YearIso8601Rem100 => {
639 let (iso_year, _) = iso_8601_year_and_week_number(
640 time.year().into(),
641 time.day_of_week()?.into(),
642 time.day_of_year()?.into(),
643 );
644 self.format_num_zeros(f, iso_year.rem_euclid(100), 2)
645 }
646 Spec::WeekNumberIso8601 => {
647 let (_, iso_week_number) = iso_8601_year_and_week_number(
648 time.year().into(),
649 time.day_of_week()?.into(),
650 time.day_of_year()?.into(),
651 );
652 self.format_num_zeros(f, iso_week_number, 2)
653 }
654 Spec::WeekNumberFromSunday => {
655 let week_number = week_number(
656 time.day_of_week()?.into(),
657 time.day_of_year()?.into(),
658 WeekStart::Sunday,
659 );
660 self.format_num_zeros(f, week_number, 2)
661 }
662 Spec::WeekNumberFromMonday => {
663 let week_number = week_number(
664 time.day_of_week()?.into(),
665 time.day_of_year()?.into(),
666 WeekStart::Monday,
667 );
668 self.format_num_zeros(f, week_number, 2)
669 }
670 Spec::SecondsSinceEpoch => self.format_num_zeros(f, time.to_int(), 1),
671 Spec::Newline => self.format_string(f, "\n"),
672 Spec::Tabulation => self.format_string(f, "\t"),
673 Spec::Percent => self.format_string(f, "%"),
674 #[expect(clippy::string_slice, reason = "month and names values are all ASCII")]
675 #[expect(
676 clippy::indexing_slicing,
677 reason = "month and day are validated to be in range"
678 )]
679 Spec::CombinationDateTime => {
680 const MIN_WIDTH_NO_YEAR: usize = "www mmm dd HH:MM:SS ".len();
681
682 let year = time.year();
683 let default_year_width = if year < 0 { 5 } else { 4 };
684 let min_width = MIN_WIDTH_NO_YEAR + year_width(year).max(default_year_width);
685 self.write_padding(f, min_width)?;
686
687 let (day_names, month_names) = if self.flags.contains(Flag::UpperCase) {
688 (&DAYS_UPPER, &MONTHS_UPPER)
689 } else {
690 (&DAYS, &MONTHS)
691 };
692
693 let week_day_name = &day_names[usize::from(time.day_of_week()?)][..3];
694 let month_name = &month_names[usize::from(time.month()? - 1)][..3];
695 let day = time.day()?;
696 let (hour, minute, second) = (time.hour()?, time.minute()?, time.second()?);
697
698 write!(f, "{week_day_name} {month_name} ")?;
699 write!(f, "{day: >2} {hour:02}:{minute:02}:{second:02} ")?;
700 write!(f, "{year:0default_year_width$}")
701 }
702 Spec::CombinationDate => {
703 self.write_padding(f, "mm/dd/yy".len())?;
704
705 let year = time.year().rem_euclid(100);
706 let month = time.month()?;
707 let day = time.day()?;
708
709 write!(f, "{month:02}/{day:02}/{year:02}")
710 }
711 Spec::CombinationIso8601 => {
712 const MIN_WIDTH_NO_YEAR: usize = "-mm-dd".len();
713
714 let year = time.year();
715 let default_year_width = if year < 0 { 5 } else { 4 };
716 let min_width = MIN_WIDTH_NO_YEAR + year_width(year).max(default_year_width);
717 self.write_padding(f, min_width)?;
718
719 let month = time.month()?;
720 let day = time.day()?;
721
722 write!(f, "{year:0default_year_width$}-{month:02}-{day:02}")
723 }
724 #[expect(clippy::string_slice, reason = "month names values are all ASCII")]
725 #[expect(clippy::indexing_slicing, reason = "month is validated to be in range")]
726 Spec::CombinationVmsDate => {
727 let year = time.year();
728 self.write_padding(f, "dd-mmm-".len() + year_width(year).max(4))?;
729
730 let month_name = &MONTHS_UPPER[usize::from(time.month()? - 1)][..3];
731 let day = time.day()?;
732
733 write!(f, "{day: >2}-{month_name}-{year:04}")
734 }
735 Spec::CombinationTime12h => {
736 self.write_padding(f, "HH:MM:SS PM".len())?;
737
738 let hour = time.hour()? % 12;
739 let hour = if hour == 0 { 12 } else { hour };
740
741 let (minute, second) = (time.minute()?, time.second()?);
742 let meridian = if time.hour()? < 12 { "AM" } else { "PM" };
743
744 write!(f, "{hour:02}:{minute:02}:{second:02} {meridian}")
745 }
746 Spec::CombinationHourMinute24h => {
747 self.write_padding(f, "HH:MM".len())?;
748 let (hour, minute) = (time.hour()?, time.minute()?);
749 write!(f, "{hour:02}:{minute:02}")
750 }
751 Spec::CombinationTime24h => {
752 self.write_padding(f, "HH:MM:SS".len())?;
753 let (hour, minute, second) = (time.hour()?, time.minute()?, time.second()?);
754 write!(f, "{hour:02}:{minute:02}:{second:02}")
755 }
756 }
757 }
758}
759
760pub(crate) struct TimeFormatter<'t, 'f, T> {
762 time: &'t T,
764 format: &'f [u8],
766}
767
768impl<'t, 'f, T: CheckedTime> TimeFormatter<'t, 'f, T> {
769 pub(crate) fn new<F: AsRef<[u8]> + ?Sized>(time: &'t T, format: &'f F) -> Self {
771 Self {
772 time,
773 format: format.as_ref(),
774 }
775 }
776
777 #[expect(
779 clippy::indexing_slicing,
780 reason = "cursor is always within the bounds of the format string"
781 )]
782 pub(crate) fn fmt(&self, buf: &mut dyn Write) -> Result<(), Error> {
783 if self.format.is_empty() {
785 return Ok(());
786 }
787
788 let size_limit = self.format.len().saturating_mul(512 * 1024);
791 let mut f = SizeLimiter::new(buf, size_limit);
792
793 let mut cursor = Cursor::new(self.format);
794
795 loop {
796 f.write_all(cursor.read_until(|&x| x == b'%'))?;
797
798 let remaining_before = cursor.remaining();
799
800 if cursor.next().is_none() {
802 break;
803 }
804
805 if let Some(piece) = Self::parse_spec(&mut cursor)? {
806 piece.fmt(&mut f, self.time)?;
807 } else {
808 let remaining_after = cursor.remaining();
810 let text = &remaining_before[..remaining_before.len() - remaining_after.len()];
811 f.write_all(text)?;
812 }
813 }
814
815 Ok(())
816 }
817
818 #[expect(clippy::too_many_lines, reason = "must handle all enum cases")]
820 fn parse_spec(cursor: &mut Cursor<'_>) -> Result<Option<Piece>, Error> {
821 let mut padding = Padding::Left;
823 let mut flags = Flags::default();
824
825 loop {
826 match cursor.remaining().first() {
833 Some(&b'-') => {
834 padding = Padding::Left;
835 flags.set(Flag::LeftPadding);
836 }
837 Some(&b'_') => {
838 padding = Padding::Spaces;
839 }
840 Some(&b'0') => {
841 padding = Padding::Zeros;
842 }
843 Some(&b'^') => {
844 flags.set(Flag::UpperCase);
845 }
846 Some(&b'#') => {
847 flags.set(Flag::ChangeCase);
848 }
849 _ => break,
850 }
851 cursor.next();
852 }
853
854 let width_digits = str::from_utf8(cursor.read_while(u8::is_ascii_digit))
856 .expect("reading ASCII digits should yield a valid UTF-8 slice");
857
858 let width = match width_digits.parse::<usize>() {
859 Ok(width) if c_int::try_from(width).is_ok() => Some(width),
860 Err(err) if *err.kind() == IntErrorKind::Empty => None,
861 _ => return Ok(None),
862 };
863
864 if let Some(&[ext, spec]) = cursor.remaining().get(..2) {
868 const EXT_E_SPECS: &[u8] = assert_sorted(b"CXYcxy");
869 const EXT_O_SPECS: &[u8] = assert_sorted(b"HIMSUVWdeklmuwy");
870
871 match ext {
872 b'E' if EXT_E_SPECS.binary_search(&spec).is_ok() => {
873 cursor.next();
874 }
875 b'O' if EXT_O_SPECS.binary_search(&spec).is_ok() => {
876 cursor.next();
877 }
878 _ => {}
879 };
880 }
881
882 let colons = cursor.read_while(|&x| x == b':');
884
885 let spec = if colons.is_empty() {
886 const POSSIBLE_SPECS: &[(u8, Spec)] = assert_sorted_elem_0(&[
887 (b'%', Spec::Percent),
888 (b'A', Spec::WeekDayName),
889 (b'B', Spec::MonthName),
890 (b'C', Spec::YearDiv100),
891 (b'D', Spec::CombinationDate),
892 (b'F', Spec::CombinationIso8601),
893 (b'G', Spec::YearIso8601),
894 (b'H', Spec::Hour24hZero),
895 (b'I', Spec::Hour12hZero),
896 (b'L', Spec::MilliSecond),
897 (b'M', Spec::Minute),
898 (b'N', Spec::FractionalSecond),
899 (b'P', Spec::MeridianLower),
900 (b'R', Spec::CombinationHourMinute24h),
901 (b'S', Spec::Second),
902 (b'T', Spec::CombinationTime24h),
903 (b'U', Spec::WeekNumberFromSunday),
904 (b'V', Spec::WeekNumberIso8601),
905 (b'W', Spec::WeekNumberFromMonday),
906 (b'X', Spec::CombinationTime24h),
907 (b'Y', Spec::Year4Digits),
908 (b'Z', Spec::TimeZoneName),
909 (b'a', Spec::WeekDayNameAbbr),
910 (b'b', Spec::MonthNameAbbr),
911 (b'c', Spec::CombinationDateTime),
912 (b'd', Spec::MonthDayZero),
913 (b'e', Spec::MonthDaySpace),
914 (b'g', Spec::YearIso8601Rem100),
915 (b'h', Spec::MonthNameAbbr),
916 (b'j', Spec::YearDay),
917 (b'k', Spec::Hour24hSpace),
918 (b'l', Spec::Hour12hSpace),
919 (b'm', Spec::Month),
920 (b'n', Spec::Newline),
921 (b'p', Spec::MeridianUpper),
922 (b'r', Spec::CombinationTime12h),
923 (b's', Spec::SecondsSinceEpoch),
924 (b't', Spec::Tabulation),
925 (b'u', Spec::WeekDayFrom1),
926 (b'v', Spec::CombinationVmsDate),
927 (b'w', Spec::WeekDayFrom0),
928 (b'x', Spec::CombinationDate),
929 (b'y', Spec::YearRem100),
930 (b'z', Spec::TimeZoneOffsetHourMinute),
931 ]);
932
933 match cursor.next() {
934 Some(x) => match POSSIBLE_SPECS.binary_search_by_key(&x, |&(c, _)| c) {
935 #[expect(
936 clippy::indexing_slicing,
937 reason = "index is returned from binary search"
938 )]
939 Ok(index) => Some(POSSIBLE_SPECS[index].1),
940 Err(_) => None,
941 },
942 None => return Err(Error::InvalidFormatString),
943 }
944 } else if cursor.read_optional_tag(b"z") {
945 match colons.len() {
946 1 => Some(Spec::TimeZoneOffsetHourMinuteColon),
947 2 => Some(Spec::TimeZoneOffsetHourMinuteSecondColon),
948 3 => Some(Spec::TimeZoneOffsetColonMinimal),
949 _ => None,
950 }
951 } else {
952 None
953 };
954
955 Ok(spec.map(|spec| Piece::new(width, padding, flags, spec)))
956 }
957}
958
959fn year_width(year: i32) -> usize {
961 const MINUS_SIGN_WIDTH: usize = 1;
962 let mut n = if year <= 0 { MINUS_SIGN_WIDTH } else { 0 };
963 let mut val = year;
964 while val != 0 {
965 val /= 10;
966 n += 1;
967 }
968 n
969}
970
971#[cfg(test)]
972mod tests {
973 use super::*;
974
975 #[test]
976 fn test_year_width() {
977 assert_eq!(year_width(-100), 4);
978 assert_eq!(year_width(-99), 3);
979 assert_eq!(year_width(-10), 3);
980 assert_eq!(year_width(-9), 2);
981 assert_eq!(year_width(-1), 2);
982 assert_eq!(year_width(0), 1);
983 assert_eq!(year_width(1), 1);
984 assert_eq!(year_width(9), 1);
985 assert_eq!(year_width(10), 2);
986 assert_eq!(year_width(99), 2);
987 assert_eq!(year_width(100), 3);
988 assert_eq!(year_width(2025), 4);
989 }
990
991 #[cfg(feature = "alloc")]
992 #[test]
993 fn test_flag_debug_is_non_empty() {
994 use alloc::format;
995
996 assert!(!format!("{:?}", Flag::LeftPadding).is_empty());
997 assert!(!format!("{:?}", Flag::ChangeCase).is_empty());
998 assert!(!format!("{:?}", Flag::UpperCase).is_empty());
999 }
1000
1001 #[cfg(feature = "alloc")]
1002 #[test]
1003 fn test_flags_debug_is_non_empty() {
1004 use alloc::format;
1005
1006 assert!(!format!("{:?}", Flags::default()).is_empty());
1007 }
1008
1009 #[cfg(feature = "alloc")]
1010 #[test]
1011 fn test_padding_debug_is_non_empty() {
1012 use alloc::format;
1013
1014 assert!(!format!("{:?}", Padding::Left).is_empty());
1015 assert!(!format!("{:?}", Padding::Spaces).is_empty());
1016 assert!(!format!("{:?}", Padding::Zeros).is_empty());
1017 }
1018
1019 #[cfg(feature = "alloc")]
1020 #[test]
1021 fn test_spec_debug_is_non_empty() {
1022 use alloc::format;
1023
1024 assert!(!format!("{:?}", Spec::Year4Digits).is_empty());
1025 assert!(!format!("{:?}", Spec::YearDiv100).is_empty());
1026 assert!(!format!("{:?}", Spec::YearRem100).is_empty());
1027 assert!(!format!("{:?}", Spec::Month).is_empty());
1028 assert!(!format!("{:?}", Spec::MonthName).is_empty());
1029 assert!(!format!("{:?}", Spec::MonthNameAbbr).is_empty());
1030 assert!(!format!("{:?}", Spec::MonthDayZero).is_empty());
1031 assert!(!format!("{:?}", Spec::MonthDaySpace).is_empty());
1032 assert!(!format!("{:?}", Spec::YearDay).is_empty());
1033 assert!(!format!("{:?}", Spec::Hour24hZero).is_empty());
1034 assert!(!format!("{:?}", Spec::Hour24hSpace).is_empty());
1035 assert!(!format!("{:?}", Spec::Hour12hZero).is_empty());
1036 assert!(!format!("{:?}", Spec::Hour12hSpace).is_empty());
1037 assert!(!format!("{:?}", Spec::MeridianLower).is_empty());
1038 assert!(!format!("{:?}", Spec::MeridianUpper).is_empty());
1039 assert!(!format!("{:?}", Spec::Minute).is_empty());
1040 assert!(!format!("{:?}", Spec::Second).is_empty());
1041 assert!(!format!("{:?}", Spec::MilliSecond).is_empty());
1042 assert!(!format!("{:?}", Spec::FractionalSecond).is_empty());
1043 assert!(!format!("{:?}", Spec::TimeZoneOffsetHourMinute).is_empty());
1044 assert!(!format!("{:?}", Spec::TimeZoneOffsetHourMinuteColon).is_empty());
1045 assert!(!format!("{:?}", Spec::TimeZoneOffsetHourMinuteSecondColon).is_empty());
1046 assert!(!format!("{:?}", Spec::TimeZoneOffsetColonMinimal).is_empty());
1047 assert!(!format!("{:?}", Spec::TimeZoneName).is_empty());
1048 assert!(!format!("{:?}", Spec::WeekDayName).is_empty());
1049 assert!(!format!("{:?}", Spec::WeekDayNameAbbr).is_empty());
1050 assert!(!format!("{:?}", Spec::WeekDayFrom1).is_empty());
1051 assert!(!format!("{:?}", Spec::WeekDayFrom0).is_empty());
1052 assert!(!format!("{:?}", Spec::YearIso8601).is_empty());
1053 assert!(!format!("{:?}", Spec::YearIso8601Rem100).is_empty());
1054 assert!(!format!("{:?}", Spec::WeekNumberIso8601).is_empty());
1055 assert!(!format!("{:?}", Spec::WeekNumberFromSunday).is_empty());
1056 assert!(!format!("{:?}", Spec::WeekNumberFromMonday).is_empty());
1057 assert!(!format!("{:?}", Spec::SecondsSinceEpoch).is_empty());
1058 assert!(!format!("{:?}", Spec::Newline).is_empty());
1059 assert!(!format!("{:?}", Spec::Tabulation).is_empty());
1060 assert!(!format!("{:?}", Spec::Percent).is_empty());
1061 assert!(!format!("{:?}", Spec::CombinationDateTime).is_empty());
1062 assert!(!format!("{:?}", Spec::CombinationDate).is_empty());
1063 assert!(!format!("{:?}", Spec::CombinationIso8601).is_empty());
1064 assert!(!format!("{:?}", Spec::CombinationVmsDate).is_empty());
1065 assert!(!format!("{:?}", Spec::CombinationTime12h).is_empty());
1066 assert!(!format!("{:?}", Spec::CombinationHourMinute24h).is_empty());
1067 assert!(!format!("{:?}", Spec::CombinationTime24h).is_empty());
1068 }
1069
1070 #[cfg(feature = "alloc")]
1071 #[test]
1072 fn test_utc_offset_debug_is_non_empty() {
1073 use alloc::format;
1074
1075 assert!(!format!("{:?}", UtcOffset::new(0.0, 0, 0)).is_empty());
1076 }
1077
1078 #[cfg(feature = "alloc")]
1079 #[test]
1080 fn test_piece_debug_is_non_empty() {
1081 use alloc::format;
1082
1083 let piece = Piece::new(
1084 None,
1085 Padding::Spaces,
1086 Flags::default(),
1087 Spec::CombinationTime24h,
1088 );
1089
1090 assert!(!format!("{piece:?}").is_empty());
1091 }
1092}