jiff/fmt/strtime/
printer.rs

1use crate::{
2    error::{
3        fmt::strtime::{Error as E, FormatError as FE},
4        ErrorContext,
5    },
6    fmt::{
7        buffer::BorrowedWriter,
8        strtime::{
9            month_name_abbrev, month_name_full, weekday_name_abbrev,
10            weekday_name_full, BrokenDownTime, Config, Custom, Extension,
11            Flag, Meridiem,
12        },
13    },
14    tz::Offset,
15    util::utf8,
16    Error,
17};
18
19enum Item {
20    AlreadyFormatted,
21    Integer(ItemInteger),
22    Fraction(ItemFraction),
23    String(ItemString),
24    Offset(ItemOffset),
25}
26
27struct ItemInteger {
28    pad_byte: u8,
29    pad_width: u8,
30    number: i64,
31}
32
33impl ItemInteger {
34    fn new(pad_byte: u8, pad_width: u8, n: impl Into<i64>) -> ItemInteger {
35        ItemInteger { pad_byte, pad_width, number: n.into() }
36    }
37}
38
39struct ItemFraction {
40    width: Option<u8>,
41    dot: bool,
42    subsec: u32,
43}
44
45struct ItemString {
46    case: Case,
47    string: &'static str,
48}
49
50impl ItemString {
51    fn new(case: Case, string: &'static str) -> ItemString {
52        ItemString { case, string }
53    }
54}
55
56struct ItemOffset {
57    offset: Offset,
58    colon: bool,
59    minute: bool,
60    second: bool,
61}
62
63impl ItemOffset {
64    fn new(
65        offset: Offset,
66        colon: bool,
67        minute: bool,
68        second: bool,
69    ) -> ItemOffset {
70        ItemOffset { offset, colon, minute, second }
71    }
72}
73
74pub(super) struct Formatter<
75    'config,
76    'fmt,
77    'tm,
78    'writer,
79    'buffer,
80    'data,
81    'write,
82    L,
83> {
84    pub(super) config: &'config Config<L>,
85    pub(super) fmt: &'fmt [u8],
86    pub(super) tm: &'tm BrokenDownTime,
87    pub(super) wtr: &'writer mut BorrowedWriter<'buffer, 'data, 'write>,
88}
89
90impl<'config, 'fmt, 'tm, 'writer, 'buffer, 'data, 'write, L: Custom>
91    Formatter<'config, 'fmt, 'tm, 'writer, 'buffer, 'data, 'write, L>
92{
93    #[inline(never)]
94    pub(super) fn format(&mut self) -> Result<(), Error> {
95        while !self.fmt.is_empty() {
96            if self.f() != b'%' {
97                if self.f().is_ascii() {
98                    self.wtr.write_ascii_char(self.f())?;
99                    self.bump_fmt();
100                } else {
101                    let ch = self.utf8_decode_and_bump()?;
102                    self.wtr.write_char(ch)?;
103                }
104                continue;
105            }
106            if !self.bump_fmt() {
107                if self.config.lenient {
108                    self.wtr.write_ascii_char(b'%')?;
109                    break;
110                }
111                return Err(E::UnexpectedEndAfterPercent.into());
112            }
113            let orig = self.fmt;
114            if let Err(err) =
115                self.parse_extension().and_then(|ext| self.format_one(&ext))
116            {
117                if !self.config.lenient {
118                    return Err(err);
119                }
120                // `orig` is whatever failed to parse immediately after a `%`.
121                // Since it failed, we write out the `%` and then proceed to
122                // handle what failed to parse literally.
123                self.wtr.write_ascii_char(b'%')?;
124                // Reset back to right after parsing the `%`.
125                self.fmt = orig;
126            }
127        }
128        Ok(())
129    }
130
131    #[cfg_attr(feature = "perf-inline", inline(always))]
132    fn format_one(&mut self, ext: &Extension) -> Result<(), Error> {
133        let i = |item: ItemInteger| Item::Integer(item);
134        let s = |item: ItemString| Item::String(item);
135        let o = |item: ItemOffset| Item::Offset(item);
136        let failc =
137            |directive, colons| E::DirectiveFailure { directive, colons };
138        let fail = |directive| failc(directive, 0);
139
140        // Parse extensions like padding/case options and padding width.
141        let mut directive = self.f();
142        let item = match directive {
143            b'%' => self.fmt_literal("%").map(s).context(fail(b'%')),
144            b'A' => self.fmt_weekday_full().map(s).context(fail(b'A')),
145            b'a' => self.fmt_weekday_abbrev().map(s).context(fail(b'a')),
146            b'B' => self.fmt_month_full().map(s).context(fail(b'B')),
147            b'b' => self.fmt_month_abbrev().map(s).context(fail(b'b')),
148            b'C' => self.fmt_century().map(i).context(fail(b'C')),
149            b'c' => self.fmt_datetime(ext).context(fail(b'c')),
150            b'D' => self.fmt_american_date().context(fail(b'D')),
151            b'd' => self.fmt_day_zero().map(i).context(fail(b'd')),
152            b'e' => self.fmt_day_space().map(i).context(fail(b'e')),
153            b'F' => self.fmt_iso_date().context(fail(b'F')),
154            b'f' => self.fmt_fractional(ext).context(fail(b'f')),
155            b'G' => self.fmt_iso_week_year().map(i).context(fail(b'G')),
156            b'g' => self.fmt_iso_week_year2().map(i).context(fail(b'g')),
157            b'H' => self.fmt_hour24_zero().map(i).context(fail(b'H')),
158            b'h' => self.fmt_month_abbrev().map(s).context(fail(b'b')),
159            b'I' => self.fmt_hour12_zero().map(i).context(fail(b'H')),
160            b'j' => self.fmt_day_of_year().map(i).context(fail(b'j')),
161            b'k' => self.fmt_hour24_space().map(i).context(fail(b'k')),
162            b'l' => self.fmt_hour12_space().map(i).context(fail(b'l')),
163            b'M' => self.fmt_minute().map(i).context(fail(b'M')),
164            b'm' => self.fmt_month().map(i).context(fail(b'm')),
165            b'N' => self.fmt_nanoseconds(ext).context(fail(b'N')),
166            b'n' => self.fmt_literal("\n").map(s).context(fail(b'n')),
167            b'P' => self.fmt_ampm_lower().map(s).context(fail(b'P')),
168            b'p' => self.fmt_ampm_upper(ext).map(s).context(fail(b'p')),
169            b'Q' => match ext.colons {
170                0 => self.fmt_iana_nocolon().context(fail(b'Q')),
171                1 => self.fmt_iana_colon().context(failc(b'Q', 1)),
172                _ => return Err(E::ColonCount { directive: b'Q' }.into()),
173            },
174            b'q' => self.fmt_quarter().map(i).context(fail(b'q')),
175            b'R' => self.fmt_clock_nosecs().context(fail(b'R')),
176            b'r' => self.fmt_12hour_time(ext).context(fail(b'r')),
177            b'S' => self.fmt_second().map(i).context(fail(b'S')),
178            b's' => self.fmt_timestamp().map(i).context(fail(b's')),
179            b'T' => self.fmt_clock_secs().context(fail(b'T')),
180            b't' => self.fmt_literal("\t").map(s).context(fail(b't')),
181            b'U' => self.fmt_week_sun().map(i).context(fail(b'U')),
182            b'u' => self.fmt_weekday_mon().map(i).context(fail(b'u')),
183            b'V' => self.fmt_week_iso().map(i).context(fail(b'V')),
184            b'W' => self.fmt_week_mon().map(i).context(fail(b'W')),
185            b'w' => self.fmt_weekday_sun().map(i).context(fail(b'w')),
186            b'X' => self.fmt_time(ext).context(fail(b'X')),
187            b'x' => self.fmt_date(ext).context(fail(b'x')),
188            b'Y' => self.fmt_year().map(i).context(fail(b'Y')),
189            b'y' => self.fmt_year2().map(i).context(fail(b'y')),
190            b'Z' => self.fmt_tzabbrev(ext).context(fail(b'Z')),
191            b'z' => match ext.colons {
192                0 => self.fmt_offset_nocolon().map(o).context(fail(b'z')),
193                1 => self.fmt_offset_colon().map(o).context(failc(b'z', 1)),
194                2 => self.fmt_offset_colon2().map(o).context(failc(b'z', 2)),
195                3 => self.fmt_offset_colon3().map(o).context(failc(b'z', 3)),
196                _ => return Err(E::ColonCount { directive: b'z' }.into()),
197            },
198            b'.' => {
199                if !self.bump_fmt() {
200                    return Err(E::UnexpectedEndAfterDot.into());
201                }
202                // Parse precision settings after the `.`, effectively
203                // overriding any digits that came before it.
204                let ext =
205                    &Extension { width: self.parse_width()?, ..ext.clone() };
206                directive = self.f();
207                match directive {
208                    b'f' => self
209                        .fmt_dot_fractional(ext)
210                        .context(E::DirectiveFailureDot { directive }),
211                    _ => {
212                        return Err(Error::from(
213                            E::UnknownDirectiveAfterDot { directive },
214                        ));
215                    }
216                }
217            }
218            _ => return Err(Error::from(E::UnknownDirective { directive })),
219        }?;
220        self.write_item(ext, &item).context(fail(directive))?;
221        self.bump_fmt();
222        Ok(())
223    }
224
225    /// Returns the byte at the current position of the format string.
226    ///
227    /// # Panics
228    ///
229    /// This panics when the entire format string has been consumed.
230    fn f(&self) -> u8 {
231        self.fmt[0]
232    }
233
234    /// Bumps the position of the format string.
235    ///
236    /// This returns true in precisely the cases where `self.f()` will not
237    /// panic. i.e., When the end of the format string hasn't been reached yet.
238    fn bump_fmt(&mut self) -> bool {
239        self.fmt = &self.fmt[1..];
240        !self.fmt.is_empty()
241    }
242
243    /// Decodes a Unicode scalar value from the beginning of `fmt` and advances
244    /// the parser accordingly.
245    ///
246    /// If a Unicode scalar value could not be decoded, then an error is
247    /// returned.
248    ///
249    /// It would be nice to just pass through bytes as-is instead of doing
250    /// actual UTF-8 decoding, but since the `Write` trait only represents
251    /// Unicode-accepting buffers, we need to actually do decoding here.
252    ///
253    /// # Errors
254    ///
255    /// Unless lenient parsing is enabled, this returns an error if UTF-8
256    /// decoding failed. When lenient parsing is enabled, decoding errors
257    /// are turned into the Unicode replacement codepoint via the
258    /// "substitution of maximal subparts" strategy.
259    ///
260    /// # Panics
261    ///
262    /// When `self.fmt` is empty. i.e., Only call this when you know there is
263    /// some remaining bytes to parse.
264    #[cold]
265    #[inline(never)]
266    fn utf8_decode_and_bump(&mut self) -> Result<char, FE> {
267        match utf8::decode(self.fmt).expect("non-empty fmt") {
268            Ok(ch) => {
269                self.fmt = &self.fmt[ch.len_utf8()..];
270                return Ok(ch);
271            }
272            Err(err) if self.config.lenient => {
273                self.fmt = &self.fmt[err.len()..];
274                return Ok(char::REPLACEMENT_CHARACTER);
275            }
276            Err(_) => Err(FE::InvalidUtf8),
277        }
278    }
279
280    /// Parses optional extensions before a specifier directive. That is, right
281    /// after the `%`. If any extensions are parsed, the parser is bumped
282    /// to the next byte. (If no next byte exists, then an error is returned.)
283    #[cfg_attr(feature = "perf-inline", inline(always))]
284    fn parse_extension(&mut self) -> Result<Extension, Error> {
285        if self.f().is_ascii_alphabetic() {
286            return Ok(Extension { flag: None, width: None, colons: 0 });
287        }
288        let flag = self.parse_flag()?;
289        let width = self.parse_width()?;
290        let colons = self.parse_colons()?;
291        Ok(Extension { flag, width, colons })
292    }
293
294    /// Parses an optional flag. And if one is parsed, the parser is bumped
295    /// to the next byte. (If no next byte exists, then an error is returned.)
296    #[cfg_attr(feature = "perf-inline", inline(always))]
297    fn parse_flag(&mut self) -> Result<Option<Flag>, Error> {
298        let (flag, fmt) = Extension::parse_flag(self.fmt)?;
299        self.fmt = fmt;
300        Ok(flag)
301    }
302
303    /// Parses an optional width that comes after a (possibly absent) flag and
304    /// before the specifier directive itself. And if a width is parsed, the
305    /// parser is bumped to the next byte. (If no next byte exists, then an
306    /// error is returned.)
307    ///
308    /// Note that this is also used to parse precision settings for `%f` and
309    /// `%.f`. In the former case, the width is just re-interpreted as a
310    /// precision setting. In the latter case, something like `%5.9f` is
311    /// technically valid, but the `5` is ignored.
312    #[cfg_attr(feature = "perf-inline", inline(always))]
313    fn parse_width(&mut self) -> Result<Option<u8>, Error> {
314        let (width, fmt) = Extension::parse_width(self.fmt)?;
315        self.fmt = fmt;
316        Ok(width)
317    }
318
319    /// Parses an optional number of colons (up to 3) immediately before a
320    /// conversion specifier.
321    #[cfg_attr(feature = "perf-inline", inline(always))]
322    fn parse_colons(&mut self) -> Result<u8, Error> {
323        let (colons, fmt) = Extension::parse_colons(self.fmt)?;
324        self.fmt = fmt;
325        Ok(colons)
326    }
327
328    #[cfg_attr(feature = "perf-inline", inline(always))]
329    fn write_item(
330        &mut self,
331        ext: &Extension,
332        item: &Item,
333    ) -> Result<(), Error> {
334        match *item {
335            Item::AlreadyFormatted => Ok(()),
336            Item::Integer(ref item) => ext.write_int(
337                item.pad_byte,
338                item.pad_width,
339                item.number,
340                self.wtr,
341            ),
342            Item::Fraction(ItemFraction { width, dot, subsec }) => {
343                if dot {
344                    self.wtr.write_ascii_char(b'.')?;
345                }
346                self.wtr.write_fraction(width, subsec)
347            }
348            Item::String(ref item) => {
349                ext.write_str(item.case, item.string, self.wtr)
350            }
351            Item::Offset(ref item) => write_offset(
352                item.offset,
353                item.colon,
354                item.minute,
355                item.second,
356                self.wtr,
357            ),
358        }
359    }
360
361    // These are the formatting functions. They are pretty much responsible
362    // for getting what they need for the broken down time and reporting a
363    // decent failure mode if what they need couldn't be found. And then,
364    // of course, doing the actual formatting.
365
366    /// %P
367    fn fmt_ampm_lower(&self) -> Result<ItemString, Error> {
368        let meridiem = match self.tm.meridiem() {
369            Some(meridiem) => meridiem,
370            None => {
371                let hour = self.tm.hour().ok_or(FE::RequiresTime)?;
372                if hour < 12 {
373                    Meridiem::AM
374                } else {
375                    Meridiem::PM
376                }
377            }
378        };
379        let s = match meridiem {
380            Meridiem::AM => "am",
381            Meridiem::PM => "pm",
382        };
383        Ok(ItemString::new(Case::AsIs, s))
384    }
385
386    /// %p
387    fn fmt_ampm_upper(&self, ext: &Extension) -> Result<ItemString, Error> {
388        let meridiem = match self.tm.meridiem() {
389            Some(meridiem) => meridiem,
390            None => {
391                let hour = self.tm.hour().ok_or(FE::RequiresTime)?;
392                if hour < 12 {
393                    Meridiem::AM
394                } else {
395                    Meridiem::PM
396                }
397            }
398        };
399        // Manually specialize this case to avoid hitting `write_str_cold`.
400        let s = if matches!(ext.flag, Some(Flag::Swapcase)) {
401            match meridiem {
402                Meridiem::AM => "am",
403                Meridiem::PM => "pm",
404            }
405        } else {
406            match meridiem {
407                Meridiem::AM => "AM",
408                Meridiem::PM => "PM",
409            }
410        };
411        Ok(ItemString::new(Case::AsIs, s))
412    }
413
414    /// %D
415    fn fmt_american_date(&mut self) -> Result<Item, Error> {
416        let ItemInteger { number, .. } = self.fmt_month()?;
417        self.wtr.write_int_pad2(number.unsigned_abs())?;
418        self.wtr.write_ascii_char(b'/')?;
419        let ItemInteger { number, .. } = self.fmt_day_zero()?;
420        self.wtr.write_int_pad2(number.unsigned_abs())?;
421        self.wtr.write_ascii_char(b'/')?;
422        let ItemInteger { number, .. } = self.fmt_year2()?;
423        if number < 0 {
424            self.wtr.write_ascii_char(b'-')?;
425        }
426        self.wtr.write_int_pad2(number.unsigned_abs())?;
427        Ok(Item::AlreadyFormatted)
428    }
429
430    /// %R
431    fn fmt_clock_nosecs(&mut self) -> Result<Item, Error> {
432        let ItemInteger { number, .. } = self.fmt_hour24_zero()?;
433        self.wtr.write_int_pad2(number.unsigned_abs())?;
434        self.wtr.write_ascii_char(b':')?;
435        let ItemInteger { number, .. } = self.fmt_minute()?;
436        self.wtr.write_int_pad2(number.unsigned_abs())?;
437        Ok(Item::AlreadyFormatted)
438    }
439
440    /// %T
441    fn fmt_clock_secs(&mut self) -> Result<Item, Error> {
442        let ItemInteger { number, .. } = self.fmt_hour24_zero()?;
443        self.wtr.write_int_pad2(number.unsigned_abs())?;
444        self.wtr.write_ascii_char(b':')?;
445        let ItemInteger { number, .. } = self.fmt_minute()?;
446        self.wtr.write_int_pad2(number.unsigned_abs())?;
447        self.wtr.write_ascii_char(b':')?;
448        let ItemInteger { number, .. } = self.fmt_second()?;
449        self.wtr.write_int_pad2(number.unsigned_abs())?;
450        Ok(Item::AlreadyFormatted)
451    }
452
453    /// %F
454    fn fmt_iso_date(&mut self) -> Result<Item, Error> {
455        let ItemInteger { number, .. } = self.fmt_year()?;
456        if number < 0 {
457            self.wtr.write_ascii_char(b'-')?;
458        }
459        self.wtr.write_int_pad4(number.unsigned_abs())?;
460        self.wtr.write_ascii_char(b'-')?;
461        let ItemInteger { number, .. } = self.fmt_month()?;
462        self.wtr.write_int_pad2(number.unsigned_abs())?;
463        self.wtr.write_ascii_char(b'-')?;
464        let ItemInteger { number, .. } = self.fmt_day_zero()?;
465        self.wtr.write_int_pad2(number.unsigned_abs())?;
466        Ok(Item::AlreadyFormatted)
467    }
468
469    /// %d
470    fn fmt_day_zero(&self) -> Result<ItemInteger, Error> {
471        let day = self
472            .tm
473            .day()
474            .or_else(|| self.tm.to_date().ok().map(|d| d.day()))
475            .ok_or(FE::RequiresDate)?;
476        Ok(ItemInteger::new(b'0', 2, day))
477    }
478
479    /// %e
480    fn fmt_day_space(&self) -> Result<ItemInteger, Error> {
481        let day = self
482            .tm
483            .day()
484            .or_else(|| self.tm.to_date().ok().map(|d| d.day()))
485            .ok_or(FE::RequiresDate)?;
486        Ok(ItemInteger::new(b' ', 2, day))
487    }
488
489    /// %I
490    fn fmt_hour12_zero(&self) -> Result<ItemInteger, Error> {
491        let mut hour = self.tm.hour().ok_or(FE::RequiresTime)?;
492        if hour == 0 {
493            hour = 12;
494        } else if hour > 12 {
495            hour -= 12;
496        }
497        Ok(ItemInteger::new(b'0', 2, hour))
498    }
499
500    /// %H
501    fn fmt_hour24_zero(&self) -> Result<ItemInteger, Error> {
502        let hour = self.tm.hour().ok_or(FE::RequiresTime)?;
503        Ok(ItemInteger::new(b'0', 2, hour))
504    }
505
506    /// %l
507    fn fmt_hour12_space(&self) -> Result<ItemInteger, Error> {
508        let mut hour = self.tm.hour().ok_or(FE::RequiresTime)?;
509        if hour == 0 {
510            hour = 12;
511        } else if hour > 12 {
512            hour -= 12;
513        }
514        Ok(ItemInteger::new(b' ', 2, hour))
515    }
516
517    /// %k
518    fn fmt_hour24_space(&self) -> Result<ItemInteger, Error> {
519        let hour = self.tm.hour().ok_or(FE::RequiresTime)?;
520        Ok(ItemInteger::new(b' ', 2, hour))
521    }
522
523    /// %M
524    fn fmt_minute(&self) -> Result<ItemInteger, Error> {
525        let minute = self.tm.minute().ok_or(FE::RequiresTime)?;
526        Ok(ItemInteger::new(b'0', 2, minute))
527    }
528
529    /// %m
530    fn fmt_month(&self) -> Result<ItemInteger, Error> {
531        let month = self
532            .tm
533            .month()
534            .or_else(|| self.tm.to_date().ok().map(|d| d.month()))
535            .ok_or(FE::RequiresDate)?;
536        Ok(ItemInteger::new(b'0', 2, month))
537    }
538
539    /// %B
540    fn fmt_month_full(&self) -> Result<ItemString, Error> {
541        let month = self
542            .tm
543            .month()
544            .or_else(|| self.tm.to_date().ok().map(|d| d.month()))
545            .ok_or(FE::RequiresDate)?;
546        Ok(ItemString::new(Case::AsIs, month_name_full(month)))
547    }
548
549    /// %b, %h
550    fn fmt_month_abbrev(&self) -> Result<ItemString, Error> {
551        let month = self
552            .tm
553            .month()
554            .or_else(|| self.tm.to_date().ok().map(|d| d.month()))
555            .ok_or(FE::RequiresDate)?;
556        Ok(ItemString::new(Case::AsIs, month_name_abbrev(month)))
557    }
558
559    /// %Q
560    fn fmt_iana_nocolon(&mut self) -> Result<Item, Error> {
561        let Some(iana) = self.tm.iana_time_zone() else {
562            let offset = self.tm.offset.ok_or(FE::RequiresTimeZoneOrOffset)?;
563            return Ok(Item::Offset(ItemOffset::new(
564                offset, false, true, false,
565            )));
566        };
567        self.wtr.write_str(iana)?;
568        Ok(Item::AlreadyFormatted)
569    }
570
571    /// %:Q
572    fn fmt_iana_colon(&mut self) -> Result<Item, Error> {
573        let Some(iana) = self.tm.iana_time_zone() else {
574            let offset = self.tm.offset.ok_or(FE::RequiresTimeZoneOrOffset)?;
575            return Ok(Item::Offset(ItemOffset::new(
576                offset, true, true, false,
577            )));
578        };
579        self.wtr.write_str(iana)?;
580        Ok(Item::AlreadyFormatted)
581    }
582
583    /// %z
584    fn fmt_offset_nocolon(&self) -> Result<ItemOffset, Error> {
585        let offset = self.tm.offset.ok_or(FE::RequiresOffset)?;
586        Ok(ItemOffset::new(offset, false, true, false))
587    }
588
589    /// %:z
590    fn fmt_offset_colon(&self) -> Result<ItemOffset, Error> {
591        let offset = self.tm.offset.ok_or(FE::RequiresOffset)?;
592        Ok(ItemOffset::new(offset, true, true, false))
593    }
594
595    /// %::z
596    fn fmt_offset_colon2(&self) -> Result<ItemOffset, Error> {
597        let offset = self.tm.offset.ok_or(FE::RequiresOffset)?;
598        Ok(ItemOffset::new(offset, true, true, true))
599    }
600
601    /// %:::z
602    fn fmt_offset_colon3(&self) -> Result<ItemOffset, Error> {
603        let offset = self.tm.offset.ok_or(FE::RequiresOffset)?;
604        Ok(ItemOffset::new(offset, true, false, false))
605    }
606
607    /// %S
608    fn fmt_second(&self) -> Result<ItemInteger, Error> {
609        let second = self.tm.second().ok_or(FE::RequiresTime)?;
610        Ok(ItemInteger::new(b'0', 2, second))
611    }
612
613    /// %s
614    fn fmt_timestamp(&self) -> Result<ItemInteger, Error> {
615        let timestamp =
616            self.tm.to_timestamp().map_err(|_| FE::RequiresInstant)?;
617        Ok(ItemInteger::new(b' ', 0, timestamp.as_second()))
618    }
619
620    /// %f
621    fn fmt_fractional(&self, ext: &Extension) -> Result<Item, Error> {
622        let subsec = self.tm.subsec.ok_or(FE::RequiresTime)?;
623        let subsec = i32::from(subsec).unsigned_abs();
624        // For %f, we always want to emit at least one digit. The only way we
625        // wouldn't is if our fractional component is zero. One exception to
626        // this is when the width is `0` (which looks like `%00f`), in which
627        // case, we emit an error. We could allow it to emit an empty string,
628        // but this seems very odd. And an empty string cannot be parsed by
629        // `%f`.
630        if ext.width == Some(0) {
631            return Err(Error::from(FE::ZeroPrecisionFloat));
632        }
633        if subsec == 0 && ext.width.is_none() {
634            return Ok(Item::String(ItemString::new(Case::AsIs, "0")));
635        }
636        Ok(Item::Fraction(ItemFraction {
637            width: ext.width,
638            dot: false,
639            subsec,
640        }))
641    }
642
643    /// %.f
644    fn fmt_dot_fractional(&self, ext: &Extension) -> Result<Item, Error> {
645        let Some(subsec) = self.tm.subsec else {
646            return Ok(Item::AlreadyFormatted);
647        };
648        let subsec = i32::from(subsec).unsigned_abs();
649        if subsec == 0 && ext.width.is_none() || ext.width == Some(0) {
650            return Ok(Item::AlreadyFormatted);
651        }
652        Ok(Item::Fraction(ItemFraction {
653            width: ext.width,
654            dot: true,
655            subsec,
656        }))
657    }
658
659    /// %N
660    fn fmt_nanoseconds(&self, ext: &Extension) -> Result<Item, Error> {
661        let subsec = self.tm.subsec.ok_or(FE::RequiresTime)?;
662        if ext.width == Some(0) {
663            return Err(Error::from(FE::ZeroPrecisionNano));
664        }
665        let subsec = i32::from(subsec).unsigned_abs();
666        // Since `%N` is actually an alias for `%9f`, when the precision
667        // is missing, we default to 9.
668        if ext.width.is_none() {
669            return Ok(Item::Fraction(ItemFraction {
670                width: Some(9),
671                dot: false,
672                subsec,
673            }));
674        }
675        Ok(Item::Fraction(ItemFraction {
676            width: ext.width,
677            dot: false,
678            subsec,
679        }))
680    }
681
682    /// %Z
683    fn fmt_tzabbrev(&mut self, ext: &Extension) -> Result<Item, Error> {
684        let tz = self.tm.tz.as_ref().ok_or(FE::RequiresTimeZone)?;
685        let ts = self.tm.to_timestamp().map_err(|_| FE::RequiresInstant)?;
686        let oinfo = tz.to_offset_info(ts);
687        ext.write_str(Case::Upper, oinfo.abbreviation(), self.wtr)?;
688        Ok(Item::AlreadyFormatted)
689    }
690
691    /// %A
692    fn fmt_weekday_full(&self) -> Result<ItemString, Error> {
693        let weekday = self
694            .tm
695            .weekday
696            .or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
697            .ok_or(FE::RequiresDate)?;
698        Ok(ItemString::new(Case::AsIs, weekday_name_full(weekday)))
699    }
700
701    /// %a
702    fn fmt_weekday_abbrev(&self) -> Result<ItemString, Error> {
703        let weekday = self
704            .tm
705            .weekday
706            .or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
707            .ok_or(FE::RequiresDate)?;
708        Ok(ItemString::new(Case::AsIs, weekday_name_abbrev(weekday)))
709    }
710
711    /// %u
712    fn fmt_weekday_mon(&self) -> Result<ItemInteger, Error> {
713        let weekday = self
714            .tm
715            .weekday
716            .or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
717            .ok_or(FE::RequiresDate)?;
718        Ok(ItemInteger::new(b' ', 0, weekday.to_monday_one_offset()))
719    }
720
721    /// %w
722    fn fmt_weekday_sun(&self) -> Result<ItemInteger, Error> {
723        let weekday = self
724            .tm
725            .weekday
726            .or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
727            .ok_or(FE::RequiresDate)?;
728        Ok(ItemInteger::new(b' ', 0, weekday.to_sunday_zero_offset()))
729    }
730
731    /// %U
732    fn fmt_week_sun(&self) -> Result<ItemInteger, Error> {
733        // Short circuit if the week number was explicitly set.
734        if let Some(weeknum) = self.tm.week_sun {
735            return Ok(ItemInteger::new(b'0', 2, weeknum));
736        }
737        let day = self
738            .tm
739            .day_of_year()
740            .or_else(|| self.tm.to_date().ok().map(|d| d.day_of_year()))
741            .ok_or(FE::RequiresDate)?;
742        let weekday = self
743            .tm
744            .weekday
745            .or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
746            .ok_or(FE::RequiresDate)?
747            .to_sunday_zero_offset();
748        // Example: 2025-01-05 is the first Sunday in 2025, and thus the start
749        // of week 1. This means that 2025-01-04 (Saturday) is in week 0.
750        //
751        // So for 2025-01-05, day=5 and weekday=0. Thus we get 11/7 = 1.
752        // For 2025-01-04, day=4 and weekday=6. Thus we get 4/7 = 0.
753        let weeknum = (day + 6 - i16::from(weekday)) / 7;
754        Ok(ItemInteger::new(b'0', 2, weeknum))
755    }
756
757    /// %V
758    fn fmt_week_iso(&self) -> Result<ItemInteger, Error> {
759        let weeknum = self
760            .tm
761            .iso_week()
762            .or_else(|| {
763                self.tm.to_date().ok().map(|d| d.iso_week_date().week())
764            })
765            .ok_or(FE::RequiresDate)?;
766        Ok(ItemInteger::new(b'0', 2, weeknum))
767    }
768
769    /// %W
770    fn fmt_week_mon(&self) -> Result<ItemInteger, Error> {
771        // Short circuit if the week number was explicitly set.
772        if let Some(weeknum) = self.tm.week_mon {
773            return Ok(ItemInteger::new(b'0', 2, weeknum));
774        }
775        let day = self
776            .tm
777            .day_of_year()
778            .or_else(|| self.tm.to_date().ok().map(|d| d.day_of_year()))
779            .ok_or(FE::RequiresDate)?;
780        let weekday = self
781            .tm
782            .weekday
783            .or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
784            .ok_or(FE::RequiresDate)?
785            .to_sunday_zero_offset();
786        // Example: 2025-01-06 is the first Monday in 2025, and thus the start
787        // of week 1. This means that 2025-01-05 (Sunday) is in week 0.
788        //
789        // So for 2025-01-06, day=6 and weekday=1. Thus we get 12/7 = 1.
790        // For 2025-01-05, day=5 and weekday=7. Thus we get 5/7 = 0.
791        let weeknum = (day + 6 - ((i16::from(weekday) + 6) % 7)) / 7;
792        Ok(ItemInteger::new(b'0', 2, weeknum))
793    }
794
795    /// %Y
796    fn fmt_year(&self) -> Result<ItemInteger, Error> {
797        let year = self
798            .tm
799            .year()
800            .or_else(|| self.tm.to_date().ok().map(|d| d.year()))
801            .ok_or(FE::RequiresDate)?;
802        Ok(ItemInteger::new(b'0', 4, year))
803    }
804
805    /// %y
806    fn fmt_year2(&self) -> Result<ItemInteger, Error> {
807        let year = self
808            .tm
809            .year()
810            .or_else(|| self.tm.to_date().ok().map(|d| d.year()))
811            .ok_or(FE::RequiresDate)?;
812        let year = year % 100;
813        Ok(ItemInteger::new(b'0', 2, year))
814    }
815
816    /// %C
817    fn fmt_century(&self) -> Result<ItemInteger, Error> {
818        let year = self
819            .tm
820            .year()
821            .or_else(|| self.tm.to_date().ok().map(|d| d.year()))
822            .ok_or(FE::RequiresDate)?;
823        let century = year / 100;
824        Ok(ItemInteger::new(b' ', 0, century))
825    }
826
827    /// %G
828    fn fmt_iso_week_year(&self) -> Result<ItemInteger, Error> {
829        let year = self
830            .tm
831            .iso_week_year()
832            .or_else(|| {
833                self.tm.to_date().ok().map(|d| d.iso_week_date().year())
834            })
835            .ok_or(FE::RequiresDate)?;
836        Ok(ItemInteger::new(b'0', 4, year))
837    }
838
839    /// %g
840    fn fmt_iso_week_year2(&self) -> Result<ItemInteger, Error> {
841        let year = self
842            .tm
843            .iso_week_year()
844            .or_else(|| {
845                self.tm.to_date().ok().map(|d| d.iso_week_date().year())
846            })
847            .ok_or(FE::RequiresDate)?;
848        let year = year % 100;
849        Ok(ItemInteger::new(b'0', 2, year))
850    }
851
852    /// %q
853    fn fmt_quarter(&self) -> Result<ItemInteger, Error> {
854        let month = self
855            .tm
856            .month()
857            .or_else(|| self.tm.to_date().ok().map(|d| d.month()))
858            .ok_or(FE::RequiresDate)?;
859        let quarter = match month {
860            1..=3 => 1,
861            4..=6 => 2,
862            7..=9 => 3,
863            10..=12 => 4,
864            _ => unreachable!(),
865        };
866        Ok(ItemInteger::new(b'0', 0, quarter))
867    }
868
869    /// %j
870    fn fmt_day_of_year(&self) -> Result<ItemInteger, Error> {
871        let day = self
872            .tm
873            .day_of_year()
874            .or_else(|| self.tm.to_date().ok().map(|d| d.day_of_year()))
875            .ok_or(FE::RequiresDate)?;
876        Ok(ItemInteger::new(b'0', 3, day))
877    }
878
879    /// %n, %t
880    fn fmt_literal(&self, literal: &'static str) -> Result<ItemString, Error> {
881        Ok(ItemString::new(Case::AsIs, literal))
882    }
883
884    /// %c
885    fn fmt_datetime(&mut self, ext: &Extension) -> Result<Item, Error> {
886        self.config.custom.format_datetime(
887            self.config,
888            ext,
889            self.tm,
890            self.wtr,
891        )?;
892        Ok(Item::AlreadyFormatted)
893    }
894
895    /// %x
896    fn fmt_date(&mut self, ext: &Extension) -> Result<Item, Error> {
897        self.config.custom.format_date(self.config, ext, self.tm, self.wtr)?;
898        Ok(Item::AlreadyFormatted)
899    }
900
901    /// %X
902    fn fmt_time(&mut self, ext: &Extension) -> Result<Item, Error> {
903        self.config.custom.format_time(self.config, ext, self.tm, self.wtr)?;
904        Ok(Item::AlreadyFormatted)
905    }
906
907    /// %r
908    fn fmt_12hour_time(&mut self, ext: &Extension) -> Result<Item, Error> {
909        self.config.custom.format_12hour_time(
910            self.config,
911            ext,
912            self.tm,
913            self.wtr,
914        )?;
915        Ok(Item::AlreadyFormatted)
916    }
917}
918
919/// Writes the given time zone offset to the writer.
920///
921/// When `colon` is true, the hour, minute and optional second components are
922/// delimited by a colon. Otherwise, no delimiter is used.
923///
924/// When `minute` is true, the minute component is always printed. When
925/// false, the minute component is only printed when it is non-zero (or if
926/// the second component is non-zero).
927///
928/// When `second` is true, the second component is always printed. When false,
929/// the second component is only printed when it is non-zero.
930#[cfg_attr(feature = "perf-inline", inline(always))]
931fn write_offset(
932    offset: Offset,
933    colon: bool,
934    minute: bool,
935    second: bool,
936    wtr: &mut BorrowedWriter<'_, '_, '_>,
937) -> Result<(), Error> {
938    let total_seconds = offset.seconds().unsigned_abs();
939    let hours = (total_seconds / (60 * 60)) as u8;
940    let minutes = ((total_seconds / 60) % 60) as u8;
941    let seconds = (total_seconds % 60) as u8;
942
943    wtr.write_ascii_char(if offset.is_negative() { b'-' } else { b'+' })?;
944    wtr.write_int_pad2(hours)?;
945    if minute || minutes != 0 || seconds != 0 {
946        if colon {
947            wtr.write_ascii_char(b':')?;
948        }
949        wtr.write_int_pad2(minutes)?;
950        if second || seconds != 0 {
951            if colon {
952                wtr.write_ascii_char(b':')?;
953            }
954            wtr.write_int_pad2(seconds)?;
955        }
956    }
957    Ok(())
958}
959
960impl Extension {
961    /// Writes the given string using the default case rule provided, unless
962    /// an option in this extension config overrides the default case.
963    #[cfg_attr(feature = "perf-inline", inline(always))]
964    fn write_str(
965        &self,
966        default: Case,
967        string: &str,
968        wtr: &mut BorrowedWriter<'_, '_, '_>,
969    ) -> Result<(), Error> {
970        if self.flag.is_none() && matches!(default, Case::AsIs) {
971            return wtr.write_str(string);
972        }
973        self.write_str_cold(default, string, wtr)
974    }
975
976    #[cold]
977    #[inline(never)]
978    fn write_str_cold(
979        &self,
980        default: Case,
981        string: &str,
982        wtr: &mut BorrowedWriter<'_, '_, '_>,
983    ) -> Result<(), Error> {
984        let case = match self.flag {
985            Some(Flag::Uppercase) => Case::Upper,
986            Some(Flag::Swapcase) => default.swap(),
987            _ => default,
988        };
989        match case {
990            Case::AsIs => {
991                wtr.write_str(string)?;
992            }
993            Case::Upper | Case::Lower => {
994                for ch in string.chars() {
995                    wtr.write_char(if matches!(case, Case::Upper) {
996                        ch.to_ascii_uppercase()
997                    } else {
998                        ch.to_ascii_lowercase()
999                    })?;
1000                }
1001            }
1002        }
1003        Ok(())
1004    }
1005
1006    /// Writes the given integer using the given padding width and byte, unless
1007    /// an option in this extension config overrides a default setting.
1008    #[cfg_attr(feature = "perf-inline", inline(always))]
1009    fn write_int(
1010        &self,
1011        pad_byte: u8,
1012        pad_width: u8,
1013        number: impl Into<i64>,
1014        wtr: &mut BorrowedWriter<'_, '_, '_>,
1015    ) -> Result<(), Error> {
1016        let number = number.into();
1017        let pad_byte = match self.flag {
1018            Some(Flag::PadZero) => b'0',
1019            Some(Flag::PadSpace) => b' ',
1020            _ => pad_byte,
1021        };
1022        let pad_width = if matches!(self.flag, Some(Flag::NoPad)) {
1023            0
1024        } else {
1025            self.width.unwrap_or(pad_width)
1026        };
1027        if number < 0 {
1028            // Special case formatting a negative integer with non-zero
1029            // padding. This case isn't handled by the `BorrowedWriter`
1030            // API, so just handle it specially instead of bloating up
1031            // the hot path.
1032            if pad_width > 0 {
1033                return Self::write_negative_int(
1034                    pad_byte,
1035                    pad_width,
1036                    number.unsigned_abs(),
1037                    wtr,
1038                );
1039            }
1040            wtr.write_ascii_char(b'-')?;
1041        }
1042        let number = number.unsigned_abs();
1043        match (pad_byte, pad_width) {
1044            (b'0', 2) if number <= 99 => wtr.write_int_pad2(number),
1045            (b' ', 2) if number <= 99 => wtr.write_int_pad2_space(number),
1046            (b'0', 4) if number <= 9999 => wtr.write_int_pad4(number),
1047            _ => wtr.write_int_pad(number, pad_byte, pad_width),
1048        }
1049    }
1050
1051    /// Writes a negative integer with explicit width, where sign is part of
1052    /// total width.
1053    #[cold]
1054    #[inline(never)]
1055    fn write_negative_int(
1056        pad_byte: u8,
1057        pad_width: u8,
1058        number: u64,
1059        wtr: &mut BorrowedWriter<'_, '_, '_>,
1060    ) -> Result<(), Error> {
1061        // The sign is part of the total width, so offset the
1062        // padding to account for it.
1063        let mut pad_width = pad_width.saturating_sub(1);
1064        // When padding to spaces, we want the padding first
1065        // and then the `-` sign. So handle the padding here
1066        // explicitly. For padding to zeros, we handle it
1067        // normally (other than offsetting the padding width
1068        // above).
1069        if pad_byte == b' ' {
1070            let d = 1 + number.checked_ilog10().unwrap_or(0) as u8;
1071            for _ in 0..pad_width.saturating_sub(d) {
1072                wtr.write_ascii_char(b' ')?;
1073            }
1074            // Don't do any padding to the call below.
1075            // We could instead add a `BorrowedWriter::write_int`
1076            // routine, but I don't think it's worth doing for
1077            // this case.
1078            pad_width = 0;
1079        }
1080        wtr.write_ascii_char(b'-')?;
1081        wtr.write_int_pad(number, pad_byte, pad_width)
1082    }
1083}
1084
1085/// The case to use when printing a string like weekday or TZ abbreviation.
1086#[derive(Clone, Copy, Debug)]
1087enum Case {
1088    AsIs,
1089    Upper,
1090    Lower,
1091}
1092
1093impl Case {
1094    /// Swap upper to lowercase, and lower to uppercase.
1095    fn swap(self) -> Case {
1096        match self {
1097            Case::AsIs => Case::AsIs,
1098            Case::Upper => Case::Lower,
1099            Case::Lower => Case::Upper,
1100        }
1101    }
1102}
1103
1104#[cfg(feature = "alloc")]
1105#[cfg(test)]
1106mod tests {
1107    use crate::{
1108        civil::{date, time, Date, DateTime, Time},
1109        fmt::strtime::{format, BrokenDownTime, Config, PosixCustom},
1110        tz::Offset,
1111        Timestamp, Zoned,
1112    };
1113
1114    #[test]
1115    fn ok_format_american_date() {
1116        let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1117
1118        insta::assert_snapshot!(f("%D", date(2024, 7, 9)), @"07/09/24");
1119        insta::assert_snapshot!(f("%-D", date(2024, 7, 9)), @"07/09/24");
1120        insta::assert_snapshot!(f("%3D", date(2024, 7, 9)), @"07/09/24");
1121        insta::assert_snapshot!(f("%03D", date(2024, 7, 9)), @"07/09/24");
1122    }
1123
1124    #[test]
1125    fn ok_format_ampm() {
1126        let f = |fmt: &str, time: Time| format(fmt, time).unwrap();
1127
1128        insta::assert_snapshot!(f("%H%P", time(9, 0, 0, 0)), @"09am");
1129        insta::assert_snapshot!(f("%H%P", time(11, 0, 0, 0)), @"11am");
1130        insta::assert_snapshot!(f("%H%P", time(23, 0, 0, 0)), @"23pm");
1131        insta::assert_snapshot!(f("%H%P", time(0, 0, 0, 0)), @"00am");
1132
1133        insta::assert_snapshot!(f("%H%p", time(9, 0, 0, 0)), @"09AM");
1134        insta::assert_snapshot!(f("%H%p", time(11, 0, 0, 0)), @"11AM");
1135        insta::assert_snapshot!(f("%H%p", time(23, 0, 0, 0)), @"23PM");
1136        insta::assert_snapshot!(f("%H%p", time(0, 0, 0, 0)), @"00AM");
1137
1138        insta::assert_snapshot!(f("%H%#p", time(9, 0, 0, 0)), @"09am");
1139    }
1140
1141    #[test]
1142    fn ok_format_clock() {
1143        let f = |fmt: &str, time: Time| format(fmt, time).unwrap();
1144
1145        insta::assert_snapshot!(f("%R", time(23, 59, 8, 0)), @"23:59");
1146        insta::assert_snapshot!(f("%T", time(23, 59, 8, 0)), @"23:59:08");
1147    }
1148
1149    #[test]
1150    fn ok_format_day() {
1151        let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1152
1153        insta::assert_snapshot!(f("%d", date(2024, 7, 9)), @"09");
1154        insta::assert_snapshot!(f("%0d", date(2024, 7, 9)), @"09");
1155        insta::assert_snapshot!(f("%-d", date(2024, 7, 9)), @"9");
1156        insta::assert_snapshot!(f("%_d", date(2024, 7, 9)), @" 9");
1157
1158        insta::assert_snapshot!(f("%e", date(2024, 7, 9)), @" 9");
1159        insta::assert_snapshot!(f("%0e", date(2024, 7, 9)), @"09");
1160        insta::assert_snapshot!(f("%-e", date(2024, 7, 9)), @"9");
1161        insta::assert_snapshot!(f("%_e", date(2024, 7, 9)), @" 9");
1162    }
1163
1164    #[test]
1165    fn ok_format_iso_date() {
1166        let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1167
1168        insta::assert_snapshot!(f("%F", date(2024, 7, 9)), @"2024-07-09");
1169        insta::assert_snapshot!(f("%-F", date(2024, 7, 9)), @"2024-07-09");
1170        insta::assert_snapshot!(f("%3F", date(2024, 7, 9)), @"2024-07-09");
1171        insta::assert_snapshot!(f("%03F", date(2024, 7, 9)), @"2024-07-09");
1172    }
1173
1174    #[test]
1175    fn ok_format_hour() {
1176        let f = |fmt: &str, time: Time| format(fmt, time).unwrap();
1177
1178        insta::assert_snapshot!(f("%H", time(9, 0, 0, 0)), @"09");
1179        insta::assert_snapshot!(f("%H", time(11, 0, 0, 0)), @"11");
1180        insta::assert_snapshot!(f("%H", time(23, 0, 0, 0)), @"23");
1181        insta::assert_snapshot!(f("%H", time(0, 0, 0, 0)), @"00");
1182
1183        insta::assert_snapshot!(f("%I", time(9, 0, 0, 0)), @"09");
1184        insta::assert_snapshot!(f("%I", time(11, 0, 0, 0)), @"11");
1185        insta::assert_snapshot!(f("%I", time(23, 0, 0, 0)), @"11");
1186        insta::assert_snapshot!(f("%I", time(0, 0, 0, 0)), @"12");
1187
1188        insta::assert_snapshot!(f("%k", time(9, 0, 0, 0)), @" 9");
1189        insta::assert_snapshot!(f("%k", time(11, 0, 0, 0)), @"11");
1190        insta::assert_snapshot!(f("%k", time(23, 0, 0, 0)), @"23");
1191        insta::assert_snapshot!(f("%k", time(0, 0, 0, 0)), @" 0");
1192
1193        insta::assert_snapshot!(f("%l", time(9, 0, 0, 0)), @" 9");
1194        insta::assert_snapshot!(f("%l", time(11, 0, 0, 0)), @"11");
1195        insta::assert_snapshot!(f("%l", time(23, 0, 0, 0)), @"11");
1196        insta::assert_snapshot!(f("%l", time(0, 0, 0, 0)), @"12");
1197    }
1198
1199    #[test]
1200    fn ok_format_minute() {
1201        let f = |fmt: &str, time: Time| format(fmt, time).unwrap();
1202
1203        insta::assert_snapshot!(f("%M", time(0, 9, 0, 0)), @"09");
1204        insta::assert_snapshot!(f("%M", time(0, 11, 0, 0)), @"11");
1205        insta::assert_snapshot!(f("%M", time(0, 23, 0, 0)), @"23");
1206        insta::assert_snapshot!(f("%M", time(0, 0, 0, 0)), @"00");
1207    }
1208
1209    #[test]
1210    fn ok_format_month() {
1211        let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1212
1213        insta::assert_snapshot!(f("%m", date(2024, 7, 14)), @"07");
1214        insta::assert_snapshot!(f("%m", date(2024, 12, 14)), @"12");
1215        insta::assert_snapshot!(f("%0m", date(2024, 7, 14)), @"07");
1216        insta::assert_snapshot!(f("%0m", date(2024, 12, 14)), @"12");
1217        insta::assert_snapshot!(f("%-m", date(2024, 7, 14)), @"7");
1218        insta::assert_snapshot!(f("%-m", date(2024, 12, 14)), @"12");
1219        insta::assert_snapshot!(f("%_m", date(2024, 7, 14)), @" 7");
1220        insta::assert_snapshot!(f("%_m", date(2024, 12, 14)), @"12");
1221    }
1222
1223    #[test]
1224    fn ok_format_month_name() {
1225        let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1226
1227        insta::assert_snapshot!(f("%B", date(2024, 7, 14)), @"July");
1228        insta::assert_snapshot!(f("%b", date(2024, 7, 14)), @"Jul");
1229        insta::assert_snapshot!(f("%h", date(2024, 7, 14)), @"Jul");
1230
1231        insta::assert_snapshot!(f("%#B", date(2024, 7, 14)), @"July");
1232        insta::assert_snapshot!(f("%^B", date(2024, 7, 14)), @"JULY");
1233    }
1234
1235    #[test]
1236    fn ok_format_offset_from_zoned() {
1237        if crate::tz::db().is_definitively_empty() {
1238            return;
1239        }
1240
1241        let f = |fmt: &str, zdt: &Zoned| format(fmt, zdt).unwrap();
1242
1243        let zdt = date(2024, 7, 14)
1244            .at(22, 24, 0, 0)
1245            .in_tz("America/New_York")
1246            .unwrap();
1247        insta::assert_snapshot!(f("%z", &zdt), @"-0400");
1248        insta::assert_snapshot!(f("%:z", &zdt), @"-04:00");
1249
1250        let zdt = zdt.checked_add(crate::Span::new().months(5)).unwrap();
1251        insta::assert_snapshot!(f("%z", &zdt), @"-0500");
1252        insta::assert_snapshot!(f("%:z", &zdt), @"-05:00");
1253    }
1254
1255    #[test]
1256    fn ok_format_offset_plain() {
1257        let o = |h: i8, m: i8, s: i8| -> Offset { Offset::hms(h, m, s) };
1258        let f = |fmt: &str, offset: Offset| {
1259            let mut tm = BrokenDownTime::default();
1260            tm.set_offset(Some(offset));
1261            tm.to_string(fmt).unwrap()
1262        };
1263
1264        insta::assert_snapshot!(f("%z", o(0, 0, 0)), @"+0000");
1265        insta::assert_snapshot!(f("%:z", o(0, 0, 0)), @"+00:00");
1266        insta::assert_snapshot!(f("%::z", o(0, 0, 0)), @"+00:00:00");
1267        insta::assert_snapshot!(f("%:::z", o(0, 0, 0)), @"+00");
1268
1269        insta::assert_snapshot!(f("%z", -o(4, 0, 0)), @"-0400");
1270        insta::assert_snapshot!(f("%:z", -o(4, 0, 0)), @"-04:00");
1271        insta::assert_snapshot!(f("%::z", -o(4, 0, 0)), @"-04:00:00");
1272        insta::assert_snapshot!(f("%:::z", -o(4, 0, 0)), @"-04");
1273
1274        insta::assert_snapshot!(f("%z", o(5, 30, 0)), @"+0530");
1275        insta::assert_snapshot!(f("%:z", o(5, 30, 0)), @"+05:30");
1276        insta::assert_snapshot!(f("%::z", o(5, 30, 0)), @"+05:30:00");
1277        insta::assert_snapshot!(f("%:::z", o(5, 30, 0)), @"+05:30");
1278
1279        insta::assert_snapshot!(f("%z", o(5, 30, 15)), @"+053015");
1280        insta::assert_snapshot!(f("%:z", o(5, 30, 15)), @"+05:30:15");
1281        insta::assert_snapshot!(f("%::z", o(5, 30, 15)), @"+05:30:15");
1282        insta::assert_snapshot!(f("%:::z", o(5, 30, 15)), @"+05:30:15");
1283
1284        insta::assert_snapshot!(f("%z", o(5, 0, 15)), @"+050015");
1285        insta::assert_snapshot!(f("%:z", o(5, 0, 15)), @"+05:00:15");
1286        insta::assert_snapshot!(f("%::z", o(5, 0, 15)), @"+05:00:15");
1287        insta::assert_snapshot!(f("%:::z", o(5, 0, 15)), @"+05:00:15");
1288    }
1289
1290    #[test]
1291    fn ok_format_second() {
1292        let f = |fmt: &str, time: Time| format(fmt, time).unwrap();
1293
1294        insta::assert_snapshot!(f("%S", time(0, 0, 9, 0)), @"09");
1295        insta::assert_snapshot!(f("%S", time(0, 0, 11, 0)), @"11");
1296        insta::assert_snapshot!(f("%S", time(0, 0, 23, 0)), @"23");
1297        insta::assert_snapshot!(f("%S", time(0, 0, 0, 0)), @"00");
1298    }
1299
1300    #[test]
1301    fn ok_format_subsec_nanosecond() {
1302        let f = |fmt: &str, time: Time| format(fmt, time).unwrap();
1303        let mk = |subsec| time(0, 0, 0, subsec);
1304
1305        insta::assert_snapshot!(f("%f", mk(123_000_000)), @"123");
1306        insta::assert_snapshot!(f("%f", mk(0)), @"0");
1307        insta::assert_snapshot!(f("%3f", mk(0)), @"000");
1308        insta::assert_snapshot!(f("%3f", mk(123_000_000)), @"123");
1309        insta::assert_snapshot!(f("%6f", mk(123_000_000)), @"123000");
1310        insta::assert_snapshot!(f("%9f", mk(123_000_000)), @"123000000");
1311        insta::assert_snapshot!(f("%255f", mk(123_000_000)), @"123000000");
1312
1313        insta::assert_snapshot!(f("%.f", mk(123_000_000)), @".123");
1314        insta::assert_snapshot!(f("%.f", mk(0)), @"");
1315        insta::assert_snapshot!(f("%3.f", mk(0)), @"");
1316        insta::assert_snapshot!(f("%.3f", mk(0)), @".000");
1317        insta::assert_snapshot!(f("%.3f", mk(123_000_000)), @".123");
1318        insta::assert_snapshot!(f("%.6f", mk(123_000_000)), @".123000");
1319        insta::assert_snapshot!(f("%.9f", mk(123_000_000)), @".123000000");
1320        insta::assert_snapshot!(f("%.255f", mk(123_000_000)), @".123000000");
1321
1322        insta::assert_snapshot!(f("%3f", mk(123_456_789)), @"123");
1323        insta::assert_snapshot!(f("%6f", mk(123_456_789)), @"123456");
1324        insta::assert_snapshot!(f("%9f", mk(123_456_789)), @"123456789");
1325
1326        insta::assert_snapshot!(f("%.0f", mk(123_456_789)), @"");
1327        insta::assert_snapshot!(f("%.3f", mk(123_456_789)), @".123");
1328        insta::assert_snapshot!(f("%.6f", mk(123_456_789)), @".123456");
1329        insta::assert_snapshot!(f("%.9f", mk(123_456_789)), @".123456789");
1330
1331        insta::assert_snapshot!(f("%N", mk(123_000_000)), @"123000000");
1332        insta::assert_snapshot!(f("%N", mk(0)), @"000000000");
1333        insta::assert_snapshot!(f("%N", mk(000_123_000)), @"000123000");
1334        insta::assert_snapshot!(f("%3N", mk(0)), @"000");
1335        insta::assert_snapshot!(f("%3N", mk(123_000_000)), @"123");
1336        insta::assert_snapshot!(f("%6N", mk(123_000_000)), @"123000");
1337        insta::assert_snapshot!(f("%9N", mk(123_000_000)), @"123000000");
1338        insta::assert_snapshot!(f("%255N", mk(123_000_000)), @"123000000");
1339    }
1340
1341    #[test]
1342    fn ok_format_tzabbrev() {
1343        if crate::tz::db().is_definitively_empty() {
1344            return;
1345        }
1346
1347        let f = |fmt: &str, zdt: &Zoned| format(fmt, zdt).unwrap();
1348
1349        let zdt = date(2024, 7, 14)
1350            .at(22, 24, 0, 0)
1351            .in_tz("America/New_York")
1352            .unwrap();
1353        insta::assert_snapshot!(f("%Z", &zdt), @"EDT");
1354        insta::assert_snapshot!(f("%^Z", &zdt), @"EDT");
1355        insta::assert_snapshot!(f("%#Z", &zdt), @"edt");
1356
1357        let zdt = zdt.checked_add(crate::Span::new().months(5)).unwrap();
1358        insta::assert_snapshot!(f("%Z", &zdt), @"EST");
1359    }
1360
1361    #[test]
1362    fn ok_format_iana() {
1363        if crate::tz::db().is_definitively_empty() {
1364            return;
1365        }
1366
1367        let f = |fmt: &str, zdt: &Zoned| format(fmt, zdt).unwrap();
1368
1369        let zdt = date(2024, 7, 14)
1370            .at(22, 24, 0, 0)
1371            .in_tz("America/New_York")
1372            .unwrap();
1373        insta::assert_snapshot!(f("%Q", &zdt), @"America/New_York");
1374        insta::assert_snapshot!(f("%:Q", &zdt), @"America/New_York");
1375
1376        let zdt = date(2024, 7, 14)
1377            .at(22, 24, 0, 0)
1378            .to_zoned(crate::tz::offset(-4).to_time_zone())
1379            .unwrap();
1380        insta::assert_snapshot!(f("%Q", &zdt), @"-0400");
1381        insta::assert_snapshot!(f("%:Q", &zdt), @"-04:00");
1382
1383        let zdt = date(2024, 7, 14)
1384            .at(22, 24, 0, 0)
1385            .to_zoned(crate::tz::TimeZone::UTC)
1386            .unwrap();
1387        insta::assert_snapshot!(f("%Q", &zdt), @"UTC");
1388        insta::assert_snapshot!(f("%:Q", &zdt), @"UTC");
1389    }
1390
1391    #[test]
1392    fn ok_format_weekday_name() {
1393        let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1394
1395        insta::assert_snapshot!(f("%A", date(2024, 7, 14)), @"Sunday");
1396        insta::assert_snapshot!(f("%a", date(2024, 7, 14)), @"Sun");
1397
1398        insta::assert_snapshot!(f("%#A", date(2024, 7, 14)), @"Sunday");
1399        insta::assert_snapshot!(f("%^A", date(2024, 7, 14)), @"SUNDAY");
1400
1401        insta::assert_snapshot!(f("%u", date(2024, 7, 14)), @"7");
1402        insta::assert_snapshot!(f("%w", date(2024, 7, 14)), @"0");
1403    }
1404
1405    #[test]
1406    fn ok_format_year() {
1407        let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1408
1409        insta::assert_snapshot!(f("%Y", date(2024, 7, 14)), @"2024");
1410        insta::assert_snapshot!(f("%Y", date(24, 7, 14)), @"0024");
1411        insta::assert_snapshot!(f("%Y", date(-24, 7, 14)), @"-024");
1412
1413        insta::assert_snapshot!(f("%C", date(2024, 7, 14)), @"20");
1414        insta::assert_snapshot!(f("%C", date(1815, 7, 14)), @"18");
1415        insta::assert_snapshot!(f("%C", date(915, 7, 14)), @"9");
1416        insta::assert_snapshot!(f("%C", date(1, 7, 14)), @"0");
1417        insta::assert_snapshot!(f("%C", date(0, 7, 14)), @"0");
1418        insta::assert_snapshot!(f("%C", date(-1, 7, 14)), @"0");
1419        insta::assert_snapshot!(f("%C", date(-2024, 7, 14)), @"-20");
1420        insta::assert_snapshot!(f("%C", date(-1815, 7, 14)), @"-18");
1421        insta::assert_snapshot!(f("%C", date(-915, 7, 14)), @"-9");
1422    }
1423
1424    #[test]
1425    fn ok_format_year_negative_padded() {
1426        let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1427
1428        insta::assert_snapshot!(f("%06Y", date(-2025, 1, 13)), @"-02025");
1429        insta::assert_snapshot!(f("%06Y", date(-25, 1, 13)), @"-00025");
1430        insta::assert_snapshot!(f("%06Y", date(-1, 1, 13)), @"-00001");
1431        insta::assert_snapshot!(f("%08Y", date(-2025, 1, 13)), @"-0002025");
1432        insta::assert_snapshot!(f("%_6Y", date(-2025, 1, 13)), @" -2025");
1433        insta::assert_snapshot!(f("%_6Y", date(-25, 1, 13)), @"   -25");
1434        insta::assert_snapshot!(f("%_6Y", date(-1, 1, 13)), @"    -1");
1435        insta::assert_snapshot!(f("%_8Y", date(-2025, 1, 13)), @"   -2025");
1436        insta::assert_snapshot!(f("%06Y", date(2025, 1, 13)), @"002025");
1437        insta::assert_snapshot!(f("%_6Y", date(2025, 1, 13)), @"  2025");
1438        insta::assert_snapshot!(f("%Y", date(-2025, 1, 13)), @"-2025");
1439        insta::assert_snapshot!(f("%Y", date(-25, 1, 13)), @"-025");
1440        insta::assert_snapshot!(f("%_Y", date(-2025, 1, 13)), @"-2025");
1441        insta::assert_snapshot!(f("%_Y", date(-25, 1, 13)), @" -25");
1442        insta::assert_snapshot!(f("%03Y", date(-2025, 1, 13)), @"-2025");
1443        insta::assert_snapshot!(f("%_3Y", date(-2025, 1, 13)), @"-2025");
1444        insta::assert_snapshot!(f("%-6Y", date(-2025, 1, 13)), @"-2025");
1445        insta::assert_snapshot!(f("%04Y", date(-2025, 1, 13)), @"-2025");
1446        insta::assert_snapshot!(f("%05Y", date(-2025, 1, 13)), @"-2025");
1447    }
1448
1449    #[test]
1450    fn ok_format_default_locale() {
1451        let f = |fmt: &str, date: DateTime| format(fmt, date).unwrap();
1452
1453        insta::assert_snapshot!(
1454            f("%c", date(2024, 7, 14).at(0, 0, 0, 0)),
1455            @"2024 M07 14, Sun 00:00:00",
1456        );
1457        insta::assert_snapshot!(
1458            f("%c", date(24, 7, 14).at(0, 0, 0, 0)),
1459            @"0024 M07 14, Sun 00:00:00",
1460        );
1461        insta::assert_snapshot!(
1462            f("%c", date(-24, 7, 14).at(0, 0, 0, 0)),
1463            @"-024 M07 14, Wed 00:00:00",
1464        );
1465        insta::assert_snapshot!(
1466            f("%c", date(2024, 7, 14).at(17, 31, 59, 123_456_789)),
1467            @"2024 M07 14, Sun 17:31:59",
1468        );
1469
1470        insta::assert_snapshot!(
1471            f("%r", date(2024, 7, 14).at(8, 30, 0, 0)),
1472            @"8:30:00 AM",
1473        );
1474        insta::assert_snapshot!(
1475            f("%r", date(2024, 7, 14).at(17, 31, 59, 123_456_789)),
1476            @"5:31:59 PM",
1477        );
1478
1479        insta::assert_snapshot!(
1480            f("%x", date(2024, 7, 14).at(0, 0, 0, 0)),
1481            @"2024 M07 14",
1482        );
1483
1484        insta::assert_snapshot!(
1485            f("%X", date(2024, 7, 14).at(8, 30, 0, 0)),
1486            @"08:30:00",
1487        );
1488        insta::assert_snapshot!(
1489            f("%X", date(2024, 7, 14).at(17, 31, 59, 123_456_789)),
1490            @"17:31:59",
1491        );
1492    }
1493
1494    #[test]
1495    fn ok_format_compound_uppercase() {
1496        let f = |fmt: &str, date: DateTime| format(fmt, date).unwrap();
1497
1498        insta::assert_snapshot!(
1499            f("%^c", date(2024, 7, 14).at(0, 0, 0, 0)),
1500            @"2024 M07 14, SUN 00:00:00",
1501        );
1502        insta::assert_snapshot!(
1503            f("%^x", date(2024, 7, 14).at(0, 0, 0, 0)),
1504            @"2024 M07 14",
1505        );
1506        insta::assert_snapshot!(
1507            f("%^X", date(2024, 7, 14).at(8, 30, 0, 0)),
1508            @"08:30:00",
1509        );
1510        insta::assert_snapshot!(
1511            f("%^r", date(2024, 7, 14).at(8, 30, 0, 0)),
1512            @"8:30:00 AM",
1513        );
1514    }
1515
1516    #[test]
1517    fn ok_format_posix_locale() {
1518        let f = |fmt: &str, date: DateTime| {
1519            let config = Config::new().custom(PosixCustom::default());
1520            let tm = BrokenDownTime::from(date);
1521            tm.to_string_with_config(&config, fmt).unwrap()
1522        };
1523
1524        insta::assert_snapshot!(
1525            f("%c", date(2024, 7, 14).at(0, 0, 0, 0)),
1526            @"Sun Jul 14 00:00:00 2024",
1527        );
1528        insta::assert_snapshot!(
1529            f("%c", date(24, 7, 14).at(0, 0, 0, 0)),
1530            @"Sun Jul 14 00:00:00 0024",
1531        );
1532        insta::assert_snapshot!(
1533            f("%c", date(-24, 7, 14).at(0, 0, 0, 0)),
1534            @"Wed Jul 14 00:00:00 -024",
1535        );
1536        insta::assert_snapshot!(
1537            f("%c", date(2024, 7, 14).at(17, 31, 59, 123_456_789)),
1538            @"Sun Jul 14 17:31:59 2024",
1539        );
1540
1541        insta::assert_snapshot!(
1542            f("%r", date(2024, 7, 14).at(8, 30, 0, 0)),
1543            @"08:30:00 AM",
1544        );
1545        insta::assert_snapshot!(
1546            f("%r", date(2024, 7, 14).at(17, 31, 59, 123_456_789)),
1547            @"05:31:59 PM",
1548        );
1549
1550        insta::assert_snapshot!(
1551            f("%x", date(2024, 7, 14).at(0, 0, 0, 0)),
1552            @"07/14/24",
1553        );
1554
1555        insta::assert_snapshot!(
1556            f("%X", date(2024, 7, 14).at(8, 30, 0, 0)),
1557            @"08:30:00",
1558        );
1559        insta::assert_snapshot!(
1560            f("%X", date(2024, 7, 14).at(17, 31, 59, 123_456_789)),
1561            @"17:31:59",
1562        );
1563    }
1564
1565    #[test]
1566    fn ok_format_year_2digit() {
1567        let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1568
1569        insta::assert_snapshot!(f("%y", date(2024, 7, 14)), @"24");
1570        insta::assert_snapshot!(f("%y", date(2001, 7, 14)), @"01");
1571        insta::assert_snapshot!(f("%-y", date(2001, 7, 14)), @"1");
1572        insta::assert_snapshot!(f("%5y", date(2001, 7, 14)), @"00001");
1573        insta::assert_snapshot!(f("%-5y", date(2001, 7, 14)), @"1");
1574        insta::assert_snapshot!(f("%05y", date(2001, 7, 14)), @"00001");
1575        insta::assert_snapshot!(f("%_y", date(2001, 7, 14)), @" 1");
1576        insta::assert_snapshot!(f("%_5y", date(2001, 7, 14)), @"    1");
1577
1578        insta::assert_snapshot!(f("%y", date(1824, 7, 14)), @"24");
1579        insta::assert_snapshot!(f("%g", date(1824, 7, 14)), @"24");
1580    }
1581
1582    #[test]
1583    fn ok_format_iso_week_year() {
1584        let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1585
1586        insta::assert_snapshot!(f("%G", date(2019, 11, 30)), @"2019");
1587        insta::assert_snapshot!(f("%G", date(19, 11, 30)), @"0019");
1588        insta::assert_snapshot!(f("%G", date(-19, 11, 30)), @"-019");
1589
1590        // tricksy
1591        insta::assert_snapshot!(f("%G", date(2019, 12, 30)), @"2020");
1592    }
1593
1594    #[test]
1595    fn ok_format_week_num() {
1596        let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1597
1598        insta::assert_snapshot!(f("%U", date(2025, 1, 4)), @"00");
1599        insta::assert_snapshot!(f("%U", date(2025, 1, 5)), @"01");
1600
1601        insta::assert_snapshot!(f("%W", date(2025, 1, 5)), @"00");
1602        insta::assert_snapshot!(f("%W", date(2025, 1, 6)), @"01");
1603    }
1604
1605    #[test]
1606    fn ok_format_timestamp() {
1607        let f = |fmt: &str, ts: Timestamp| format(fmt, ts).unwrap();
1608
1609        let ts = "1970-01-01T00:00Z".parse().unwrap();
1610        insta::assert_snapshot!(f("%s", ts), @"0");
1611        insta::assert_snapshot!(f("%3s", ts), @"  0");
1612        insta::assert_snapshot!(f("%03s", ts), @"000");
1613        insta::assert_snapshot!(f("%2s", ts), @" 0");
1614
1615        let ts = "2025-01-20T13:09-05[US/Eastern]".parse().unwrap();
1616        insta::assert_snapshot!(f("%s", ts), @"1737396540");
1617    }
1618
1619    #[test]
1620    fn ok_format_quarter() {
1621        let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1622
1623        insta::assert_snapshot!(f("%q", date(2024, 3, 31)), @"1");
1624        insta::assert_snapshot!(f("%q", date(2024, 4, 1)), @"2");
1625        insta::assert_snapshot!(f("%q", date(2024, 7, 14)), @"3");
1626        insta::assert_snapshot!(f("%q", date(2024, 12, 31)), @"4");
1627
1628        insta::assert_snapshot!(f("%2q", date(2024, 3, 31)), @"01");
1629        insta::assert_snapshot!(f("%02q", date(2024, 3, 31)), @"01");
1630        insta::assert_snapshot!(f("%_2q", date(2024, 3, 31)), @" 1");
1631    }
1632
1633    #[test]
1634    fn err_format_subsec_nanosecond() {
1635        let f = |fmt: &str, time: Time| format(fmt, time).unwrap_err();
1636        let mk = |subsec| time(0, 0, 0, subsec);
1637
1638        insta::assert_snapshot!(
1639            f("%00f", mk(123_456_789)),
1640            @"strftime formatting failed: %f failed: zero precision with %f is not allowed",
1641        );
1642    }
1643
1644    #[test]
1645    fn err_format_timestamp() {
1646        let f = |fmt: &str, dt: DateTime| format(fmt, dt).unwrap_err();
1647
1648        let dt = date(2025, 1, 20).at(13, 9, 0, 0);
1649        insta::assert_snapshot!(
1650            f("%s", dt),
1651            @"strftime formatting failed: %s failed: requires instant (a timestamp or a date, time and offset)",
1652        );
1653    }
1654
1655    #[test]
1656    fn err_invalid_utf8() {
1657        let d = date(2025, 1, 20);
1658        insta::assert_snapshot!(
1659            format("abc %F xyz", d).unwrap(),
1660            @"abc 2025-01-20 xyz",
1661        );
1662        insta::assert_snapshot!(
1663            format(b"abc %F \xFFxyz", d).unwrap_err(),
1664            @"strftime formatting failed: invalid format string, it must be valid UTF-8",
1665        );
1666    }
1667
1668    #[test]
1669    fn lenient() {
1670        fn f(
1671            fmt: impl AsRef<[u8]>,
1672            tm: impl Into<BrokenDownTime>,
1673        ) -> alloc::string::String {
1674            let config = Config::new().lenient(true);
1675            tm.into().to_string_with_config(&config, fmt).unwrap()
1676        }
1677
1678        insta::assert_snapshot!(f("%z", date(2024, 7, 9)), @"%z");
1679        insta::assert_snapshot!(f("%:z", date(2024, 7, 9)), @"%:z");
1680        insta::assert_snapshot!(f("%Q", date(2024, 7, 9)), @"%Q");
1681        insta::assert_snapshot!(f("%+", date(2024, 7, 9)), @"%+");
1682        insta::assert_snapshot!(f("%F", date(2024, 7, 9)), @"2024-07-09");
1683        insta::assert_snapshot!(f("%T", date(2024, 7, 9)), @"%T");
1684        insta::assert_snapshot!(f("%F%", date(2024, 7, 9)), @"2024-07-09%");
1685        insta::assert_snapshot!(
1686            f(b"abc %F \xFFxyz", date(2024, 7, 9)),
1687            @"abc 2024-07-09 �xyz",
1688        );
1689        // Demonstrates substitution of maximal subparts.
1690        // Namely, `\xF0\x9F\x92` is a prefix of a valid
1691        // UTF-8 encoding of a codepoint, such as `💩`.
1692        // So the entire prefix should get substituted with
1693        // a single replacement character...
1694        insta::assert_snapshot!(
1695            f(b"%F\xF0\x9F\x92%Y", date(2024, 7, 9)),
1696            @"2024-07-09�2024",
1697        );
1698        // ... but \xFF is never part of a valid encoding.
1699        // So each instance gets its own replacement
1700        // character.
1701        insta::assert_snapshot!(
1702            f(b"%F\xFF\xFF\xFF%Y", date(2024, 7, 9)),
1703            @"2024-07-09���2024",
1704        );
1705    }
1706
1707    // Regression test for a failed optimization where we would
1708    // inappropriately call `write_int_pad2` without checking that
1709    // the number to format was two digits.
1710    //
1711    // See: https://github.com/BurntSushi/jiff/issues/497
1712    #[test]
1713    fn regression_timestamp_2s() {
1714        use alloc::string::ToString;
1715
1716        let ts: Timestamp = "2024-07-09T20:24:00Z".parse().unwrap();
1717        assert_eq!(ts.strftime("%2s").to_string(), "1720556640");
1718
1719        let ts: Timestamp = "2024-07-09T20:24:00Z".parse().unwrap();
1720        assert_eq!(ts.strftime("%_2s").to_string(), "1720556640");
1721
1722        let ts: Timestamp = "2024-07-09T20:24:00Z".parse().unwrap();
1723        assert_eq!(ts.strftime("%02s").to_string(), "1720556640");
1724
1725        let ts: Timestamp = "2024-07-09T20:24:00Z".parse().unwrap();
1726        assert_eq!(ts.strftime("%04s").to_string(), "1720556640");
1727    }
1728}