jiff/fmt/strtime/
format.rs

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                // `orig` is whatever failed to parse immediately after a `%`.
53                // Since it failed, we write out the `%` and then proceed to
54                // handle what failed to parse literally.
55                self.wtr.write_str("%")?;
56                // Reset back to right after parsing the `%`.
57                self.fmt = orig;
58            }
59        }
60        Ok(())
61    }
62
63    fn format_one(&mut self) -> Result<(), Error> {
64        // Parse extensions like padding/case options and padding width.
65        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                // Parse precision settings after the `.`, effectively
137                // overriding any digits that came before it.
138                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    /// Returns the byte at the current position of the format string.
163    ///
164    /// # Panics
165    ///
166    /// This panics when the entire format string has been consumed.
167    fn f(&self) -> u8 {
168        self.fmt[0]
169    }
170
171    /// Bumps the position of the format string.
172    ///
173    /// This returns true in precisely the cases where `self.f()` will not
174    /// panic. i.e., When the end of the format string hasn't been reached yet.
175    fn bump_fmt(&mut self) -> bool {
176        self.fmt = &self.fmt[1..];
177        !self.fmt.is_empty()
178    }
179
180    /// Decodes a Unicode scalar value from the beginning of `fmt` and advances
181    /// the parser accordingly.
182    ///
183    /// If a Unicode scalar value could not be decoded, then an error is
184    /// returned.
185    ///
186    /// It would be nice to just pass through bytes as-is instead of doing
187    /// actual UTF-8 decoding, but since the `Write` trait only represents
188    /// Unicode-accepting buffers, we need to actually do decoding here.
189    ///
190    /// # Errors
191    ///
192    /// Unless lenient parsing is enabled, this returns an error if UTF-8
193    /// decoding failed. When lenient parsing is enabled, decoding errors
194    /// are turned into the Unicode replacement codepoint via the
195    /// "substitution of maximal subparts" strategy.
196    ///
197    /// # Panics
198    ///
199    /// When `self.fmt` is empty. i.e., Only call this when you know there is
200    /// some remaining bytes to parse.
201    #[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    /// Parses optional extensions before a specifier directive. That is, right
222    /// after the `%`. If any extensions are parsed, the parser is bumped
223    /// to the next byte. (If no next byte exists, then an error is returned.)
224    #[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    /// Parses an optional flag. And if one is parsed, the parser is bumped
233    /// to the next byte. (If no next byte exists, then an error is returned.)
234    #[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    /// Parses an optional width that comes after a (possibly absent) flag and
242    /// before the specifier directive itself. And if a width is parsed, the
243    /// parser is bumped to the next byte. (If no next byte exists, then an
244    /// error is returned.)
245    ///
246    /// Note that this is also used to parse precision settings for `%f` and
247    /// `%.f`. In the former case, the width is just re-interpreted as a
248    /// precision setting. In the latter case, something like `%5.9f` is
249    /// technically valid, but the `5` is ignored.
250    #[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    /// Parses an optional number of colons (up to 3) immediately before a
258    /// conversion specifier.
259    #[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    // These are the formatting functions. They are pretty much responsible
267    // for getting what they need for the broken down time and reporting a
268    // decent failure mode if what they need couldn't be found. And then,
269    // of course, doing the actual formatting.
270
271    /// %P
272    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    /// %p
286    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        // Manually specialize this case to avoid hitting `write_str_cold`.
293        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    /// %D
310    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    /// %R
320    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    /// %T
328    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    /// %d
338    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    /// %e
349    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    /// %I
360    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    /// %H
375    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    /// %l
385    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    /// %k
400    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    /// %F
410    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    /// %M
420    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    /// %m
430    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    /// %B
441    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    /// %b, %h
451    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    /// %Q
461    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    /// %:Q
476    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    /// %z
491    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    /// %:z
499    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    /// %::z
507    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    /// %:::z
515    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    /// %S
523    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    /// %s
533    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    /// %f
544    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        // For %f, we always want to emit at least one digit. The only way we
550        // wouldn't is if our fractional component is zero. One exception to
551        // this is when the width is `0` (which looks like `%00f`), in which
552        // case, we emit an error. We could allow it to emit an empty string,
553        // but this seems very odd. And an empty string cannot be parsed by
554        // `%f`.
555        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    /// %.f
567    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    /// %N
579    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        // Since `%N` is actually an alias for `%9f`, when the precision
588        // is missing, we default to 9.
589        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    /// %Z
598    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    /// %A
612    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    /// %a
622    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    /// %u
632    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    /// %w
642    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    /// %U
652    fn fmt_week_sun(&mut self, ext: &Extension) -> Result<(), Error> {
653        // Short circuit if the week number was explicitly set.
654        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        // Example: 2025-01-05 is the first Sunday in 2025, and thus the start
674        // of week 1. This means that 2025-01-04 (Saturday) is in week 0.
675        //
676        // So for 2025-01-05, day=5 and weekday=0. Thus we get 11/7 = 1.
677        // For 2025-01-04, day=4 and weekday=6. Thus we get 4/7 = 0.
678        let weeknum = (day + 6 - i16::from(weekday)) / 7;
679        ext.write_int(b'0', Some(2), weeknum, self.wtr)
680    }
681
682    /// %V
683    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    /// %W
697    fn fmt_week_mon(&mut self, ext: &Extension) -> Result<(), Error> {
698        // Short circuit if the week number was explicitly set.
699        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        // Example: 2025-01-06 is the first Monday in 2025, and thus the start
719        // of week 1. This means that 2025-01-05 (Sunday) is in week 0.
720        //
721        // So for 2025-01-06, day=6 and weekday=1. Thus we get 12/7 = 1.
722        // For 2025-01-05, day=5 and weekday=7. Thus we get 5/7 = 0.
723        let weeknum = (day + 6 - ((i16::from(weekday) + 6) % 7)) / 7;
724        ext.write_int(b'0', Some(2), weeknum, self.wtr)
725    }
726
727    /// %Y
728    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    /// %y
739    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    /// %C
751    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    /// %G
763    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    /// %g
778    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    /// %q
797    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    /// %j
815    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    /// %n, %t
826    fn fmt_literal(&mut self, literal: &str) -> Result<(), Error> {
827        self.wtr.write_str(literal)
828    }
829
830    /// %c
831    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    /// %x
836    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    /// %X
841    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    /// %r
846    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
856/// Writes the given time zone offset to the writer.
857///
858/// When `colon` is true, the hour, minute and optional second components are
859/// delimited by a colon. Otherwise, no delimiter is used.
860///
861/// When `minute` is true, the minute component is always printed. When
862/// false, the minute component is only printed when it is non-zero (or if
863/// the second component is non-zero).
864///
865/// When `second` is true, the second component is always printed. When false,
866/// the second component is only printed when it is non-zero.
867fn 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    /// Writes the given string using the default case rule provided, unless
899    /// an option in this extension config overrides the default case.
900    #[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    /// Writes the given integer using the given padding width and byte, unless
949    /// an option in this extension config overrides a default setting.
950    #[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    /// Writes the given number of nanoseconds as a fractional component of
978    /// a second. This does not include the leading `.`.
979    ///
980    /// The `width` setting on `Extension` is treated as a precision setting.
981    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/// The case to use when printing a string like weekday or TZ abbreviation.
994#[derive(Clone, Copy, Debug)]
995enum Case {
996    AsIs,
997    Upper,
998    Lower,
999}
1000
1001impl Case {
1002    /// Swap upper to lowercase, and lower to uppercase.
1003    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        // tricksy
1452        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        // Demonstrates substitution of maximal subparts.
1550        // Namely, `\xF0\x9F\x92` is a prefix of a valid
1551        // UTF-8 encoding of a codepoint, such as `💩`.
1552        // So the entire prefix should get substituted with
1553        // a single replacement character...
1554        insta::assert_snapshot!(
1555            f(b"%F\xF0\x9F\x92%Y", date(2024, 7, 9)),
1556            @"2024-07-09�2024",
1557        );
1558        // ... but \xFF is never part of a valid encoding.
1559        // So each instance gets its own replacement
1560        // character.
1561        insta::assert_snapshot!(
1562            f(b"%F\xFF\xFF\xFF%Y", date(2024, 7, 9)),
1563            @"2024-07-09���2024",
1564        );
1565    }
1566}