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, Config, Custom, Extension,
7 Flag,
8 },
9 util::{DecimalFormatter, FractionalFormatter},
10 Write, WriteExt,
11 },
12 tz::Offset,
13 util::{escape, t::C, utf8},
14 Error,
15};
16
17pub(super) struct Formatter<'c, 'f, 't, 'w, W, L> {
18 pub(super) config: &'c Config<L>,
19 pub(super) fmt: &'f [u8],
20 pub(super) tm: &'t BrokenDownTime,
21 pub(super) wtr: &'w mut W,
22}
23
24impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
25 pub(super) fn format(&mut self) -> Result<(), Error> {
26 while !self.fmt.is_empty() {
27 if self.f() != b'%' {
28 if self.f().is_ascii() {
29 self.wtr.write_char(char::from(self.f()))?;
30 self.bump_fmt();
31 } else {
32 let ch = self.utf8_decode_and_bump()?;
33 self.wtr.write_char(ch)?;
34 }
35 continue;
36 }
37 if !self.bump_fmt() {
38 if self.config.lenient {
39 self.wtr.write_str("%")?;
40 break;
41 }
42 return Err(err!(
43 "invalid format string, expected byte after '%', \
44 but found end of format string",
45 ));
46 }
47 let orig = self.fmt;
48 if let Err(err) = self.format_one() {
49 if !self.config.lenient {
50 return Err(err);
51 }
52 self.wtr.write_str("%")?;
56 self.fmt = orig;
58 }
59 }
60 Ok(())
61 }
62
63 fn format_one(&mut self) -> Result<(), Error> {
64 let ext = self.parse_extension()?;
66 match self.f() {
67 b'%' => self.wtr.write_str("%").context("%% failed")?,
68 b'A' => self.fmt_weekday_full(&ext).context("%A failed")?,
69 b'a' => self.fmt_weekday_abbrev(&ext).context("%a failed")?,
70 b'B' => self.fmt_month_full(&ext).context("%B failed")?,
71 b'b' => self.fmt_month_abbrev(&ext).context("%b failed")?,
72 b'C' => self.fmt_century(&ext).context("%C failed")?,
73 b'c' => self.fmt_datetime(&ext).context("%c failed")?,
74 b'D' => self.fmt_american_date(&ext).context("%D failed")?,
75 b'd' => self.fmt_day_zero(&ext).context("%d failed")?,
76 b'e' => self.fmt_day_space(&ext).context("%e failed")?,
77 b'F' => self.fmt_iso_date(&ext).context("%F failed")?,
78 b'f' => self.fmt_fractional(&ext).context("%f failed")?,
79 b'G' => self.fmt_iso_week_year(&ext).context("%G failed")?,
80 b'g' => self.fmt_iso_week_year2(&ext).context("%g failed")?,
81 b'H' => self.fmt_hour24_zero(&ext).context("%H failed")?,
82 b'h' => self.fmt_month_abbrev(&ext).context("%b failed")?,
83 b'I' => self.fmt_hour12_zero(&ext).context("%H failed")?,
84 b'j' => self.fmt_day_of_year(&ext).context("%j failed")?,
85 b'k' => self.fmt_hour24_space(&ext).context("%k failed")?,
86 b'l' => self.fmt_hour12_space(&ext).context("%l failed")?,
87 b'M' => self.fmt_minute(&ext).context("%M failed")?,
88 b'm' => self.fmt_month(&ext).context("%m failed")?,
89 b'N' => self.fmt_nanoseconds(&ext).context("%N failed")?,
90 b'n' => self.fmt_literal("\n").context("%n failed")?,
91 b'P' => self.fmt_ampm_lower(&ext).context("%P failed")?,
92 b'p' => self.fmt_ampm_upper(&ext).context("%p failed")?,
93 b'Q' => match ext.colons {
94 0 => self.fmt_iana_nocolon().context("%Q failed")?,
95 1 => self.fmt_iana_colon().context("%:Q failed")?,
96 _ => {
97 return Err(err!(
98 "invalid number of `:` in `%Q` directive"
99 ))
100 }
101 },
102 b'q' => self.fmt_quarter(&ext).context("%q failed")?,
103 b'R' => self.fmt_clock_nosecs(&ext).context("%R failed")?,
104 b'r' => self.fmt_12hour_time(&ext).context("%r failed")?,
105 b'S' => self.fmt_second(&ext).context("%S failed")?,
106 b's' => self.fmt_timestamp(&ext).context("%s failed")?,
107 b'T' => self.fmt_clock_secs(&ext).context("%T failed")?,
108 b't' => self.fmt_literal("\t").context("%t failed")?,
109 b'U' => self.fmt_week_sun(&ext).context("%U failed")?,
110 b'u' => self.fmt_weekday_mon(&ext).context("%u failed")?,
111 b'V' => self.fmt_week_iso(&ext).context("%V failed")?,
112 b'W' => self.fmt_week_mon(&ext).context("%W failed")?,
113 b'w' => self.fmt_weekday_sun(&ext).context("%w failed")?,
114 b'X' => self.fmt_time(&ext).context("%X failed")?,
115 b'x' => self.fmt_date(&ext).context("%x failed")?,
116 b'Y' => self.fmt_year(&ext).context("%Y failed")?,
117 b'y' => self.fmt_year2(&ext).context("%y failed")?,
118 b'Z' => self.fmt_tzabbrev(&ext).context("%Z failed")?,
119 b'z' => match ext.colons {
120 0 => self.fmt_offset_nocolon().context("%z failed")?,
121 1 => self.fmt_offset_colon().context("%:z failed")?,
122 2 => self.fmt_offset_colon2().context("%::z failed")?,
123 3 => self.fmt_offset_colon3().context("%:::z failed")?,
124 _ => {
125 return Err(err!(
126 "invalid number of `:` in `%z` directive"
127 ))
128 }
129 },
130 b'.' => {
131 if !self.bump_fmt() {
132 return Err(err!(
133 "invalid format string, expected directive \
134 after '%.'",
135 ));
136 }
137 let ext = Extension { width: self.parse_width()?, ..ext };
140 match self.f() {
141 b'f' => {
142 self.fmt_dot_fractional(&ext).context("%.f failed")?
143 }
144 unk => {
145 return Err(err!(
146 "found unrecognized directive %{unk} \
147 following %.",
148 unk = escape::Byte(unk),
149 ));
150 }
151 }
152 }
153 unk => {
154 return Err(err!(
155 "found unrecognized specifier directive %{unk}",
156 unk = escape::Byte(unk),
157 ));
158 }
159 }
160 self.bump_fmt();
161 Ok(())
162 }
163
164 fn f(&self) -> u8 {
170 self.fmt[0]
171 }
172
173 fn bump_fmt(&mut self) -> bool {
178 self.fmt = &self.fmt[1..];
179 !self.fmt.is_empty()
180 }
181
182 #[cold]
197 #[inline(never)]
198 fn utf8_decode_and_bump(&mut self) -> Result<char, Error> {
199 match utf8::decode(self.fmt).expect("non-empty fmt") {
200 Ok(ch) => {
201 self.fmt = &self.fmt[ch.len_utf8()..];
202 return Ok(ch);
203 }
204 Err(invalid) => Err(err!(
205 "found invalid UTF-8 byte {byte:?} in format \
206 string (format strings must be valid UTF-8)",
207 byte = escape::Byte(invalid),
208 )),
209 }
210 }
211
212 #[cfg_attr(feature = "perf-inline", inline(always))]
216 fn parse_extension(&mut self) -> Result<Extension, Error> {
217 let flag = self.parse_flag()?;
218 let width = self.parse_width()?;
219 let colons = self.parse_colons();
220 Ok(Extension { flag, width, colons })
221 }
222
223 #[cfg_attr(feature = "perf-inline", inline(always))]
226 fn parse_flag(&mut self) -> Result<Option<Flag>, Error> {
227 let (flag, fmt) = Extension::parse_flag(self.fmt)?;
228 self.fmt = fmt;
229 Ok(flag)
230 }
231
232 #[cfg_attr(feature = "perf-inline", inline(always))]
242 fn parse_width(&mut self) -> Result<Option<u8>, Error> {
243 let (width, fmt) = Extension::parse_width(self.fmt)?;
244 self.fmt = fmt;
245 Ok(width)
246 }
247
248 #[cfg_attr(feature = "perf-inline", inline(always))]
251 fn parse_colons(&mut self) -> u8 {
252 let (colons, fmt) = Extension::parse_colons(self.fmt);
253 self.fmt = fmt;
254 colons
255 }
256
257 fn fmt_ampm_lower(&mut self, ext: &Extension) -> Result<(), Error> {
264 let hour = self
265 .tm
266 .hour
267 .ok_or_else(|| err!("requires time to format AM/PM"))?
268 .get();
269 ext.write_str(
270 Case::AsIs,
271 if hour < 12 { "am" } else { "pm" },
272 self.wtr,
273 )
274 }
275
276 fn fmt_ampm_upper(&mut self, ext: &Extension) -> Result<(), Error> {
278 let hour = self
279 .tm
280 .hour
281 .ok_or_else(|| err!("requires time to format AM/PM"))?
282 .get();
283 let s = if matches!(ext.flag, Some(Flag::Swapcase)) {
285 if hour < 12 {
286 "am"
287 } else {
288 "pm"
289 }
290 } else {
291 if hour < 12 {
292 "AM"
293 } else {
294 "PM"
295 }
296 };
297 self.wtr.write_str(s)
298 }
299
300 fn fmt_american_date(&mut self, ext: &Extension) -> Result<(), Error> {
302 self.fmt_month(ext)?;
303 self.wtr.write_char('/')?;
304 self.fmt_day_zero(ext)?;
305 self.wtr.write_char('/')?;
306 self.fmt_year2(ext)?;
307 Ok(())
308 }
309
310 fn fmt_clock_nosecs(&mut self, ext: &Extension) -> Result<(), Error> {
312 self.fmt_hour24_zero(ext)?;
313 self.wtr.write_char(':')?;
314 self.fmt_minute(ext)?;
315 Ok(())
316 }
317
318 fn fmt_clock_secs(&mut self, ext: &Extension) -> Result<(), Error> {
320 self.fmt_hour24_zero(ext)?;
321 self.wtr.write_char(':')?;
322 self.fmt_minute(ext)?;
323 self.wtr.write_char(':')?;
324 self.fmt_second(ext)?;
325 Ok(())
326 }
327
328 fn fmt_day_zero(&mut self, ext: &Extension) -> Result<(), Error> {
330 let day = self
331 .tm
332 .day
333 .or_else(|| self.tm.to_date().ok().map(|d| d.day_ranged()))
334 .ok_or_else(|| err!("requires date to format day"))?
335 .get();
336 ext.write_int(b'0', Some(2), day, self.wtr)
337 }
338
339 fn fmt_day_space(&mut self, ext: &Extension) -> Result<(), Error> {
341 let day = self
342 .tm
343 .day
344 .or_else(|| self.tm.to_date().ok().map(|d| d.day_ranged()))
345 .ok_or_else(|| err!("requires date to format day"))?
346 .get();
347 ext.write_int(b' ', Some(2), day, self.wtr)
348 }
349
350 fn fmt_hour12_zero(&mut self, ext: &Extension) -> Result<(), Error> {
352 let mut hour = self
353 .tm
354 .hour
355 .ok_or_else(|| err!("requires time to format hour"))?
356 .get();
357 if hour == 0 {
358 hour = 12;
359 } else if hour > 12 {
360 hour -= 12;
361 }
362 ext.write_int(b'0', Some(2), hour, self.wtr)
363 }
364
365 fn fmt_hour24_zero(&mut self, ext: &Extension) -> Result<(), Error> {
367 let hour = self
368 .tm
369 .hour
370 .ok_or_else(|| err!("requires time to format hour"))?
371 .get();
372 ext.write_int(b'0', Some(2), hour, self.wtr)
373 }
374
375 fn fmt_hour12_space(&mut self, ext: &Extension) -> Result<(), Error> {
377 let mut hour = self
378 .tm
379 .hour
380 .ok_or_else(|| err!("requires time to format hour"))?
381 .get();
382 if hour == 0 {
383 hour = 12;
384 } else if hour > 12 {
385 hour -= 12;
386 }
387 ext.write_int(b' ', Some(2), hour, self.wtr)
388 }
389
390 fn fmt_hour24_space(&mut self, ext: &Extension) -> Result<(), Error> {
392 let hour = self
393 .tm
394 .hour
395 .ok_or_else(|| err!("requires time to format hour"))?
396 .get();
397 ext.write_int(b' ', Some(2), hour, self.wtr)
398 }
399
400 fn fmt_iso_date(&mut self, ext: &Extension) -> Result<(), Error> {
402 self.fmt_year(ext)?;
403 self.wtr.write_char('-')?;
404 self.fmt_month(ext)?;
405 self.wtr.write_char('-')?;
406 self.fmt_day_zero(ext)?;
407 Ok(())
408 }
409
410 fn fmt_minute(&mut self, ext: &Extension) -> Result<(), Error> {
412 let minute = self
413 .tm
414 .minute
415 .ok_or_else(|| err!("requires time to format minute"))?
416 .get();
417 ext.write_int(b'0', Some(2), minute, self.wtr)
418 }
419
420 fn fmt_month(&mut self, ext: &Extension) -> Result<(), Error> {
422 let month = self
423 .tm
424 .month
425 .or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged()))
426 .ok_or_else(|| err!("requires date to format month"))?
427 .get();
428 ext.write_int(b'0', Some(2), month, self.wtr)
429 }
430
431 fn fmt_month_full(&mut self, ext: &Extension) -> Result<(), Error> {
433 let month = self
434 .tm
435 .month
436 .or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged()))
437 .ok_or_else(|| err!("requires date to format month"))?;
438 ext.write_str(Case::AsIs, month_name_full(month), self.wtr)
439 }
440
441 fn fmt_month_abbrev(&mut self, ext: &Extension) -> Result<(), Error> {
443 let month = self
444 .tm
445 .month
446 .or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged()))
447 .ok_or_else(|| err!("requires date to format month"))?;
448 ext.write_str(Case::AsIs, month_name_abbrev(month), self.wtr)
449 }
450
451 fn fmt_iana_nocolon(&mut self) -> Result<(), Error> {
453 let Some(iana) = self.tm.iana_time_zone() else {
454 let offset = self.tm.offset.ok_or_else(|| {
455 err!(
456 "requires IANA time zone identifier or time \
457 zone offset, but none were present"
458 )
459 })?;
460 return write_offset(offset, false, true, false, &mut self.wtr);
461 };
462 self.wtr.write_str(iana)?;
463 Ok(())
464 }
465
466 fn fmt_iana_colon(&mut self) -> Result<(), Error> {
468 let Some(iana) = self.tm.iana_time_zone() else {
469 let offset = self.tm.offset.ok_or_else(|| {
470 err!(
471 "requires IANA time zone identifier or time \
472 zone offset, but none were present"
473 )
474 })?;
475 return write_offset(offset, true, true, false, &mut self.wtr);
476 };
477 self.wtr.write_str(iana)?;
478 Ok(())
479 }
480
481 fn fmt_offset_nocolon(&mut self) -> Result<(), Error> {
483 let offset = self.tm.offset.ok_or_else(|| {
484 err!("requires offset to format time zone offset")
485 })?;
486 write_offset(offset, false, true, false, self.wtr)
487 }
488
489 fn fmt_offset_colon(&mut self) -> Result<(), Error> {
491 let offset = self.tm.offset.ok_or_else(|| {
492 err!("requires offset to format time zone offset")
493 })?;
494 write_offset(offset, true, true, false, self.wtr)
495 }
496
497 fn fmt_offset_colon2(&mut self) -> Result<(), Error> {
499 let offset = self.tm.offset.ok_or_else(|| {
500 err!("requires offset to format time zone offset")
501 })?;
502 write_offset(offset, true, true, true, self.wtr)
503 }
504
505 fn fmt_offset_colon3(&mut self) -> Result<(), Error> {
507 let offset = self.tm.offset.ok_or_else(|| {
508 err!("requires offset to format time zone offset")
509 })?;
510 write_offset(offset, true, false, false, self.wtr)
511 }
512
513 fn fmt_second(&mut self, ext: &Extension) -> Result<(), Error> {
515 let second = self
516 .tm
517 .second
518 .ok_or_else(|| err!("requires time to format second"))?
519 .get();
520 ext.write_int(b'0', Some(2), second, self.wtr)
521 }
522
523 fn fmt_timestamp(&mut self, ext: &Extension) -> Result<(), Error> {
525 let timestamp = self.tm.to_timestamp().map_err(|_| {
526 err!(
527 "requires instant (a date, time and offset) \
528 to format Unix timestamp",
529 )
530 })?;
531 ext.write_int(b' ', None, timestamp.as_second(), self.wtr)
532 }
533
534 fn fmt_fractional(&mut self, ext: &Extension) -> Result<(), Error> {
536 let subsec = self.tm.subsec.ok_or_else(|| {
537 err!("requires time to format subsecond nanoseconds")
538 })?;
539 if ext.width == Some(0) {
546 return Err(err!("zero precision with %f is not allowed"));
547 }
548 if subsec == C(0) && ext.width.is_none() {
549 self.wtr.write_str("0")?;
550 return Ok(());
551 }
552 ext.write_fractional_seconds(subsec, self.wtr)?;
553 Ok(())
554 }
555
556 fn fmt_dot_fractional(&mut self, ext: &Extension) -> Result<(), Error> {
558 let Some(subsec) = self.tm.subsec else { return Ok(()) };
559 if subsec == C(0) && ext.width.is_none() || ext.width == Some(0) {
560 return Ok(());
561 }
562 ext.write_str(Case::AsIs, ".", self.wtr)?;
563 ext.write_fractional_seconds(subsec, self.wtr)?;
564 Ok(())
565 }
566
567 fn fmt_nanoseconds(&mut self, ext: &Extension) -> Result<(), Error> {
569 let subsec = self.tm.subsec.ok_or_else(|| {
570 err!("requires time to format subsecond nanoseconds")
571 })?;
572 if ext.width == Some(0) {
573 return Err(err!("zero precision with %N is not allowed"));
574 }
575 if ext.width.is_none() {
578 let formatter = FractionalFormatter::new().precision(Some(9));
579 return self.wtr.write_fraction(&formatter, subsec);
580 }
581 ext.write_fractional_seconds(subsec, self.wtr)?;
582 Ok(())
583 }
584
585 fn fmt_tzabbrev(&mut self, ext: &Extension) -> Result<(), Error> {
587 let tz =
588 self.tm.tz.as_ref().ok_or_else(|| {
589 err!("requires time zone in broken down time")
590 })?;
591 let ts = self
592 .tm
593 .timestamp
594 .ok_or_else(|| err!("requires timestamp in broken down time"))?;
595 let oinfo = tz.to_offset_info(ts);
596 ext.write_str(Case::Upper, oinfo.abbreviation(), self.wtr)
597 }
598
599 fn fmt_weekday_full(&mut self, ext: &Extension) -> Result<(), Error> {
601 let weekday = self
602 .tm
603 .weekday
604 .or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
605 .ok_or_else(|| err!("requires date to format weekday"))?;
606 ext.write_str(Case::AsIs, weekday_name_full(weekday), self.wtr)
607 }
608
609 fn fmt_weekday_abbrev(&mut self, ext: &Extension) -> Result<(), Error> {
611 let weekday = self
612 .tm
613 .weekday
614 .or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
615 .ok_or_else(|| err!("requires date to format weekday"))?;
616 ext.write_str(Case::AsIs, weekday_name_abbrev(weekday), self.wtr)
617 }
618
619 fn fmt_weekday_mon(&mut self, ext: &Extension) -> Result<(), Error> {
621 let weekday = self
622 .tm
623 .weekday
624 .or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
625 .ok_or_else(|| err!("requires date to format weekday number"))?;
626 ext.write_int(b' ', None, weekday.to_monday_one_offset(), self.wtr)
627 }
628
629 fn fmt_weekday_sun(&mut self, ext: &Extension) -> Result<(), Error> {
631 let weekday = self
632 .tm
633 .weekday
634 .or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
635 .ok_or_else(|| err!("requires date to format weekday number"))?;
636 ext.write_int(b' ', None, weekday.to_sunday_zero_offset(), self.wtr)
637 }
638
639 fn fmt_week_sun(&mut self, ext: &Extension) -> Result<(), Error> {
641 if let Some(weeknum) = self.tm.week_sun {
643 return ext.write_int(b'0', Some(2), weeknum, self.wtr);
644 }
645 let day = self
646 .tm
647 .day_of_year
648 .map(|day| day.get())
649 .or_else(|| self.tm.to_date().ok().map(|d| d.day_of_year()))
650 .ok_or_else(|| {
651 err!("requires date to format Sunday-based week number")
652 })?;
653 let weekday = self
654 .tm
655 .weekday
656 .or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
657 .ok_or_else(|| {
658 err!("requires date to format Sunday-based week number")
659 })?
660 .to_sunday_zero_offset();
661 let weeknum = (day + 6 - i16::from(weekday)) / 7;
667 ext.write_int(b'0', Some(2), weeknum, self.wtr)
668 }
669
670 fn fmt_week_iso(&mut self, ext: &Extension) -> Result<(), Error> {
672 let weeknum = self
673 .tm
674 .iso_week
675 .or_else(|| {
676 self.tm.to_date().ok().map(|d| d.iso_week_date().week_ranged())
677 })
678 .ok_or_else(|| {
679 err!("requires date to format ISO 8601 week number")
680 })?;
681 ext.write_int(b'0', Some(2), weeknum, self.wtr)
682 }
683
684 fn fmt_week_mon(&mut self, ext: &Extension) -> Result<(), Error> {
686 if let Some(weeknum) = self.tm.week_mon {
688 return ext.write_int(b'0', Some(2), weeknum, self.wtr);
689 }
690 let day = self
691 .tm
692 .day_of_year
693 .map(|day| day.get())
694 .or_else(|| self.tm.to_date().ok().map(|d| d.day_of_year()))
695 .ok_or_else(|| {
696 err!("requires date to format Monday-based week number")
697 })?;
698 let weekday = self
699 .tm
700 .weekday
701 .or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
702 .ok_or_else(|| {
703 err!("requires date to format Monday-based week number")
704 })?
705 .to_sunday_zero_offset();
706 let weeknum = (day + 6 - ((i16::from(weekday) + 6) % 7)) / 7;
712 ext.write_int(b'0', Some(2), weeknum, self.wtr)
713 }
714
715 fn fmt_year(&mut self, ext: &Extension) -> Result<(), Error> {
717 let year = self
718 .tm
719 .year
720 .or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged()))
721 .ok_or_else(|| err!("requires date to format year"))?
722 .get();
723 ext.write_int(b'0', Some(4), year, self.wtr)
724 }
725
726 fn fmt_year2(&mut self, ext: &Extension) -> Result<(), Error> {
728 let year = self
729 .tm
730 .year
731 .or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged()))
732 .ok_or_else(|| err!("requires date to format year (2-digit)"))?
733 .get();
734 if !(1969 <= year && year <= 2068) {
735 return Err(err!(
736 "formatting a 2-digit year requires that it be in \
737 the inclusive range 1969 to 2068, but got {year}",
738 ));
739 }
740 let year = year % 100;
741 ext.write_int(b'0', Some(2), year, self.wtr)
742 }
743
744 fn fmt_century(&mut self, ext: &Extension) -> Result<(), Error> {
746 let year = self
747 .tm
748 .year
749 .or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged()))
750 .ok_or_else(|| err!("requires date to format century (2-digit)"))?
751 .get();
752 let century = year / 100;
753 ext.write_int(b' ', None, century, self.wtr)
754 }
755
756 fn fmt_iso_week_year(&mut self, ext: &Extension) -> Result<(), Error> {
758 let year = self
759 .tm
760 .iso_week_year
761 .or_else(|| {
762 self.tm.to_date().ok().map(|d| d.iso_week_date().year_ranged())
763 })
764 .ok_or_else(|| {
765 err!("requires date to format ISO 8601 week-based year")
766 })?
767 .get();
768 ext.write_int(b'0', Some(4), year, self.wtr)
769 }
770
771 fn fmt_iso_week_year2(&mut self, ext: &Extension) -> Result<(), Error> {
773 let year = self
774 .tm
775 .iso_week_year
776 .or_else(|| {
777 self.tm.to_date().ok().map(|d| d.iso_week_date().year_ranged())
778 })
779 .ok_or_else(|| {
780 err!(
781 "requires date to format \
782 ISO 8601 week-based year (2-digit)"
783 )
784 })?
785 .get();
786 if !(1969 <= year && year <= 2068) {
787 return Err(err!(
788 "formatting a 2-digit ISO 8601 week-based year \
789 requires that it be in \
790 the inclusive range 1969 to 2068, but got {year}",
791 ));
792 }
793 let year = year % 100;
794 ext.write_int(b'0', Some(2), year, self.wtr)
795 }
796
797 fn fmt_quarter(&mut self, ext: &Extension) -> Result<(), Error> {
799 let month = self
800 .tm
801 .month
802 .or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged()))
803 .ok_or_else(|| err!("requires date to format quarter"))?
804 .get();
805 let quarter = match month {
806 1..=3 => 1,
807 4..=6 => 2,
808 7..=9 => 3,
809 10..=12 => 4,
810 _ => unreachable!(),
811 };
812 ext.write_int(b'0', None, quarter, self.wtr)
813 }
814
815 fn fmt_day_of_year(&mut self, ext: &Extension) -> Result<(), Error> {
817 let day = self
818 .tm
819 .day_of_year
820 .map(|day| day.get())
821 .or_else(|| self.tm.to_date().ok().map(|d| d.day_of_year()))
822 .ok_or_else(|| err!("requires date to format day of year"))?;
823 ext.write_int(b'0', Some(3), day, self.wtr)
824 }
825
826 fn fmt_literal(&mut self, literal: &str) -> Result<(), Error> {
828 self.wtr.write_str(literal)
829 }
830
831 fn fmt_datetime(&mut self, ext: &Extension) -> Result<(), Error> {
833 self.config.custom.format_datetime(self.config, ext, self.tm, self.wtr)
834 }
835
836 fn fmt_date(&mut self, ext: &Extension) -> Result<(), Error> {
838 self.config.custom.format_date(self.config, ext, self.tm, self.wtr)
839 }
840
841 fn fmt_time(&mut self, ext: &Extension) -> Result<(), Error> {
843 self.config.custom.format_time(self.config, ext, self.tm, self.wtr)
844 }
845
846 fn fmt_12hour_time(&mut self, ext: &Extension) -> Result<(), Error> {
848 self.config.custom.format_12hour_time(
849 self.config,
850 ext,
851 self.tm,
852 self.wtr,
853 )
854 }
855}
856
857fn write_offset<W: Write>(
869 offset: Offset,
870 colon: bool,
871 minute: bool,
872 second: bool,
873 wtr: &mut W,
874) -> Result<(), Error> {
875 static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2);
876
877 let hours = offset.part_hours_ranged().abs().get();
878 let minutes = offset.part_minutes_ranged().abs().get();
879 let seconds = offset.part_seconds_ranged().abs().get();
880
881 wtr.write_str(if offset.is_negative() { "-" } else { "+" })?;
882 wtr.write_int(&FMT_TWO, hours)?;
883 if minute || minutes != 0 || seconds != 0 {
884 if colon {
885 wtr.write_str(":")?;
886 }
887 wtr.write_int(&FMT_TWO, minutes)?;
888 if second || seconds != 0 {
889 if colon {
890 wtr.write_str(":")?;
891 }
892 wtr.write_int(&FMT_TWO, seconds)?;
893 }
894 }
895 Ok(())
896}
897
898impl Extension {
899 #[cfg_attr(feature = "perf-inline", inline(always))]
902 fn write_str<W: Write>(
903 &self,
904 default: Case,
905 string: &str,
906 wtr: &mut W,
907 ) -> Result<(), Error> {
908 if self.flag.is_none() && matches!(default, Case::AsIs) {
909 return wtr.write_str(string);
910 }
911 self.write_str_cold(default, string, wtr)
912 }
913
914 #[cold]
915 #[inline(never)]
916 fn write_str_cold<W: Write>(
917 &self,
918 default: Case,
919 string: &str,
920 wtr: &mut W,
921 ) -> Result<(), Error> {
922 let case = match self.flag {
923 Some(Flag::Uppercase) => Case::Upper,
924 Some(Flag::Swapcase) => default.swap(),
925 _ => default,
926 };
927 match case {
928 Case::AsIs => {
929 wtr.write_str(string)?;
930 }
931 Case::Upper => {
932 for ch in string.chars() {
933 for ch in ch.to_uppercase() {
934 wtr.write_char(ch)?;
935 }
936 }
937 }
938 Case::Lower => {
939 for ch in string.chars() {
940 for ch in ch.to_lowercase() {
941 wtr.write_char(ch)?;
942 }
943 }
944 }
945 }
946 Ok(())
947 }
948
949 #[cfg_attr(feature = "perf-inline", inline(always))]
952 fn write_int<W: Write>(
953 &self,
954 pad_byte: u8,
955 pad_width: Option<u8>,
956 number: impl Into<i64>,
957 wtr: &mut W,
958 ) -> Result<(), Error> {
959 let number = number.into();
960 let pad_byte = match self.flag {
961 Some(Flag::PadZero) => b'0',
962 Some(Flag::PadSpace) => b' ',
963 _ => pad_byte,
964 };
965 let pad_width = if matches!(self.flag, Some(Flag::NoPad)) {
966 None
967 } else {
968 self.width.or(pad_width)
969 };
970
971 let mut formatter = DecimalFormatter::new().padding_byte(pad_byte);
972 if let Some(width) = pad_width {
973 formatter = formatter.padding(width);
974 }
975 wtr.write_int(&formatter, number)
976 }
977
978 fn write_fractional_seconds<W: Write>(
983 &self,
984 number: impl Into<i64>,
985 wtr: &mut W,
986 ) -> Result<(), Error> {
987 let number = number.into();
988
989 let formatter = FractionalFormatter::new().precision(self.width);
990 wtr.write_fraction(&formatter, number)
991 }
992}
993
994#[derive(Clone, Copy, Debug)]
996enum Case {
997 AsIs,
998 Upper,
999 Lower,
1000}
1001
1002impl Case {
1003 fn swap(self) -> Case {
1005 match self {
1006 Case::AsIs => Case::AsIs,
1007 Case::Upper => Case::Lower,
1008 Case::Lower => Case::Upper,
1009 }
1010 }
1011}
1012
1013#[cfg(feature = "alloc")]
1014#[cfg(test)]
1015mod tests {
1016 use crate::{
1017 civil::{date, time, Date, DateTime, Time},
1018 fmt::strtime::{format, BrokenDownTime, Config, PosixCustom},
1019 tz::Offset,
1020 Timestamp, Zoned,
1021 };
1022
1023 #[test]
1024 fn ok_format_american_date() {
1025 let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1026
1027 insta::assert_snapshot!(f("%D", date(2024, 7, 9)), @"07/09/24");
1028 insta::assert_snapshot!(f("%-D", date(2024, 7, 9)), @"7/9/24");
1029 insta::assert_snapshot!(f("%3D", date(2024, 7, 9)), @"007/009/024");
1030 insta::assert_snapshot!(f("%03D", date(2024, 7, 9)), @"007/009/024");
1031 }
1032
1033 #[test]
1034 fn ok_format_ampm() {
1035 let f = |fmt: &str, time: Time| format(fmt, time).unwrap();
1036
1037 insta::assert_snapshot!(f("%H%P", time(9, 0, 0, 0)), @"09am");
1038 insta::assert_snapshot!(f("%H%P", time(11, 0, 0, 0)), @"11am");
1039 insta::assert_snapshot!(f("%H%P", time(23, 0, 0, 0)), @"23pm");
1040 insta::assert_snapshot!(f("%H%P", time(0, 0, 0, 0)), @"00am");
1041
1042 insta::assert_snapshot!(f("%H%p", time(9, 0, 0, 0)), @"09AM");
1043 insta::assert_snapshot!(f("%H%p", time(11, 0, 0, 0)), @"11AM");
1044 insta::assert_snapshot!(f("%H%p", time(23, 0, 0, 0)), @"23PM");
1045 insta::assert_snapshot!(f("%H%p", time(0, 0, 0, 0)), @"00AM");
1046
1047 insta::assert_snapshot!(f("%H%#p", time(9, 0, 0, 0)), @"09am");
1048 }
1049
1050 #[test]
1051 fn ok_format_clock() {
1052 let f = |fmt: &str, time: Time| format(fmt, time).unwrap();
1053
1054 insta::assert_snapshot!(f("%R", time(23, 59, 8, 0)), @"23:59");
1055 insta::assert_snapshot!(f("%T", time(23, 59, 8, 0)), @"23:59:08");
1056 }
1057
1058 #[test]
1059 fn ok_format_day() {
1060 let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1061
1062 insta::assert_snapshot!(f("%d", date(2024, 7, 9)), @"09");
1063 insta::assert_snapshot!(f("%0d", date(2024, 7, 9)), @"09");
1064 insta::assert_snapshot!(f("%-d", date(2024, 7, 9)), @"9");
1065 insta::assert_snapshot!(f("%_d", date(2024, 7, 9)), @" 9");
1066
1067 insta::assert_snapshot!(f("%e", date(2024, 7, 9)), @" 9");
1068 insta::assert_snapshot!(f("%0e", date(2024, 7, 9)), @"09");
1069 insta::assert_snapshot!(f("%-e", date(2024, 7, 9)), @"9");
1070 insta::assert_snapshot!(f("%_e", date(2024, 7, 9)), @" 9");
1071 }
1072
1073 #[test]
1074 fn ok_format_iso_date() {
1075 let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1076
1077 insta::assert_snapshot!(f("%F", date(2024, 7, 9)), @"2024-07-09");
1078 insta::assert_snapshot!(f("%-F", date(2024, 7, 9)), @"2024-7-9");
1079 insta::assert_snapshot!(f("%3F", date(2024, 7, 9)), @"2024-007-009");
1080 insta::assert_snapshot!(f("%03F", date(2024, 7, 9)), @"2024-007-009");
1081 }
1082
1083 #[test]
1084 fn ok_format_hour() {
1085 let f = |fmt: &str, time: Time| format(fmt, time).unwrap();
1086
1087 insta::assert_snapshot!(f("%H", time(9, 0, 0, 0)), @"09");
1088 insta::assert_snapshot!(f("%H", time(11, 0, 0, 0)), @"11");
1089 insta::assert_snapshot!(f("%H", time(23, 0, 0, 0)), @"23");
1090 insta::assert_snapshot!(f("%H", time(0, 0, 0, 0)), @"00");
1091
1092 insta::assert_snapshot!(f("%I", time(9, 0, 0, 0)), @"09");
1093 insta::assert_snapshot!(f("%I", time(11, 0, 0, 0)), @"11");
1094 insta::assert_snapshot!(f("%I", time(23, 0, 0, 0)), @"11");
1095 insta::assert_snapshot!(f("%I", time(0, 0, 0, 0)), @"12");
1096
1097 insta::assert_snapshot!(f("%k", time(9, 0, 0, 0)), @" 9");
1098 insta::assert_snapshot!(f("%k", time(11, 0, 0, 0)), @"11");
1099 insta::assert_snapshot!(f("%k", time(23, 0, 0, 0)), @"23");
1100 insta::assert_snapshot!(f("%k", time(0, 0, 0, 0)), @" 0");
1101
1102 insta::assert_snapshot!(f("%l", time(9, 0, 0, 0)), @" 9");
1103 insta::assert_snapshot!(f("%l", time(11, 0, 0, 0)), @"11");
1104 insta::assert_snapshot!(f("%l", time(23, 0, 0, 0)), @"11");
1105 insta::assert_snapshot!(f("%l", time(0, 0, 0, 0)), @"12");
1106 }
1107
1108 #[test]
1109 fn ok_format_minute() {
1110 let f = |fmt: &str, time: Time| format(fmt, time).unwrap();
1111
1112 insta::assert_snapshot!(f("%M", time(0, 9, 0, 0)), @"09");
1113 insta::assert_snapshot!(f("%M", time(0, 11, 0, 0)), @"11");
1114 insta::assert_snapshot!(f("%M", time(0, 23, 0, 0)), @"23");
1115 insta::assert_snapshot!(f("%M", time(0, 0, 0, 0)), @"00");
1116 }
1117
1118 #[test]
1119 fn ok_format_month() {
1120 let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1121
1122 insta::assert_snapshot!(f("%m", date(2024, 7, 14)), @"07");
1123 insta::assert_snapshot!(f("%m", date(2024, 12, 14)), @"12");
1124 insta::assert_snapshot!(f("%0m", date(2024, 7, 14)), @"07");
1125 insta::assert_snapshot!(f("%0m", date(2024, 12, 14)), @"12");
1126 insta::assert_snapshot!(f("%-m", date(2024, 7, 14)), @"7");
1127 insta::assert_snapshot!(f("%-m", date(2024, 12, 14)), @"12");
1128 insta::assert_snapshot!(f("%_m", date(2024, 7, 14)), @" 7");
1129 insta::assert_snapshot!(f("%_m", date(2024, 12, 14)), @"12");
1130 }
1131
1132 #[test]
1133 fn ok_format_month_name() {
1134 let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1135
1136 insta::assert_snapshot!(f("%B", date(2024, 7, 14)), @"July");
1137 insta::assert_snapshot!(f("%b", date(2024, 7, 14)), @"Jul");
1138 insta::assert_snapshot!(f("%h", date(2024, 7, 14)), @"Jul");
1139
1140 insta::assert_snapshot!(f("%#B", date(2024, 7, 14)), @"July");
1141 insta::assert_snapshot!(f("%^B", date(2024, 7, 14)), @"JULY");
1142 }
1143
1144 #[test]
1145 fn ok_format_offset_from_zoned() {
1146 if crate::tz::db().is_definitively_empty() {
1147 return;
1148 }
1149
1150 let f = |fmt: &str, zdt: &Zoned| format(fmt, zdt).unwrap();
1151
1152 let zdt = date(2024, 7, 14)
1153 .at(22, 24, 0, 0)
1154 .in_tz("America/New_York")
1155 .unwrap();
1156 insta::assert_snapshot!(f("%z", &zdt), @"-0400");
1157 insta::assert_snapshot!(f("%:z", &zdt), @"-04:00");
1158
1159 let zdt = zdt.checked_add(crate::Span::new().months(5)).unwrap();
1160 insta::assert_snapshot!(f("%z", &zdt), @"-0500");
1161 insta::assert_snapshot!(f("%:z", &zdt), @"-05:00");
1162 }
1163
1164 #[test]
1165 fn ok_format_offset_plain() {
1166 let o = |h: i8, m: i8, s: i8| -> Offset { Offset::hms(h, m, s) };
1167 let f = |fmt: &str, offset: Offset| {
1168 let mut tm = BrokenDownTime::default();
1169 tm.set_offset(Some(offset));
1170 tm.to_string(fmt).unwrap()
1171 };
1172
1173 insta::assert_snapshot!(f("%z", o(0, 0, 0)), @"+0000");
1174 insta::assert_snapshot!(f("%:z", o(0, 0, 0)), @"+00:00");
1175 insta::assert_snapshot!(f("%::z", o(0, 0, 0)), @"+00:00:00");
1176 insta::assert_snapshot!(f("%:::z", o(0, 0, 0)), @"+00");
1177
1178 insta::assert_snapshot!(f("%z", -o(4, 0, 0)), @"-0400");
1179 insta::assert_snapshot!(f("%:z", -o(4, 0, 0)), @"-04:00");
1180 insta::assert_snapshot!(f("%::z", -o(4, 0, 0)), @"-04:00:00");
1181 insta::assert_snapshot!(f("%:::z", -o(4, 0, 0)), @"-04");
1182
1183 insta::assert_snapshot!(f("%z", o(5, 30, 0)), @"+0530");
1184 insta::assert_snapshot!(f("%:z", o(5, 30, 0)), @"+05:30");
1185 insta::assert_snapshot!(f("%::z", o(5, 30, 0)), @"+05:30:00");
1186 insta::assert_snapshot!(f("%:::z", o(5, 30, 0)), @"+05:30");
1187
1188 insta::assert_snapshot!(f("%z", o(5, 30, 15)), @"+053015");
1189 insta::assert_snapshot!(f("%:z", o(5, 30, 15)), @"+05:30:15");
1190 insta::assert_snapshot!(f("%::z", o(5, 30, 15)), @"+05:30:15");
1191 insta::assert_snapshot!(f("%:::z", o(5, 30, 15)), @"+05:30:15");
1192
1193 insta::assert_snapshot!(f("%z", o(5, 0, 15)), @"+050015");
1194 insta::assert_snapshot!(f("%:z", o(5, 0, 15)), @"+05:00:15");
1195 insta::assert_snapshot!(f("%::z", o(5, 0, 15)), @"+05:00:15");
1196 insta::assert_snapshot!(f("%:::z", o(5, 0, 15)), @"+05:00:15");
1197 }
1198
1199 #[test]
1200 fn ok_format_second() {
1201 let f = |fmt: &str, time: Time| format(fmt, time).unwrap();
1202
1203 insta::assert_snapshot!(f("%S", time(0, 0, 9, 0)), @"09");
1204 insta::assert_snapshot!(f("%S", time(0, 0, 11, 0)), @"11");
1205 insta::assert_snapshot!(f("%S", time(0, 0, 23, 0)), @"23");
1206 insta::assert_snapshot!(f("%S", time(0, 0, 0, 0)), @"00");
1207 }
1208
1209 #[test]
1210 fn ok_format_subsec_nanosecond() {
1211 let f = |fmt: &str, time: Time| format(fmt, time).unwrap();
1212 let mk = |subsec| time(0, 0, 0, subsec);
1213
1214 insta::assert_snapshot!(f("%f", mk(123_000_000)), @"123");
1215 insta::assert_snapshot!(f("%f", mk(0)), @"0");
1216 insta::assert_snapshot!(f("%3f", mk(0)), @"000");
1217 insta::assert_snapshot!(f("%3f", mk(123_000_000)), @"123");
1218 insta::assert_snapshot!(f("%6f", mk(123_000_000)), @"123000");
1219 insta::assert_snapshot!(f("%9f", mk(123_000_000)), @"123000000");
1220 insta::assert_snapshot!(f("%255f", mk(123_000_000)), @"123000000");
1221
1222 insta::assert_snapshot!(f("%.f", mk(123_000_000)), @".123");
1223 insta::assert_snapshot!(f("%.f", mk(0)), @"");
1224 insta::assert_snapshot!(f("%3.f", mk(0)), @"");
1225 insta::assert_snapshot!(f("%.3f", mk(0)), @".000");
1226 insta::assert_snapshot!(f("%.3f", mk(123_000_000)), @".123");
1227 insta::assert_snapshot!(f("%.6f", mk(123_000_000)), @".123000");
1228 insta::assert_snapshot!(f("%.9f", mk(123_000_000)), @".123000000");
1229 insta::assert_snapshot!(f("%.255f", mk(123_000_000)), @".123000000");
1230
1231 insta::assert_snapshot!(f("%3f", mk(123_456_789)), @"123");
1232 insta::assert_snapshot!(f("%6f", mk(123_456_789)), @"123456");
1233 insta::assert_snapshot!(f("%9f", mk(123_456_789)), @"123456789");
1234
1235 insta::assert_snapshot!(f("%.0f", mk(123_456_789)), @"");
1236 insta::assert_snapshot!(f("%.3f", mk(123_456_789)), @".123");
1237 insta::assert_snapshot!(f("%.6f", mk(123_456_789)), @".123456");
1238 insta::assert_snapshot!(f("%.9f", mk(123_456_789)), @".123456789");
1239
1240 insta::assert_snapshot!(f("%N", mk(123_000_000)), @"123000000");
1241 insta::assert_snapshot!(f("%N", mk(0)), @"000000000");
1242 insta::assert_snapshot!(f("%N", mk(000_123_000)), @"000123000");
1243 insta::assert_snapshot!(f("%3N", mk(0)), @"000");
1244 insta::assert_snapshot!(f("%3N", mk(123_000_000)), @"123");
1245 insta::assert_snapshot!(f("%6N", mk(123_000_000)), @"123000");
1246 insta::assert_snapshot!(f("%9N", mk(123_000_000)), @"123000000");
1247 insta::assert_snapshot!(f("%255N", mk(123_000_000)), @"123000000");
1248 }
1249
1250 #[test]
1251 fn ok_format_tzabbrev() {
1252 if crate::tz::db().is_definitively_empty() {
1253 return;
1254 }
1255
1256 let f = |fmt: &str, zdt: &Zoned| format(fmt, zdt).unwrap();
1257
1258 let zdt = date(2024, 7, 14)
1259 .at(22, 24, 0, 0)
1260 .in_tz("America/New_York")
1261 .unwrap();
1262 insta::assert_snapshot!(f("%Z", &zdt), @"EDT");
1263 insta::assert_snapshot!(f("%^Z", &zdt), @"EDT");
1264 insta::assert_snapshot!(f("%#Z", &zdt), @"edt");
1265
1266 let zdt = zdt.checked_add(crate::Span::new().months(5)).unwrap();
1267 insta::assert_snapshot!(f("%Z", &zdt), @"EST");
1268 }
1269
1270 #[test]
1271 fn ok_format_iana() {
1272 if crate::tz::db().is_definitively_empty() {
1273 return;
1274 }
1275
1276 let f = |fmt: &str, zdt: &Zoned| format(fmt, zdt).unwrap();
1277
1278 let zdt = date(2024, 7, 14)
1279 .at(22, 24, 0, 0)
1280 .in_tz("America/New_York")
1281 .unwrap();
1282 insta::assert_snapshot!(f("%Q", &zdt), @"America/New_York");
1283 insta::assert_snapshot!(f("%:Q", &zdt), @"America/New_York");
1284
1285 let zdt = date(2024, 7, 14)
1286 .at(22, 24, 0, 0)
1287 .to_zoned(crate::tz::offset(-4).to_time_zone())
1288 .unwrap();
1289 insta::assert_snapshot!(f("%Q", &zdt), @"-0400");
1290 insta::assert_snapshot!(f("%:Q", &zdt), @"-04:00");
1291
1292 let zdt = date(2024, 7, 14)
1293 .at(22, 24, 0, 0)
1294 .to_zoned(crate::tz::TimeZone::UTC)
1295 .unwrap();
1296 insta::assert_snapshot!(f("%Q", &zdt), @"UTC");
1297 insta::assert_snapshot!(f("%:Q", &zdt), @"UTC");
1298 }
1299
1300 #[test]
1301 fn ok_format_weekday_name() {
1302 let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1303
1304 insta::assert_snapshot!(f("%A", date(2024, 7, 14)), @"Sunday");
1305 insta::assert_snapshot!(f("%a", date(2024, 7, 14)), @"Sun");
1306
1307 insta::assert_snapshot!(f("%#A", date(2024, 7, 14)), @"Sunday");
1308 insta::assert_snapshot!(f("%^A", date(2024, 7, 14)), @"SUNDAY");
1309
1310 insta::assert_snapshot!(f("%u", date(2024, 7, 14)), @"7");
1311 insta::assert_snapshot!(f("%w", date(2024, 7, 14)), @"0");
1312 }
1313
1314 #[test]
1315 fn ok_format_year() {
1316 let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1317
1318 insta::assert_snapshot!(f("%Y", date(2024, 7, 14)), @"2024");
1319 insta::assert_snapshot!(f("%Y", date(24, 7, 14)), @"0024");
1320 insta::assert_snapshot!(f("%Y", date(-24, 7, 14)), @"-0024");
1321
1322 insta::assert_snapshot!(f("%C", date(2024, 7, 14)), @"20");
1323 insta::assert_snapshot!(f("%C", date(1815, 7, 14)), @"18");
1324 insta::assert_snapshot!(f("%C", date(915, 7, 14)), @"9");
1325 insta::assert_snapshot!(f("%C", date(1, 7, 14)), @"0");
1326 insta::assert_snapshot!(f("%C", date(0, 7, 14)), @"0");
1327 insta::assert_snapshot!(f("%C", date(-1, 7, 14)), @"0");
1328 insta::assert_snapshot!(f("%C", date(-2024, 7, 14)), @"-20");
1329 insta::assert_snapshot!(f("%C", date(-1815, 7, 14)), @"-18");
1330 insta::assert_snapshot!(f("%C", date(-915, 7, 14)), @"-9");
1331 }
1332
1333 #[test]
1334 fn ok_format_default_locale() {
1335 let f = |fmt: &str, date: DateTime| format(fmt, date).unwrap();
1336
1337 insta::assert_snapshot!(
1338 f("%c", date(2024, 7, 14).at(0, 0, 0, 0)),
1339 @"2024 M07 14, Sun 00:00:00",
1340 );
1341 insta::assert_snapshot!(
1342 f("%c", date(24, 7, 14).at(0, 0, 0, 0)),
1343 @"0024 M07 14, Sun 00:00:00",
1344 );
1345 insta::assert_snapshot!(
1346 f("%c", date(-24, 7, 14).at(0, 0, 0, 0)),
1347 @"-0024 M07 14, Wed 00:00:00",
1348 );
1349 insta::assert_snapshot!(
1350 f("%c", date(2024, 7, 14).at(17, 31, 59, 123_456_789)),
1351 @"2024 M07 14, Sun 17:31:59",
1352 );
1353
1354 insta::assert_snapshot!(
1355 f("%r", date(2024, 7, 14).at(8, 30, 0, 0)),
1356 @"8:30:00 AM",
1357 );
1358 insta::assert_snapshot!(
1359 f("%r", date(2024, 7, 14).at(17, 31, 59, 123_456_789)),
1360 @"5:31:59 PM",
1361 );
1362
1363 insta::assert_snapshot!(
1364 f("%x", date(2024, 7, 14).at(0, 0, 0, 0)),
1365 @"2024 M07 14",
1366 );
1367
1368 insta::assert_snapshot!(
1369 f("%X", date(2024, 7, 14).at(8, 30, 0, 0)),
1370 @"08:30:00",
1371 );
1372 insta::assert_snapshot!(
1373 f("%X", date(2024, 7, 14).at(17, 31, 59, 123_456_789)),
1374 @"17:31:59",
1375 );
1376 }
1377
1378 #[test]
1379 fn ok_format_posix_locale() {
1380 let f = |fmt: &str, date: DateTime| {
1381 let config = Config::new().custom(PosixCustom::default());
1382 let tm = BrokenDownTime::from(date);
1383 tm.to_string_with_config(&config, fmt).unwrap()
1384 };
1385
1386 insta::assert_snapshot!(
1387 f("%c", date(2024, 7, 14).at(0, 0, 0, 0)),
1388 @"Sun Jul 14 00:00:00 2024",
1389 );
1390 insta::assert_snapshot!(
1391 f("%c", date(24, 7, 14).at(0, 0, 0, 0)),
1392 @"Sun Jul 14 00:00:00 0024",
1393 );
1394 insta::assert_snapshot!(
1395 f("%c", date(-24, 7, 14).at(0, 0, 0, 0)),
1396 @"Wed Jul 14 00:00:00 -0024",
1397 );
1398 insta::assert_snapshot!(
1399 f("%c", date(2024, 7, 14).at(17, 31, 59, 123_456_789)),
1400 @"Sun Jul 14 17:31:59 2024",
1401 );
1402
1403 insta::assert_snapshot!(
1404 f("%r", date(2024, 7, 14).at(8, 30, 0, 0)),
1405 @"08:30:00 AM",
1406 );
1407 insta::assert_snapshot!(
1408 f("%r", date(2024, 7, 14).at(17, 31, 59, 123_456_789)),
1409 @"05:31:59 PM",
1410 );
1411
1412 insta::assert_snapshot!(
1413 f("%x", date(2024, 7, 14).at(0, 0, 0, 0)),
1414 @"07/14/24",
1415 );
1416
1417 insta::assert_snapshot!(
1418 f("%X", date(2024, 7, 14).at(8, 30, 0, 0)),
1419 @"08:30:00",
1420 );
1421 insta::assert_snapshot!(
1422 f("%X", date(2024, 7, 14).at(17, 31, 59, 123_456_789)),
1423 @"17:31:59",
1424 );
1425 }
1426
1427 #[test]
1428 fn ok_format_year_2digit() {
1429 let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1430
1431 insta::assert_snapshot!(f("%y", date(2024, 7, 14)), @"24");
1432 insta::assert_snapshot!(f("%y", date(2001, 7, 14)), @"01");
1433 insta::assert_snapshot!(f("%-y", date(2001, 7, 14)), @"1");
1434 insta::assert_snapshot!(f("%5y", date(2001, 7, 14)), @"00001");
1435 insta::assert_snapshot!(f("%-5y", date(2001, 7, 14)), @"1");
1436 insta::assert_snapshot!(f("%05y", date(2001, 7, 14)), @"00001");
1437 insta::assert_snapshot!(f("%_y", date(2001, 7, 14)), @" 1");
1438 insta::assert_snapshot!(f("%_5y", date(2001, 7, 14)), @" 1");
1439 }
1440
1441 #[test]
1442 fn ok_format_iso_week_year() {
1443 let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1444
1445 insta::assert_snapshot!(f("%G", date(2019, 11, 30)), @"2019");
1446 insta::assert_snapshot!(f("%G", date(19, 11, 30)), @"0019");
1447 insta::assert_snapshot!(f("%G", date(-19, 11, 30)), @"-0019");
1448
1449 insta::assert_snapshot!(f("%G", date(2019, 12, 30)), @"2020");
1451 }
1452
1453 #[test]
1454 fn ok_format_week_num() {
1455 let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1456
1457 insta::assert_snapshot!(f("%U", date(2025, 1, 4)), @"00");
1458 insta::assert_snapshot!(f("%U", date(2025, 1, 5)), @"01");
1459
1460 insta::assert_snapshot!(f("%W", date(2025, 1, 5)), @"00");
1461 insta::assert_snapshot!(f("%W", date(2025, 1, 6)), @"01");
1462 }
1463
1464 #[test]
1465 fn ok_format_timestamp() {
1466 let f = |fmt: &str, ts: Timestamp| format(fmt, ts).unwrap();
1467
1468 let ts = "1970-01-01T00:00Z".parse().unwrap();
1469 insta::assert_snapshot!(f("%s", ts), @"0");
1470 insta::assert_snapshot!(f("%3s", ts), @" 0");
1471 insta::assert_snapshot!(f("%03s", ts), @"000");
1472
1473 let ts = "2025-01-20T13:09-05[US/Eastern]".parse().unwrap();
1474 insta::assert_snapshot!(f("%s", ts), @"1737396540");
1475 }
1476
1477 #[test]
1478 fn ok_format_quarter() {
1479 let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1480
1481 insta::assert_snapshot!(f("%q", date(2024, 3, 31)), @"1");
1482 insta::assert_snapshot!(f("%q", date(2024, 4, 1)), @"2");
1483 insta::assert_snapshot!(f("%q", date(2024, 7, 14)), @"3");
1484 insta::assert_snapshot!(f("%q", date(2024, 12, 31)), @"4");
1485
1486 insta::assert_snapshot!(f("%2q", date(2024, 3, 31)), @"01");
1487 insta::assert_snapshot!(f("%02q", date(2024, 3, 31)), @"01");
1488 insta::assert_snapshot!(f("%_2q", date(2024, 3, 31)), @" 1");
1489 }
1490
1491 #[test]
1492 fn err_format_subsec_nanosecond() {
1493 let f = |fmt: &str, time: Time| format(fmt, time).unwrap_err();
1494 let mk = |subsec| time(0, 0, 0, subsec);
1495
1496 insta::assert_snapshot!(
1497 f("%00f", mk(123_456_789)),
1498 @"strftime formatting failed: %f failed: zero precision with %f is not allowed",
1499 );
1500 }
1501
1502 #[test]
1503 fn err_format_timestamp() {
1504 let f = |fmt: &str, dt: DateTime| format(fmt, dt).unwrap_err();
1505
1506 let dt = date(2025, 1, 20).at(13, 9, 0, 0);
1507 insta::assert_snapshot!(
1508 f("%s", dt),
1509 @"strftime formatting failed: %s failed: requires instant (a date, time and offset) to format Unix timestamp",
1510 );
1511 }
1512
1513 #[test]
1514 fn lenient() {
1515 fn f(
1516 fmt: &str,
1517 tm: impl Into<BrokenDownTime>,
1518 ) -> alloc::string::String {
1519 let config = Config::new().lenient(true);
1520 tm.into().to_string_with_config(&config, fmt).unwrap()
1521 }
1522
1523 insta::assert_snapshot!(f("%z", date(2024, 7, 9)), @"%z");
1524 insta::assert_snapshot!(f("%:z", date(2024, 7, 9)), @"%:z");
1525 insta::assert_snapshot!(f("%Q", date(2024, 7, 9)), @"%Q");
1526 insta::assert_snapshot!(f("%+", date(2024, 7, 9)), @"%+");
1527 insta::assert_snapshot!(f("%F", date(2024, 7, 9)), @"2024-07-09");
1528 insta::assert_snapshot!(f("%T", date(2024, 7, 9)), @"%T");
1529 }
1530}