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