1use crate::{
2 error::{err, ErrorContext},
3 fmt::{
4 strtime::{
5 month_name_abbrev, month_name_full, weekday_name_abbrev,
6 weekday_name_full, BrokenDownTime, Extension, Flag,
7 },
8 util::{DecimalFormatter, FractionalFormatter},
9 Write, WriteExt,
10 },
11 tz::Offset,
12 util::{escape, t::C, utf8},
13 Error,
14};
15
16pub(super) struct Formatter<'f, 't, 'w, W> {
17 pub(super) fmt: &'f [u8],
18 pub(super) tm: &'t BrokenDownTime,
19 pub(super) wtr: &'w mut W,
20}
21
22impl<'f, 't, 'w, W: Write> Formatter<'f, 't, 'w, W> {
23 pub(super) fn format(&mut self) -> Result<(), Error> {
24 while !self.fmt.is_empty() {
25 if self.f() != b'%' {
26 if self.f().is_ascii() {
27 self.wtr.write_char(char::from(self.f()))?;
28 self.bump_fmt();
29 } else {
30 let ch = self.utf8_decode_and_bump()?;
31 self.wtr.write_char(ch)?;
32 }
33 continue;
34 }
35 if !self.bump_fmt() {
36 return Err(err!(
37 "invalid format string, expected byte after '%', \
38 but found end of format string",
39 ));
40 }
41 let ext = self.parse_extension()?;
43 match self.f() {
44 b'%' => self.wtr.write_str("%").context("%% failed")?,
45 b'A' => self.fmt_weekday_full(ext).context("%A failed")?,
46 b'a' => self.fmt_weekday_abbrev(ext).context("%a failed")?,
47 b'B' => self.fmt_month_full(ext).context("%B failed")?,
48 b'b' => self.fmt_month_abbrev(ext).context("%b failed")?,
49 b'C' => self.fmt_century(ext).context("%C failed")?,
50 b'D' => self.fmt_american_date(ext).context("%D failed")?,
51 b'd' => self.fmt_day_zero(ext).context("%d failed")?,
52 b'e' => self.fmt_day_space(ext).context("%e failed")?,
53 b'F' => self.fmt_iso_date(ext).context("%F failed")?,
54 b'f' => self.fmt_fractional(ext).context("%f failed")?,
55 b'G' => self.fmt_iso_week_year(ext).context("%G failed")?,
56 b'g' => self.fmt_iso_week_year2(ext).context("%g failed")?,
57 b'H' => self.fmt_hour24_zero(ext).context("%H failed")?,
58 b'h' => self.fmt_month_abbrev(ext).context("%b failed")?,
59 b'I' => self.fmt_hour12_zero(ext).context("%H failed")?,
60 b'j' => self.fmt_day_of_year(ext).context("%j failed")?,
61 b'k' => self.fmt_hour24_space(ext).context("%k failed")?,
62 b'l' => self.fmt_hour12_space(ext).context("%l failed")?,
63 b'M' => self.fmt_minute(ext).context("%M failed")?,
64 b'm' => self.fmt_month(ext).context("%m failed")?,
65 b'n' => self.fmt_literal("\n").context("%n failed")?,
66 b'P' => self.fmt_ampm_lower(ext).context("%P failed")?,
67 b'p' => self.fmt_ampm_upper(ext).context("%p failed")?,
68 b'Q' => self.fmt_iana_nocolon().context("%Q failed")?,
69 b'R' => self.fmt_clock_nosecs(ext).context("%R failed")?,
70 b'S' => self.fmt_second(ext).context("%S failed")?,
71 b's' => self.fmt_timestamp(ext).context("%s failed")?,
72 b'T' => self.fmt_clock_secs(ext).context("%T failed")?,
73 b't' => self.fmt_literal("\t").context("%t failed")?,
74 b'U' => self.fmt_week_sun(ext).context("%U failed")?,
75 b'u' => self.fmt_weekday_mon(ext).context("%u failed")?,
76 b'V' => self.fmt_week_iso(ext).context("%V failed")?,
77 b'W' => self.fmt_week_mon(ext).context("%W failed")?,
78 b'w' => self.fmt_weekday_sun(ext).context("%w failed")?,
79 b'Y' => self.fmt_year(ext).context("%Y failed")?,
80 b'y' => self.fmt_year2(ext).context("%y failed")?,
81 b'Z' => self.fmt_tzabbrev(ext).context("%Z failed")?,
82 b'z' => self.fmt_offset_nocolon().context("%z failed")?,
83 b':' => {
84 if !self.bump_fmt() {
85 return Err(err!(
86 "invalid format string, expected directive \
87 after '%:'",
88 ));
89 }
90 match self.f() {
91 b'Q' => self.fmt_iana_colon().context("%:Q failed")?,
92 b'z' => {
93 self.fmt_offset_colon().context("%:z failed")?
94 }
95 unk => {
96 return Err(err!(
97 "found unrecognized directive %{unk} \
98 following %:",
99 unk = escape::Byte(unk),
100 ));
101 }
102 }
103 }
104 b'.' => {
105 if !self.bump_fmt() {
106 return Err(err!(
107 "invalid format string, expected directive \
108 after '%.'",
109 ));
110 }
111 let ext = Extension { width: self.parse_width()?, ..ext };
114 match self.f() {
115 b'f' => self
116 .fmt_dot_fractional(ext)
117 .context("%.f failed")?,
118 unk => {
119 return Err(err!(
120 "found unrecognized directive %{unk} \
121 following %.",
122 unk = escape::Byte(unk),
123 ));
124 }
125 }
126 }
127 unk => {
128 return Err(err!(
129 "found unrecognized specifier directive %{unk}",
130 unk = escape::Byte(unk),
131 ));
132 }
133 }
134 self.bump_fmt();
135 }
136 Ok(())
137 }
138
139 fn f(&self) -> u8 {
145 self.fmt[0]
146 }
147
148 fn bump_fmt(&mut self) -> bool {
153 self.fmt = &self.fmt[1..];
154 !self.fmt.is_empty()
155 }
156
157 #[inline(never)]
172 fn utf8_decode_and_bump(&mut self) -> Result<char, Error> {
173 match utf8::decode(self.fmt).expect("non-empty fmt") {
174 Ok(ch) => {
175 self.fmt = &self.fmt[ch.len_utf8()..];
176 return Ok(ch);
177 }
178 Err(invalid) => Err(err!(
179 "found invalid UTF-8 byte {byte:?} in format \
180 string (format strings must be valid UTF-8)",
181 byte = escape::Byte(invalid),
182 )),
183 }
184 }
185
186 fn parse_extension(&mut self) -> Result<Extension, Error> {
190 let flag = self.parse_flag()?;
191 let width = self.parse_width()?;
192 Ok(Extension { flag, width })
193 }
194
195 fn parse_flag(&mut self) -> Result<Option<Flag>, Error> {
198 let (flag, fmt) = Extension::parse_flag(self.fmt)?;
199 self.fmt = fmt;
200 Ok(flag)
201 }
202
203 fn parse_width(&mut self) -> Result<Option<u8>, Error> {
213 let (width, fmt) = Extension::parse_width(self.fmt)?;
214 self.fmt = fmt;
215 Ok(width)
216 }
217
218 fn fmt_ampm_lower(&mut self, ext: Extension) -> Result<(), Error> {
225 let hour = self
226 .tm
227 .hour
228 .ok_or_else(|| err!("requires time to format AM/PM"))?
229 .get();
230 ext.write_str(
231 Case::AsIs,
232 if hour < 12 { "am" } else { "pm" },
233 self.wtr,
234 )
235 }
236
237 fn fmt_ampm_upper(&mut self, ext: Extension) -> Result<(), Error> {
239 let hour = self
240 .tm
241 .hour
242 .ok_or_else(|| err!("requires time to format AM/PM"))?
243 .get();
244 ext.write_str(
245 Case::Upper,
246 if hour < 12 { "AM" } else { "PM" },
247 self.wtr,
248 )
249 }
250
251 fn fmt_american_date(&mut self, ext: Extension) -> Result<(), Error> {
253 self.fmt_month(ext)?;
254 self.wtr.write_char('/')?;
255 self.fmt_day_zero(ext)?;
256 self.wtr.write_char('/')?;
257 self.fmt_year2(ext)?;
258 Ok(())
259 }
260
261 fn fmt_clock_nosecs(&mut self, ext: Extension) -> Result<(), Error> {
263 self.fmt_hour24_zero(ext)?;
264 self.wtr.write_char(':')?;
265 self.fmt_minute(ext)?;
266 Ok(())
267 }
268
269 fn fmt_clock_secs(&mut self, ext: Extension) -> Result<(), Error> {
271 self.fmt_hour24_zero(ext)?;
272 self.wtr.write_char(':')?;
273 self.fmt_minute(ext)?;
274 self.wtr.write_char(':')?;
275 self.fmt_second(ext)?;
276 Ok(())
277 }
278
279 fn fmt_day_zero(&mut self, ext: Extension) -> Result<(), Error> {
281 let day = self
282 .tm
283 .day
284 .or_else(|| self.tm.to_date().ok().map(|d| d.day_ranged()))
285 .ok_or_else(|| err!("requires date to format day"))?
286 .get();
287 ext.write_int(b'0', Some(2), day, self.wtr)
288 }
289
290 fn fmt_day_space(&mut self, ext: Extension) -> Result<(), Error> {
292 let day = self
293 .tm
294 .day
295 .or_else(|| self.tm.to_date().ok().map(|d| d.day_ranged()))
296 .ok_or_else(|| err!("requires date to format day"))?
297 .get();
298 ext.write_int(b' ', Some(2), day, self.wtr)
299 }
300
301 fn fmt_hour12_zero(&mut self, ext: Extension) -> Result<(), Error> {
303 let mut hour = self
304 .tm
305 .hour
306 .ok_or_else(|| err!("requires time to format hour"))?
307 .get();
308 if hour == 0 {
309 hour = 12;
310 } else if hour > 12 {
311 hour -= 12;
312 }
313 ext.write_int(b'0', Some(2), hour, self.wtr)
314 }
315
316 fn fmt_hour24_zero(&mut self, ext: Extension) -> Result<(), Error> {
318 let hour = self
319 .tm
320 .hour
321 .ok_or_else(|| err!("requires time to format hour"))?
322 .get();
323 ext.write_int(b'0', Some(2), hour, self.wtr)
324 }
325
326 fn fmt_hour12_space(&mut self, ext: Extension) -> Result<(), Error> {
328 let mut hour = self
329 .tm
330 .hour
331 .ok_or_else(|| err!("requires time to format hour"))?
332 .get();
333 if hour == 0 {
334 hour = 12;
335 } else if hour > 12 {
336 hour -= 12;
337 }
338 ext.write_int(b' ', Some(2), hour, self.wtr)
339 }
340
341 fn fmt_hour24_space(&mut self, ext: Extension) -> Result<(), Error> {
343 let hour = self
344 .tm
345 .hour
346 .ok_or_else(|| err!("requires time to format hour"))?
347 .get();
348 ext.write_int(b' ', Some(2), hour, self.wtr)
349 }
350
351 fn fmt_iso_date(&mut self, ext: Extension) -> Result<(), Error> {
353 self.fmt_year(ext)?;
354 self.wtr.write_char('-')?;
355 self.fmt_month(ext)?;
356 self.wtr.write_char('-')?;
357 self.fmt_day_zero(ext)?;
358 Ok(())
359 }
360
361 fn fmt_minute(&mut self, ext: Extension) -> Result<(), Error> {
363 let minute = self
364 .tm
365 .minute
366 .ok_or_else(|| err!("requires time to format minute"))?
367 .get();
368 ext.write_int(b'0', Some(2), minute, self.wtr)
369 }
370
371 fn fmt_month(&mut self, ext: Extension) -> Result<(), Error> {
373 let month = self
374 .tm
375 .month
376 .or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged()))
377 .ok_or_else(|| err!("requires date to format month"))?
378 .get();
379 ext.write_int(b'0', Some(2), month, self.wtr)
380 }
381
382 fn fmt_month_full(&mut self, ext: Extension) -> Result<(), Error> {
384 let month = self
385 .tm
386 .month
387 .or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged()))
388 .ok_or_else(|| err!("requires date to format month"))?;
389 ext.write_str(Case::AsIs, month_name_full(month), self.wtr)
390 }
391
392 fn fmt_month_abbrev(&mut self, ext: Extension) -> Result<(), Error> {
394 let month = self
395 .tm
396 .month
397 .or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged()))
398 .ok_or_else(|| err!("requires date to format month"))?;
399 ext.write_str(Case::AsIs, month_name_abbrev(month), self.wtr)
400 }
401
402 fn fmt_iana_nocolon(&mut self) -> Result<(), Error> {
404 let Some(iana) = self.tm.iana_time_zone() else {
405 let offset = self.tm.offset.ok_or_else(|| {
406 err!(
407 "requires IANA time zone identifier or time \
408 zone offset, but none were present"
409 )
410 })?;
411 return write_offset(offset, false, &mut self.wtr);
412 };
413 self.wtr.write_str(iana)?;
414 Ok(())
415 }
416
417 fn fmt_iana_colon(&mut self) -> Result<(), Error> {
419 let Some(iana) = self.tm.iana_time_zone() else {
420 let offset = self.tm.offset.ok_or_else(|| {
421 err!(
422 "requires IANA time zone identifier or time \
423 zone offset, but none were present"
424 )
425 })?;
426 return write_offset(offset, true, &mut self.wtr);
427 };
428 self.wtr.write_str(iana)?;
429 Ok(())
430 }
431
432 fn fmt_offset_nocolon(&mut self) -> Result<(), Error> {
434 let offset = self.tm.offset.ok_or_else(|| {
435 err!("requires offset to format time zone offset")
436 })?;
437 write_offset(offset, false, self.wtr)
438 }
439
440 fn fmt_offset_colon(&mut self) -> Result<(), Error> {
442 let offset = self.tm.offset.ok_or_else(|| {
443 err!("requires offset to format time zone offset")
444 })?;
445 write_offset(offset, true, self.wtr)
446 }
447
448 fn fmt_second(&mut self, ext: Extension) -> Result<(), Error> {
450 let second = self
451 .tm
452 .second
453 .ok_or_else(|| err!("requires time to format second"))?
454 .get();
455 ext.write_int(b'0', Some(2), second, self.wtr)
456 }
457
458 fn fmt_timestamp(&mut self, ext: Extension) -> Result<(), Error> {
460 let timestamp = self.tm.to_timestamp().map_err(|_| {
461 err!(
462 "requires instant (a date, time and offset) \
463 to format Unix timestamp",
464 )
465 })?;
466 ext.write_int(b' ', None, timestamp.as_second(), self.wtr)
467 }
468
469 fn fmt_fractional(&mut self, ext: Extension) -> Result<(), Error> {
471 let subsec = self.tm.subsec.ok_or_else(|| {
472 err!("requires time to format subsecond nanoseconds")
473 })?;
474 if ext.width == Some(0) {
481 return Err(err!("zero precision with %f is not allowed"));
482 }
483 if subsec == C(0) && ext.width.is_none() {
484 self.wtr.write_str("0")?;
485 return Ok(());
486 }
487 ext.write_fractional_seconds(subsec, self.wtr)?;
488 Ok(())
489 }
490
491 fn fmt_dot_fractional(&mut self, ext: Extension) -> Result<(), Error> {
493 let Some(subsec) = self.tm.subsec else { return Ok(()) };
494 if subsec == C(0) && ext.width.is_none() || ext.width == Some(0) {
495 return Ok(());
496 }
497 ext.write_str(Case::AsIs, ".", self.wtr)?;
498 ext.write_fractional_seconds(subsec, self.wtr)?;
499 Ok(())
500 }
501
502 fn fmt_tzabbrev(&mut self, ext: Extension) -> Result<(), Error> {
504 let tzabbrev = self.tm.tzabbrev.as_ref().ok_or_else(|| {
505 err!("requires time zone abbreviation in broken down time")
506 })?;
507 ext.write_str(Case::Upper, tzabbrev.as_str(), self.wtr)
508 }
509
510 fn fmt_weekday_full(&mut self, ext: Extension) -> Result<(), Error> {
512 let weekday = self
513 .tm
514 .weekday
515 .or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
516 .ok_or_else(|| err!("requires date to format weekday"))?;
517 ext.write_str(Case::AsIs, weekday_name_full(weekday), self.wtr)
518 }
519
520 fn fmt_weekday_abbrev(&mut self, ext: Extension) -> Result<(), Error> {
522 let weekday = self
523 .tm
524 .weekday
525 .or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
526 .ok_or_else(|| err!("requires date to format weekday"))?;
527 ext.write_str(Case::AsIs, weekday_name_abbrev(weekday), self.wtr)
528 }
529
530 fn fmt_weekday_mon(&mut self, ext: Extension) -> Result<(), Error> {
532 let weekday = self
533 .tm
534 .weekday
535 .or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
536 .ok_or_else(|| err!("requires date to format weekday number"))?;
537 ext.write_int(b' ', None, weekday.to_monday_one_offset(), self.wtr)
538 }
539
540 fn fmt_weekday_sun(&mut self, ext: Extension) -> Result<(), Error> {
542 let weekday = self
543 .tm
544 .weekday
545 .or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
546 .ok_or_else(|| err!("requires date to format weekday number"))?;
547 ext.write_int(b' ', None, weekday.to_sunday_zero_offset(), self.wtr)
548 }
549
550 fn fmt_week_sun(&mut self, ext: Extension) -> Result<(), Error> {
552 if let Some(weeknum) = self.tm.week_sun {
554 return ext.write_int(b'0', Some(2), weeknum, self.wtr);
555 }
556 let day = self
557 .tm
558 .day_of_year
559 .map(|day| day.get())
560 .or_else(|| self.tm.to_date().ok().map(|d| d.day_of_year()))
561 .ok_or_else(|| {
562 err!("requires date to format Sunday-based week number")
563 })?;
564 let weekday = self
565 .tm
566 .weekday
567 .or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
568 .ok_or_else(|| {
569 err!("requires date to format Sunday-based week number")
570 })?
571 .to_sunday_zero_offset();
572 let weeknum = (day + 6 - i16::from(weekday)) / 7;
578 ext.write_int(b'0', Some(2), weeknum, self.wtr)
579 }
580
581 fn fmt_week_iso(&mut self, ext: Extension) -> Result<(), Error> {
583 let weeknum = self
584 .tm
585 .iso_week
586 .or_else(|| {
587 self.tm.to_date().ok().map(|d| d.iso_week_date().week_ranged())
588 })
589 .ok_or_else(|| {
590 err!("requires date to format ISO 8601 week number")
591 })?;
592 ext.write_int(b'0', Some(2), weeknum, self.wtr)
593 }
594
595 fn fmt_week_mon(&mut self, ext: Extension) -> Result<(), Error> {
597 if let Some(weeknum) = self.tm.week_mon {
599 return ext.write_int(b'0', Some(2), weeknum, self.wtr);
600 }
601 let day = self
602 .tm
603 .day_of_year
604 .map(|day| day.get())
605 .or_else(|| self.tm.to_date().ok().map(|d| d.day_of_year()))
606 .ok_or_else(|| {
607 err!("requires date to format Monday-based week number")
608 })?;
609 let weekday = self
610 .tm
611 .weekday
612 .or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
613 .ok_or_else(|| {
614 err!("requires date to format Monday-based week number")
615 })?
616 .to_sunday_zero_offset();
617 let weeknum = (day + 6 - ((i16::from(weekday) + 6) % 7)) / 7;
623 ext.write_int(b'0', Some(2), weeknum, self.wtr)
624 }
625
626 fn fmt_year(&mut self, ext: Extension) -> Result<(), Error> {
628 let year = self
629 .tm
630 .year
631 .or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged()))
632 .ok_or_else(|| err!("requires date to format year"))?
633 .get();
634 ext.write_int(b'0', Some(4), year, self.wtr)
635 }
636
637 fn fmt_year2(&mut self, ext: Extension) -> Result<(), Error> {
639 let year = self
640 .tm
641 .year
642 .or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged()))
643 .ok_or_else(|| err!("requires date to format year (2-digit)"))?
644 .get();
645 if !(1969 <= year && year <= 2068) {
646 return Err(err!(
647 "formatting a 2-digit year requires that it be in \
648 the inclusive range 1969 to 2068, but got {year}",
649 ));
650 }
651 let year = year % 100;
652 ext.write_int(b'0', Some(2), year, self.wtr)
653 }
654
655 fn fmt_century(&mut self, ext: Extension) -> Result<(), Error> {
657 let year = self
658 .tm
659 .year
660 .or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged()))
661 .ok_or_else(|| err!("requires date to format century (2-digit)"))?
662 .get();
663 let century = year / 100;
664 ext.write_int(b' ', None, century, self.wtr)
665 }
666
667 fn fmt_iso_week_year(&mut self, ext: Extension) -> Result<(), Error> {
669 let year = self
670 .tm
671 .iso_week_year
672 .or_else(|| {
673 self.tm.to_date().ok().map(|d| d.iso_week_date().year_ranged())
674 })
675 .ok_or_else(|| {
676 err!("requires date to format ISO 8601 week-based year")
677 })?
678 .get();
679 ext.write_int(b'0', Some(4), year, self.wtr)
680 }
681
682 fn fmt_iso_week_year2(&mut self, ext: Extension) -> Result<(), Error> {
684 let year = self
685 .tm
686 .iso_week_year
687 .or_else(|| {
688 self.tm.to_date().ok().map(|d| d.iso_week_date().year_ranged())
689 })
690 .ok_or_else(|| {
691 err!(
692 "requires date to format \
693 ISO 8601 week-based year (2-digit)"
694 )
695 })?
696 .get();
697 if !(1969 <= year && year <= 2068) {
698 return Err(err!(
699 "formatting a 2-digit ISO 8601 week-based year \
700 requires that it be in \
701 the inclusive range 1969 to 2068, but got {year}",
702 ));
703 }
704 let year = year % 100;
705 ext.write_int(b'0', Some(2), year, self.wtr)
706 }
707
708 fn fmt_day_of_year(&mut self, ext: Extension) -> Result<(), Error> {
710 let day = self
711 .tm
712 .day_of_year
713 .map(|day| day.get())
714 .or_else(|| self.tm.to_date().ok().map(|d| d.day_of_year()))
715 .ok_or_else(|| err!("requires date to format day of year"))?;
716 ext.write_int(b'0', Some(3), day, self.wtr)
717 }
718
719 fn fmt_literal(&mut self, literal: &str) -> Result<(), Error> {
721 self.wtr.write_str(literal)
722 }
723}
724
725fn write_offset<W: Write>(
730 offset: Offset,
731 colon: bool,
732 wtr: &mut W,
733) -> Result<(), Error> {
734 static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2);
735
736 let hours = offset.part_hours_ranged().abs().get();
737 let minutes = offset.part_minutes_ranged().abs().get();
738 let seconds = offset.part_seconds_ranged().abs().get();
739
740 wtr.write_str(if offset.is_negative() { "-" } else { "+" })?;
741 wtr.write_int(&FMT_TWO, hours)?;
742 if colon {
743 wtr.write_str(":")?;
744 }
745 wtr.write_int(&FMT_TWO, minutes)?;
746 if seconds != 0 {
747 if colon {
748 wtr.write_str(":")?;
749 }
750 wtr.write_int(&FMT_TWO, seconds)?;
751 }
752 Ok(())
753}
754
755impl Extension {
756 fn write_str<W: Write>(
759 self,
760 default: Case,
761 string: &str,
762 wtr: &mut W,
763 ) -> Result<(), Error> {
764 let case = match self.flag {
765 Some(Flag::Uppercase) => Case::Upper,
766 Some(Flag::Swapcase) => default.swap(),
767 _ => default,
768 };
769 match case {
770 Case::AsIs => {
771 wtr.write_str(string)?;
772 }
773 Case::Upper => {
774 for ch in string.chars() {
775 for ch in ch.to_uppercase() {
776 wtr.write_char(ch)?;
777 }
778 }
779 }
780 Case::Lower => {
781 for ch in string.chars() {
782 for ch in ch.to_lowercase() {
783 wtr.write_char(ch)?;
784 }
785 }
786 }
787 }
788 Ok(())
789 }
790
791 fn write_int<W: Write>(
794 self,
795 pad_byte: u8,
796 pad_width: Option<u8>,
797 number: impl Into<i64>,
798 wtr: &mut W,
799 ) -> Result<(), Error> {
800 let number = number.into();
801 let pad_byte = match self.flag {
802 Some(Flag::PadZero) => b'0',
803 Some(Flag::PadSpace) => b' ',
804 _ => pad_byte,
805 };
806 let pad_width = if matches!(self.flag, Some(Flag::NoPad)) {
807 None
808 } else {
809 self.width.or(pad_width)
810 };
811
812 let mut formatter = DecimalFormatter::new().padding_byte(pad_byte);
813 if let Some(width) = pad_width {
814 formatter = formatter.padding(width);
815 }
816 wtr.write_int(&formatter, number)
817 }
818
819 fn write_fractional_seconds<W: Write>(
824 self,
825 number: impl Into<i64>,
826 wtr: &mut W,
827 ) -> Result<(), Error> {
828 let number = number.into();
829
830 let formatter = FractionalFormatter::new().precision(self.width);
831 wtr.write_fraction(&formatter, number)
832 }
833}
834
835#[derive(Clone, Copy, Debug)]
837enum Case {
838 AsIs,
839 Upper,
840 Lower,
841}
842
843impl Case {
844 fn swap(self) -> Case {
846 match self {
847 Case::AsIs => Case::AsIs,
848 Case::Upper => Case::Lower,
849 Case::Lower => Case::Upper,
850 }
851 }
852}
853
854#[cfg(feature = "alloc")]
855#[cfg(test)]
856mod tests {
857 use crate::{
858 civil::{date, time, Date, DateTime, Time},
859 fmt::strtime::format,
860 Timestamp, Zoned,
861 };
862
863 #[test]
864 fn ok_format_american_date() {
865 let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
866
867 insta::assert_snapshot!(f("%D", date(2024, 7, 9)), @"07/09/24");
868 insta::assert_snapshot!(f("%-D", date(2024, 7, 9)), @"7/9/24");
869 insta::assert_snapshot!(f("%3D", date(2024, 7, 9)), @"007/009/024");
870 insta::assert_snapshot!(f("%03D", date(2024, 7, 9)), @"007/009/024");
871 }
872
873 #[test]
874 fn ok_format_ampm() {
875 let f = |fmt: &str, time: Time| format(fmt, time).unwrap();
876
877 insta::assert_snapshot!(f("%H%P", time(9, 0, 0, 0)), @"09am");
878 insta::assert_snapshot!(f("%H%P", time(11, 0, 0, 0)), @"11am");
879 insta::assert_snapshot!(f("%H%P", time(23, 0, 0, 0)), @"23pm");
880 insta::assert_snapshot!(f("%H%P", time(0, 0, 0, 0)), @"00am");
881
882 insta::assert_snapshot!(f("%H%p", time(9, 0, 0, 0)), @"09AM");
883 insta::assert_snapshot!(f("%H%p", time(11, 0, 0, 0)), @"11AM");
884 insta::assert_snapshot!(f("%H%p", time(23, 0, 0, 0)), @"23PM");
885 insta::assert_snapshot!(f("%H%p", time(0, 0, 0, 0)), @"00AM");
886
887 insta::assert_snapshot!(f("%H%#p", time(9, 0, 0, 0)), @"09am");
888 }
889
890 #[test]
891 fn ok_format_clock() {
892 let f = |fmt: &str, time: Time| format(fmt, time).unwrap();
893
894 insta::assert_snapshot!(f("%R", time(23, 59, 8, 0)), @"23:59");
895 insta::assert_snapshot!(f("%T", time(23, 59, 8, 0)), @"23:59:08");
896 }
897
898 #[test]
899 fn ok_format_day() {
900 let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
901
902 insta::assert_snapshot!(f("%d", date(2024, 7, 9)), @"09");
903 insta::assert_snapshot!(f("%0d", date(2024, 7, 9)), @"09");
904 insta::assert_snapshot!(f("%-d", date(2024, 7, 9)), @"9");
905 insta::assert_snapshot!(f("%_d", date(2024, 7, 9)), @" 9");
906
907 insta::assert_snapshot!(f("%e", date(2024, 7, 9)), @" 9");
908 insta::assert_snapshot!(f("%0e", date(2024, 7, 9)), @"09");
909 insta::assert_snapshot!(f("%-e", date(2024, 7, 9)), @"9");
910 insta::assert_snapshot!(f("%_e", date(2024, 7, 9)), @" 9");
911 }
912
913 #[test]
914 fn ok_format_iso_date() {
915 let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
916
917 insta::assert_snapshot!(f("%F", date(2024, 7, 9)), @"2024-07-09");
918 insta::assert_snapshot!(f("%-F", date(2024, 7, 9)), @"2024-7-9");
919 insta::assert_snapshot!(f("%3F", date(2024, 7, 9)), @"2024-007-009");
920 insta::assert_snapshot!(f("%03F", date(2024, 7, 9)), @"2024-007-009");
921 }
922
923 #[test]
924 fn ok_format_hour() {
925 let f = |fmt: &str, time: Time| format(fmt, time).unwrap();
926
927 insta::assert_snapshot!(f("%H", time(9, 0, 0, 0)), @"09");
928 insta::assert_snapshot!(f("%H", time(11, 0, 0, 0)), @"11");
929 insta::assert_snapshot!(f("%H", time(23, 0, 0, 0)), @"23");
930 insta::assert_snapshot!(f("%H", time(0, 0, 0, 0)), @"00");
931
932 insta::assert_snapshot!(f("%I", time(9, 0, 0, 0)), @"09");
933 insta::assert_snapshot!(f("%I", time(11, 0, 0, 0)), @"11");
934 insta::assert_snapshot!(f("%I", time(23, 0, 0, 0)), @"11");
935 insta::assert_snapshot!(f("%I", time(0, 0, 0, 0)), @"12");
936
937 insta::assert_snapshot!(f("%k", time(9, 0, 0, 0)), @" 9");
938 insta::assert_snapshot!(f("%k", time(11, 0, 0, 0)), @"11");
939 insta::assert_snapshot!(f("%k", time(23, 0, 0, 0)), @"23");
940 insta::assert_snapshot!(f("%k", time(0, 0, 0, 0)), @" 0");
941
942 insta::assert_snapshot!(f("%l", time(9, 0, 0, 0)), @" 9");
943 insta::assert_snapshot!(f("%l", time(11, 0, 0, 0)), @"11");
944 insta::assert_snapshot!(f("%l", time(23, 0, 0, 0)), @"11");
945 insta::assert_snapshot!(f("%l", time(0, 0, 0, 0)), @"12");
946 }
947
948 #[test]
949 fn ok_format_minute() {
950 let f = |fmt: &str, time: Time| format(fmt, time).unwrap();
951
952 insta::assert_snapshot!(f("%M", time(0, 9, 0, 0)), @"09");
953 insta::assert_snapshot!(f("%M", time(0, 11, 0, 0)), @"11");
954 insta::assert_snapshot!(f("%M", time(0, 23, 0, 0)), @"23");
955 insta::assert_snapshot!(f("%M", time(0, 0, 0, 0)), @"00");
956 }
957
958 #[test]
959 fn ok_format_month() {
960 let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
961
962 insta::assert_snapshot!(f("%m", date(2024, 7, 14)), @"07");
963 insta::assert_snapshot!(f("%m", date(2024, 12, 14)), @"12");
964 insta::assert_snapshot!(f("%0m", date(2024, 7, 14)), @"07");
965 insta::assert_snapshot!(f("%0m", date(2024, 12, 14)), @"12");
966 insta::assert_snapshot!(f("%-m", date(2024, 7, 14)), @"7");
967 insta::assert_snapshot!(f("%-m", date(2024, 12, 14)), @"12");
968 insta::assert_snapshot!(f("%_m", date(2024, 7, 14)), @" 7");
969 insta::assert_snapshot!(f("%_m", date(2024, 12, 14)), @"12");
970 }
971
972 #[test]
973 fn ok_format_month_name() {
974 let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
975
976 insta::assert_snapshot!(f("%B", date(2024, 7, 14)), @"July");
977 insta::assert_snapshot!(f("%b", date(2024, 7, 14)), @"Jul");
978 insta::assert_snapshot!(f("%h", date(2024, 7, 14)), @"Jul");
979
980 insta::assert_snapshot!(f("%#B", date(2024, 7, 14)), @"July");
981 insta::assert_snapshot!(f("%^B", date(2024, 7, 14)), @"JULY");
982 }
983
984 #[test]
985 fn ok_format_offset() {
986 if crate::tz::db().is_definitively_empty() {
987 return;
988 }
989
990 let f = |fmt: &str, zdt: &Zoned| format(fmt, zdt).unwrap();
991
992 let zdt = date(2024, 7, 14)
993 .at(22, 24, 0, 0)
994 .in_tz("America/New_York")
995 .unwrap();
996 insta::assert_snapshot!(f("%z", &zdt), @"-0400");
997 insta::assert_snapshot!(f("%:z", &zdt), @"-04:00");
998
999 let zdt = zdt.checked_add(crate::Span::new().months(5)).unwrap();
1000 insta::assert_snapshot!(f("%z", &zdt), @"-0500");
1001 insta::assert_snapshot!(f("%:z", &zdt), @"-05:00");
1002 }
1003
1004 #[test]
1005 fn ok_format_second() {
1006 let f = |fmt: &str, time: Time| format(fmt, time).unwrap();
1007
1008 insta::assert_snapshot!(f("%S", time(0, 0, 9, 0)), @"09");
1009 insta::assert_snapshot!(f("%S", time(0, 0, 11, 0)), @"11");
1010 insta::assert_snapshot!(f("%S", time(0, 0, 23, 0)), @"23");
1011 insta::assert_snapshot!(f("%S", time(0, 0, 0, 0)), @"00");
1012 }
1013
1014 #[test]
1015 fn ok_format_subsec_nanosecond() {
1016 let f = |fmt: &str, time: Time| format(fmt, time).unwrap();
1017 let mk = |subsec| time(0, 0, 0, subsec);
1018
1019 insta::assert_snapshot!(f("%f", mk(123_000_000)), @"123");
1020 insta::assert_snapshot!(f("%f", mk(0)), @"0");
1021 insta::assert_snapshot!(f("%3f", mk(0)), @"000");
1022 insta::assert_snapshot!(f("%3f", mk(123_000_000)), @"123");
1023 insta::assert_snapshot!(f("%6f", mk(123_000_000)), @"123000");
1024 insta::assert_snapshot!(f("%9f", mk(123_000_000)), @"123000000");
1025 insta::assert_snapshot!(f("%255f", mk(123_000_000)), @"123000000");
1026
1027 insta::assert_snapshot!(f("%.f", mk(123_000_000)), @".123");
1028 insta::assert_snapshot!(f("%.f", mk(0)), @"");
1029 insta::assert_snapshot!(f("%3.f", mk(0)), @"");
1030 insta::assert_snapshot!(f("%.3f", mk(0)), @".000");
1031 insta::assert_snapshot!(f("%.3f", mk(123_000_000)), @".123");
1032 insta::assert_snapshot!(f("%.6f", mk(123_000_000)), @".123000");
1033 insta::assert_snapshot!(f("%.9f", mk(123_000_000)), @".123000000");
1034 insta::assert_snapshot!(f("%.255f", mk(123_000_000)), @".123000000");
1035
1036 insta::assert_snapshot!(f("%3f", mk(123_456_789)), @"123");
1037 insta::assert_snapshot!(f("%6f", mk(123_456_789)), @"123456");
1038 insta::assert_snapshot!(f("%9f", mk(123_456_789)), @"123456789");
1039
1040 insta::assert_snapshot!(f("%.0f", mk(123_456_789)), @"");
1041 insta::assert_snapshot!(f("%.3f", mk(123_456_789)), @".123");
1042 insta::assert_snapshot!(f("%.6f", mk(123_456_789)), @".123456");
1043 insta::assert_snapshot!(f("%.9f", mk(123_456_789)), @".123456789");
1044 }
1045
1046 #[test]
1047 fn ok_format_tzabbrev() {
1048 if crate::tz::db().is_definitively_empty() {
1049 return;
1050 }
1051
1052 let f = |fmt: &str, zdt: &Zoned| format(fmt, zdt).unwrap();
1053
1054 let zdt = date(2024, 7, 14)
1055 .at(22, 24, 0, 0)
1056 .in_tz("America/New_York")
1057 .unwrap();
1058 insta::assert_snapshot!(f("%Z", &zdt), @"EDT");
1059 insta::assert_snapshot!(f("%^Z", &zdt), @"EDT");
1060 insta::assert_snapshot!(f("%#Z", &zdt), @"edt");
1061
1062 let zdt = zdt.checked_add(crate::Span::new().months(5)).unwrap();
1063 insta::assert_snapshot!(f("%Z", &zdt), @"EST");
1064 }
1065
1066 #[test]
1067 fn ok_format_iana() {
1068 if crate::tz::db().is_definitively_empty() {
1069 return;
1070 }
1071
1072 let f = |fmt: &str, zdt: &Zoned| format(fmt, zdt).unwrap();
1073
1074 let zdt = date(2024, 7, 14)
1075 .at(22, 24, 0, 0)
1076 .in_tz("America/New_York")
1077 .unwrap();
1078 insta::assert_snapshot!(f("%Q", &zdt), @"America/New_York");
1079 insta::assert_snapshot!(f("%:Q", &zdt), @"America/New_York");
1080
1081 let zdt = date(2024, 7, 14)
1082 .at(22, 24, 0, 0)
1083 .to_zoned(crate::tz::offset(-4).to_time_zone())
1084 .unwrap();
1085 insta::assert_snapshot!(f("%Q", &zdt), @"-0400");
1086 insta::assert_snapshot!(f("%:Q", &zdt), @"-04:00");
1087
1088 let zdt = date(2024, 7, 14)
1089 .at(22, 24, 0, 0)
1090 .to_zoned(crate::tz::TimeZone::UTC)
1091 .unwrap();
1092 insta::assert_snapshot!(f("%Q", &zdt), @"UTC");
1093 insta::assert_snapshot!(f("%:Q", &zdt), @"UTC");
1094 }
1095
1096 #[test]
1097 fn ok_format_weekday_name() {
1098 let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1099
1100 insta::assert_snapshot!(f("%A", date(2024, 7, 14)), @"Sunday");
1101 insta::assert_snapshot!(f("%a", date(2024, 7, 14)), @"Sun");
1102
1103 insta::assert_snapshot!(f("%#A", date(2024, 7, 14)), @"Sunday");
1104 insta::assert_snapshot!(f("%^A", date(2024, 7, 14)), @"SUNDAY");
1105
1106 insta::assert_snapshot!(f("%u", date(2024, 7, 14)), @"7");
1107 insta::assert_snapshot!(f("%w", date(2024, 7, 14)), @"0");
1108 }
1109
1110 #[test]
1111 fn ok_format_year() {
1112 let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1113
1114 insta::assert_snapshot!(f("%Y", date(2024, 7, 14)), @"2024");
1115 insta::assert_snapshot!(f("%Y", date(24, 7, 14)), @"0024");
1116 insta::assert_snapshot!(f("%Y", date(-24, 7, 14)), @"-0024");
1117
1118 insta::assert_snapshot!(f("%C", date(2024, 7, 14)), @"20");
1119 insta::assert_snapshot!(f("%C", date(1815, 7, 14)), @"18");
1120 insta::assert_snapshot!(f("%C", date(915, 7, 14)), @"9");
1121 insta::assert_snapshot!(f("%C", date(1, 7, 14)), @"0");
1122 insta::assert_snapshot!(f("%C", date(0, 7, 14)), @"0");
1123 insta::assert_snapshot!(f("%C", date(-1, 7, 14)), @"0");
1124 insta::assert_snapshot!(f("%C", date(-2024, 7, 14)), @"-20");
1125 insta::assert_snapshot!(f("%C", date(-1815, 7, 14)), @"-18");
1126 insta::assert_snapshot!(f("%C", date(-915, 7, 14)), @"-9");
1127 }
1128
1129 #[test]
1130 fn ok_format_year_2digit() {
1131 let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1132
1133 insta::assert_snapshot!(f("%y", date(2024, 7, 14)), @"24");
1134 insta::assert_snapshot!(f("%y", date(2001, 7, 14)), @"01");
1135 insta::assert_snapshot!(f("%-y", date(2001, 7, 14)), @"1");
1136 insta::assert_snapshot!(f("%5y", date(2001, 7, 14)), @"00001");
1137 insta::assert_snapshot!(f("%-5y", date(2001, 7, 14)), @"1");
1138 insta::assert_snapshot!(f("%05y", date(2001, 7, 14)), @"00001");
1139 insta::assert_snapshot!(f("%_y", date(2001, 7, 14)), @" 1");
1140 insta::assert_snapshot!(f("%_5y", date(2001, 7, 14)), @" 1");
1141 }
1142
1143 #[test]
1144 fn ok_format_iso_week_year() {
1145 let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1146
1147 insta::assert_snapshot!(f("%G", date(2019, 11, 30)), @"2019");
1148 insta::assert_snapshot!(f("%G", date(19, 11, 30)), @"0019");
1149 insta::assert_snapshot!(f("%G", date(-19, 11, 30)), @"-0019");
1150
1151 insta::assert_snapshot!(f("%G", date(2019, 12, 30)), @"2020");
1153 }
1154
1155 #[test]
1156 fn ok_format_week_num() {
1157 let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1158
1159 insta::assert_snapshot!(f("%U", date(2025, 1, 4)), @"00");
1160 insta::assert_snapshot!(f("%U", date(2025, 1, 5)), @"01");
1161
1162 insta::assert_snapshot!(f("%W", date(2025, 1, 5)), @"00");
1163 insta::assert_snapshot!(f("%W", date(2025, 1, 6)), @"01");
1164 }
1165
1166 #[test]
1167 fn ok_format_timestamp() {
1168 let f = |fmt: &str, ts: Timestamp| format(fmt, ts).unwrap();
1169
1170 let ts = "1970-01-01T00:00Z".parse().unwrap();
1171 insta::assert_snapshot!(f("%s", ts), @"0");
1172 insta::assert_snapshot!(f("%3s", ts), @" 0");
1173 insta::assert_snapshot!(f("%03s", ts), @"000");
1174
1175 let ts = "2025-01-20T13:09-05[US/Eastern]".parse().unwrap();
1176 insta::assert_snapshot!(f("%s", ts), @"1737396540");
1177 }
1178
1179 #[test]
1180 fn err_format_subsec_nanosecond() {
1181 let f = |fmt: &str, time: Time| format(fmt, time).unwrap_err();
1182 let mk = |subsec| time(0, 0, 0, subsec);
1183
1184 insta::assert_snapshot!(
1185 f("%00f", mk(123_456_789)),
1186 @"strftime formatting failed: %f failed: zero precision with %f is not allowed",
1187 );
1188 }
1189
1190 #[test]
1191 fn err_format_timestamp() {
1192 let f = |fmt: &str, dt: DateTime| format(fmt, dt).unwrap_err();
1193
1194 let dt = date(2025, 1, 20).at(13, 9, 0, 0);
1195 insta::assert_snapshot!(
1196 f("%s", dt),
1197 @"strftime formatting failed: %s failed: requires instant (a date, time and offset) to format Unix timestamp",
1198 );
1199 }
1200}