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 // Parse extensions like padding/case options and padding width.
42 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 // Parse precision settings after the `.`, effectively
112 // overriding any digits that came before it.
113 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 /// Returns the byte at the current position of the format string.
140 ///
141 /// # Panics
142 ///
143 /// This panics when the entire format string has been consumed.
144 fn f(&self) -> u8 {
145 self.fmt[0]
146 }
147
148 /// Bumps the position of the format string.
149 ///
150 /// This returns true in precisely the cases where `self.f()` will not
151 /// panic. i.e., When the end of the format string hasn't been reached yet.
152 fn bump_fmt(&mut self) -> bool {
153 self.fmt = &self.fmt[1..];
154 !self.fmt.is_empty()
155 }
156
157 /// Decodes a Unicode scalar value from the beginning of `fmt` and advances
158 /// the parser accordingly.
159 ///
160 /// If a Unicode scalar value could not be decoded, then an error is
161 /// returned.
162 ///
163 /// It would be nice to just pass through bytes as-is instead of doing
164 /// actual UTF-8 decoding, but since the `Write` trait only represents
165 /// Unicode-accepting buffers, we need to actually do decoding here.
166 ///
167 /// # Panics
168 ///
169 /// When `self.fmt` is empty. i.e., Only call this when you know there is
170 /// some remaining bytes to parse.
171 #[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 /// Parses optional extensions before a specifier directive. That is, right
187 /// after the `%`. If any extensions are parsed, the parser is bumped
188 /// to the next byte. (If no next byte exists, then an error is returned.)
189 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 /// Parses an optional flag. And if one is parsed, the parser is bumped
196 /// to the next byte. (If no next byte exists, then an error is returned.)
197 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 /// Parses an optional width that comes after a (possibly absent) flag and
204 /// before the specifier directive itself. And if a width is parsed, the
205 /// parser is bumped to the next byte. (If no next byte exists, then an
206 /// error is returned.)
207 ///
208 /// Note that this is also used to parse precision settings for `%f` and
209 /// `%.f`. In the former case, the width is just re-interpreted as a
210 /// precision setting. In the latter case, something like `%5.9f` is
211 /// technically valid, but the `5` is ignored.
212 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 // These are the formatting functions. They are pretty much responsible
219 // for getting what they need for the broken down time and reporting a
220 // decent failure mode if what they need couldn't be found. And then,
221 // of course, doing the actual formatting.
222
223 /// %P
224 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 /// %p
238 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 /// %D
252 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 /// %R
262 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 /// %T
270 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 /// %d
280 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 /// %e
291 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 /// %I
302 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 /// %H
317 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 /// %l
327 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 /// %k
342 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 /// %F
352 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 /// %M
362 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 /// %m
372 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 /// %B
383 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 /// %b, %h
393 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 /// %Q
403 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 /// %:Q
418 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 /// %z
433 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 /// %:z
441 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 /// %S
449 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 /// %s
459 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 /// %f
470 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 // For %f, we always want to emit at least one digit. The only way we
475 // wouldn't is if our fractional component is zero. One exception to
476 // this is when the width is `0` (which looks like `%00f`), in which
477 // case, we emit an error. We could allow it to emit an empty string,
478 // but this seems very odd. And an empty string cannot be parsed by
479 // `%f`.
480 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 /// %.f
492 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 /// %Z
503 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 /// %A
511 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 /// %a
521 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 /// %u
531 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 /// %w
541 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 /// %U
551 fn fmt_week_sun(&mut self, ext: Extension) -> Result<(), Error> {
552 // Short circuit if the week number was explicitly set.
553 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 // Example: 2025-01-05 is the first Sunday in 2025, and thus the start
573 // of week 1. This means that 2025-01-04 (Saturday) is in week 0.
574 //
575 // So for 2025-01-05, day=5 and weekday=0. Thus we get 11/7 = 1.
576 // For 2025-01-04, day=4 and weekday=6. Thus we get 4/7 = 0.
577 let weeknum = (day + 6 - i16::from(weekday)) / 7;
578 ext.write_int(b'0', Some(2), weeknum, self.wtr)
579 }
580
581 /// %V
582 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 /// %W
596 fn fmt_week_mon(&mut self, ext: Extension) -> Result<(), Error> {
597 // Short circuit if the week number was explicitly set.
598 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 // Example: 2025-01-06 is the first Monday in 2025, and thus the start
618 // of week 1. This means that 2025-01-05 (Sunday) is in week 0.
619 //
620 // So for 2025-01-06, day=6 and weekday=1. Thus we get 12/7 = 1.
621 // For 2025-01-05, day=5 and weekday=7. Thus we get 5/7 = 0.
622 let weeknum = (day + 6 - ((i16::from(weekday) + 6) % 7)) / 7;
623 ext.write_int(b'0', Some(2), weeknum, self.wtr)
624 }
625
626 /// %Y
627 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 /// %y
638 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 /// %C
656 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 /// %G
668 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 /// %g
683 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 /// %j
709 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 /// %n, %t
720 fn fmt_literal(&mut self, literal: &str) -> Result<(), Error> {
721 self.wtr.write_str(literal)
722 }
723}
724
725/// Writes the given time zone offset to the writer.
726///
727/// When `colon` is true, the hour, minute and optional second components are
728/// delimited by a colon. Otherwise, no delimiter is used.
729fn 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(digits:2);
735
736 let hours: i8 = offset.part_hours_ranged().abs().get();
737 let minutes: i8 = offset.part_minutes_ranged().abs().get();
738 let seconds: i8 = offset.part_seconds_ranged().abs().get();
739
740 wtr.write_str(string:if offset.is_negative() { "-" } else { "+" })?;
741 wtr.write_int(&FMT_TWO, n:hours)?;
742 if colon {
743 wtr.write_str(string:":")?;
744 }
745 wtr.write_int(&FMT_TWO, n:minutes)?;
746 if seconds != 0 {
747 if colon {
748 wtr.write_str(string:":")?;
749 }
750 wtr.write_int(&FMT_TWO, n:seconds)?;
751 }
752 Ok(())
753}
754
755impl Extension {
756 /// Writes the given string using the default case rule provided, unless
757 /// an option in this extension config overrides the default case.
758 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 /// Writes the given integer using the given padding width and byte, unless
792 /// an option in this extension config overrides a default setting.
793 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 /// Writes the given number of nanoseconds as a fractional component of
820 /// a second. This does not include the leading `.`.
821 ///
822 /// The `width` setting on `Extension` is treated as a precision setting.
823 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/// The case to use when printing a string like weekday or TZ abbreviation.
836#[derive(Clone, Copy, Debug)]
837enum Case {
838 AsIs,
839 Upper,
840 Lower,
841}
842
843impl Case {
844 /// Swap upper to lowercase, and lower to uppercase.
845 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 // tricksy
1152 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}
1201