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 Some(byte) if !byte.is_ascii() => {
850 return Ok(None);
853 }
854 _ => break,
855 }
856 cursor.next();
857 }
858
859 let width_digits = str::from_utf8(cursor.read_while(u8::is_ascii_digit))
861 .expect("reading ASCII digits should yield a valid UTF-8 slice");
862
863 let width = match width_digits.parse::<usize>() {
864 Ok(width) if c_int::try_from(width).is_ok() => Some(width),
865 Err(err) if *err.kind() == IntErrorKind::Empty => None,
866 _ => return Ok(None),
867 };
868
869 if let Some(&[ext, spec]) = cursor.remaining().get(..2) {
873 const EXT_E_SPECS: &[u8] = assert_sorted(b"CXYcxy");
874 const EXT_O_SPECS: &[u8] = assert_sorted(b"HIMSUVWdeklmuwy");
875
876 match ext {
877 b'E' if EXT_E_SPECS.binary_search(&spec).is_ok() => {
878 cursor.next();
879 }
880 b'O' if EXT_O_SPECS.binary_search(&spec).is_ok() => {
881 cursor.next();
882 }
883 _ => {}
884 };
885 }
886
887 let colons = cursor.read_while(|&x| x == b':');
889
890 let spec = if colons.is_empty() {
891 const POSSIBLE_SPECS: &[(u8, Spec)] = assert_sorted_elem_0(&[
892 (b'%', Spec::Percent),
893 (b'A', Spec::WeekDayName),
894 (b'B', Spec::MonthName),
895 (b'C', Spec::YearDiv100),
896 (b'D', Spec::CombinationDate),
897 (b'F', Spec::CombinationIso8601),
898 (b'G', Spec::YearIso8601),
899 (b'H', Spec::Hour24hZero),
900 (b'I', Spec::Hour12hZero),
901 (b'L', Spec::MilliSecond),
902 (b'M', Spec::Minute),
903 (b'N', Spec::FractionalSecond),
904 (b'P', Spec::MeridianLower),
905 (b'R', Spec::CombinationHourMinute24h),
906 (b'S', Spec::Second),
907 (b'T', Spec::CombinationTime24h),
908 (b'U', Spec::WeekNumberFromSunday),
909 (b'V', Spec::WeekNumberIso8601),
910 (b'W', Spec::WeekNumberFromMonday),
911 (b'X', Spec::CombinationTime24h),
912 (b'Y', Spec::Year4Digits),
913 (b'Z', Spec::TimeZoneName),
914 (b'a', Spec::WeekDayNameAbbr),
915 (b'b', Spec::MonthNameAbbr),
916 (b'c', Spec::CombinationDateTime),
917 (b'd', Spec::MonthDayZero),
918 (b'e', Spec::MonthDaySpace),
919 (b'g', Spec::YearIso8601Rem100),
920 (b'h', Spec::MonthNameAbbr),
921 (b'j', Spec::YearDay),
922 (b'k', Spec::Hour24hSpace),
923 (b'l', Spec::Hour12hSpace),
924 (b'm', Spec::Month),
925 (b'n', Spec::Newline),
926 (b'p', Spec::MeridianUpper),
927 (b'r', Spec::CombinationTime12h),
928 (b's', Spec::SecondsSinceEpoch),
929 (b't', Spec::Tabulation),
930 (b'u', Spec::WeekDayFrom1),
931 (b'v', Spec::CombinationVmsDate),
932 (b'w', Spec::WeekDayFrom0),
933 (b'x', Spec::CombinationDate),
934 (b'y', Spec::YearRem100),
935 (b'z', Spec::TimeZoneOffsetHourMinute),
936 ]);
937
938 match cursor.remaining().first() {
939 Some(x) if !x.is_ascii() => {
940 return Ok(None);
943 }
944 Some(x) => {
945 cursor.next();
946
947 match POSSIBLE_SPECS.binary_search_by_key(&x, |(c, _)| c) {
948 #[expect(
949 clippy::indexing_slicing,
950 reason = "index is returned from binary search"
951 )]
952 Ok(index) => Some(POSSIBLE_SPECS[index].1),
953 Err(_) => None,
954 }
955 }
956 None => return Err(Error::InvalidFormatString),
957 }
958 } else if cursor.read_optional_tag(b"z") {
959 match colons.len() {
960 1 => Some(Spec::TimeZoneOffsetHourMinuteColon),
961 2 => Some(Spec::TimeZoneOffsetHourMinuteSecondColon),
962 3 => Some(Spec::TimeZoneOffsetColonMinimal),
963 _ => None,
964 }
965 } else {
966 None
967 };
968
969 Ok(spec.map(|spec| Piece::new(width, padding, flags, spec)))
970 }
971}
972
973fn year_width(year: i32) -> usize {
975 const MINUS_SIGN_WIDTH: usize = 1;
976 let mut n = if year <= 0 { MINUS_SIGN_WIDTH } else { 0 };
977 let mut val = year;
978 while val != 0 {
979 val /= 10;
980 n += 1;
981 }
982 n
983}
984
985#[cfg(test)]
986mod tests {
987 use super::*;
988
989 #[test]
990 fn test_year_width() {
991 assert_eq!(year_width(-100), 4);
992 assert_eq!(year_width(-99), 3);
993 assert_eq!(year_width(-10), 3);
994 assert_eq!(year_width(-9), 2);
995 assert_eq!(year_width(-1), 2);
996 assert_eq!(year_width(0), 1);
997 assert_eq!(year_width(1), 1);
998 assert_eq!(year_width(9), 1);
999 assert_eq!(year_width(10), 2);
1000 assert_eq!(year_width(99), 2);
1001 assert_eq!(year_width(100), 3);
1002 assert_eq!(year_width(2025), 4);
1003 }
1004
1005 #[cfg(feature = "alloc")]
1006 #[test]
1007 fn test_flag_debug_is_non_empty() {
1008 use alloc::format;
1009
1010 assert!(!format!("{:?}", Flag::LeftPadding).is_empty());
1011 assert!(!format!("{:?}", Flag::ChangeCase).is_empty());
1012 assert!(!format!("{:?}", Flag::UpperCase).is_empty());
1013 }
1014
1015 #[cfg(feature = "alloc")]
1016 #[test]
1017 fn test_flags_debug_is_non_empty() {
1018 use alloc::format;
1019
1020 assert!(!format!("{:?}", Flags::default()).is_empty());
1021 }
1022
1023 #[cfg(feature = "alloc")]
1024 #[test]
1025 fn test_padding_debug_is_non_empty() {
1026 use alloc::format;
1027
1028 assert!(!format!("{:?}", Padding::Left).is_empty());
1029 assert!(!format!("{:?}", Padding::Spaces).is_empty());
1030 assert!(!format!("{:?}", Padding::Zeros).is_empty());
1031 }
1032
1033 #[cfg(feature = "alloc")]
1034 #[test]
1035 fn test_spec_debug_is_non_empty() {
1036 use alloc::format;
1037
1038 assert!(!format!("{:?}", Spec::Year4Digits).is_empty());
1039 assert!(!format!("{:?}", Spec::YearDiv100).is_empty());
1040 assert!(!format!("{:?}", Spec::YearRem100).is_empty());
1041 assert!(!format!("{:?}", Spec::Month).is_empty());
1042 assert!(!format!("{:?}", Spec::MonthName).is_empty());
1043 assert!(!format!("{:?}", Spec::MonthNameAbbr).is_empty());
1044 assert!(!format!("{:?}", Spec::MonthDayZero).is_empty());
1045 assert!(!format!("{:?}", Spec::MonthDaySpace).is_empty());
1046 assert!(!format!("{:?}", Spec::YearDay).is_empty());
1047 assert!(!format!("{:?}", Spec::Hour24hZero).is_empty());
1048 assert!(!format!("{:?}", Spec::Hour24hSpace).is_empty());
1049 assert!(!format!("{:?}", Spec::Hour12hZero).is_empty());
1050 assert!(!format!("{:?}", Spec::Hour12hSpace).is_empty());
1051 assert!(!format!("{:?}", Spec::MeridianLower).is_empty());
1052 assert!(!format!("{:?}", Spec::MeridianUpper).is_empty());
1053 assert!(!format!("{:?}", Spec::Minute).is_empty());
1054 assert!(!format!("{:?}", Spec::Second).is_empty());
1055 assert!(!format!("{:?}", Spec::MilliSecond).is_empty());
1056 assert!(!format!("{:?}", Spec::FractionalSecond).is_empty());
1057 assert!(!format!("{:?}", Spec::TimeZoneOffsetHourMinute).is_empty());
1058 assert!(!format!("{:?}", Spec::TimeZoneOffsetHourMinuteColon).is_empty());
1059 assert!(!format!("{:?}", Spec::TimeZoneOffsetHourMinuteSecondColon).is_empty());
1060 assert!(!format!("{:?}", Spec::TimeZoneOffsetColonMinimal).is_empty());
1061 assert!(!format!("{:?}", Spec::TimeZoneName).is_empty());
1062 assert!(!format!("{:?}", Spec::WeekDayName).is_empty());
1063 assert!(!format!("{:?}", Spec::WeekDayNameAbbr).is_empty());
1064 assert!(!format!("{:?}", Spec::WeekDayFrom1).is_empty());
1065 assert!(!format!("{:?}", Spec::WeekDayFrom0).is_empty());
1066 assert!(!format!("{:?}", Spec::YearIso8601).is_empty());
1067 assert!(!format!("{:?}", Spec::YearIso8601Rem100).is_empty());
1068 assert!(!format!("{:?}", Spec::WeekNumberIso8601).is_empty());
1069 assert!(!format!("{:?}", Spec::WeekNumberFromSunday).is_empty());
1070 assert!(!format!("{:?}", Spec::WeekNumberFromMonday).is_empty());
1071 assert!(!format!("{:?}", Spec::SecondsSinceEpoch).is_empty());
1072 assert!(!format!("{:?}", Spec::Newline).is_empty());
1073 assert!(!format!("{:?}", Spec::Tabulation).is_empty());
1074 assert!(!format!("{:?}", Spec::Percent).is_empty());
1075 assert!(!format!("{:?}", Spec::CombinationDateTime).is_empty());
1076 assert!(!format!("{:?}", Spec::CombinationDate).is_empty());
1077 assert!(!format!("{:?}", Spec::CombinationIso8601).is_empty());
1078 assert!(!format!("{:?}", Spec::CombinationVmsDate).is_empty());
1079 assert!(!format!("{:?}", Spec::CombinationTime12h).is_empty());
1080 assert!(!format!("{:?}", Spec::CombinationHourMinute24h).is_empty());
1081 assert!(!format!("{:?}", Spec::CombinationTime24h).is_empty());
1082 }
1083
1084 #[cfg(feature = "alloc")]
1085 #[test]
1086 fn test_utc_offset_debug_is_non_empty() {
1087 use alloc::format;
1088
1089 assert!(!format!("{:?}", UtcOffset::new(0.0, 0, 0)).is_empty());
1090 }
1091
1092 #[cfg(feature = "alloc")]
1093 #[test]
1094 fn test_piece_debug_is_non_empty() {
1095 use alloc::format;
1096
1097 let piece = Piece::new(
1098 None,
1099 Padding::Spaces,
1100 Flags::default(),
1101 Spec::CombinationTime24h,
1102 );
1103
1104 assert!(!format!("{piece:?}").is_empty());
1105 }
1106}