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, Extension, Flag,
7        },
8        util::{DecimalFormatter, FractionalFormatter},
9        Write, WriteExt,
10    },
11    tz::Offset,
12    util::{escape, t::C, utf8},
13    Error,
14};
15
16pub(super) struct Formatter<'f, 't, 'w, W> {
17    pub(super) fmt: &'f [u8],
18    pub(super) tm: &'t BrokenDownTime,
19    pub(super) wtr: &'w mut W,
20}
21
22impl<'f, 't, 'w, W: Write> Formatter<'f, 't, 'w, W> {
23    pub(super) fn format(&mut self) -> Result<(), Error> {
24        while !self.fmt.is_empty() {
25            if self.f() != b'%' {
26                if self.f().is_ascii() {
27                    self.wtr.write_char(char::from(self.f()))?;
28                    self.bump_fmt();
29                } else {
30                    let ch = self.utf8_decode_and_bump()?;
31                    self.wtr.write_char(ch)?;
32                }
33                continue;
34            }
35            if !self.bump_fmt() {
36                return Err(err!(
37                    "invalid format string, expected byte after '%', \
38                     but found end of format string",
39                ));
40            }
41            // Parse extensions like padding/case options and padding width.
42            let ext = self.parse_extension()?;
43            match self.f() {
44                b'%' => self.wtr.write_str("%").context("%% failed")?,
45                b'A' => self.fmt_weekday_full(ext).context("%A failed")?,
46                b'a' => self.fmt_weekday_abbrev(ext).context("%a failed")?,
47                b'B' => self.fmt_month_full(ext).context("%B failed")?,
48                b'b' => self.fmt_month_abbrev(ext).context("%b failed")?,
49                b'C' => self.fmt_century(ext).context("%C failed")?,
50                b'D' => self.fmt_american_date(ext).context("%D failed")?,
51                b'd' => self.fmt_day_zero(ext).context("%d failed")?,
52                b'e' => self.fmt_day_space(ext).context("%e failed")?,
53                b'F' => self.fmt_iso_date(ext).context("%F failed")?,
54                b'f' => self.fmt_fractional(ext).context("%f failed")?,
55                b'G' => self.fmt_iso_week_year(ext).context("%G failed")?,
56                b'g' => self.fmt_iso_week_year2(ext).context("%g failed")?,
57                b'H' => self.fmt_hour24_zero(ext).context("%H failed")?,
58                b'h' => self.fmt_month_abbrev(ext).context("%b failed")?,
59                b'I' => self.fmt_hour12_zero(ext).context("%H failed")?,
60                b'j' => self.fmt_day_of_year(ext).context("%j failed")?,
61                b'k' => self.fmt_hour24_space(ext).context("%k failed")?,
62                b'l' => self.fmt_hour12_space(ext).context("%l failed")?,
63                b'M' => self.fmt_minute(ext).context("%M failed")?,
64                b'm' => self.fmt_month(ext).context("%m failed")?,
65                b'n' => self.fmt_literal("\n").context("%n failed")?,
66                b'P' => self.fmt_ampm_lower(ext).context("%P failed")?,
67                b'p' => self.fmt_ampm_upper(ext).context("%p failed")?,
68                b'Q' => self.fmt_iana_nocolon().context("%Q failed")?,
69                b'R' => self.fmt_clock_nosecs(ext).context("%R failed")?,
70                b'S' => self.fmt_second(ext).context("%S failed")?,
71                b's' => self.fmt_timestamp(ext).context("%s failed")?,
72                b'T' => self.fmt_clock_secs(ext).context("%T failed")?,
73                b't' => self.fmt_literal("\t").context("%t failed")?,
74                b'U' => self.fmt_week_sun(ext).context("%U failed")?,
75                b'u' => self.fmt_weekday_mon(ext).context("%u failed")?,
76                b'V' => self.fmt_week_iso(ext).context("%V failed")?,
77                b'W' => self.fmt_week_mon(ext).context("%W failed")?,
78                b'w' => self.fmt_weekday_sun(ext).context("%w failed")?,
79                b'Y' => self.fmt_year(ext).context("%Y failed")?,
80                b'y' => self.fmt_year2(ext).context("%y failed")?,
81                b'Z' => self.fmt_tzabbrev(ext).context("%Z failed")?,
82                b'z' => self.fmt_offset_nocolon().context("%z failed")?,
83                b':' => {
84                    if !self.bump_fmt() {
85                        return Err(err!(
86                            "invalid format string, expected directive \
87                             after '%:'",
88                        ));
89                    }
90                    match self.f() {
91                        b'Q' => self.fmt_iana_colon().context("%:Q failed")?,
92                        b'z' => {
93                            self.fmt_offset_colon().context("%:z failed")?
94                        }
95                        unk => {
96                            return Err(err!(
97                                "found unrecognized directive %{unk} \
98                                 following %:",
99                                unk = escape::Byte(unk),
100                            ));
101                        }
102                    }
103                }
104                b'.' => {
105                    if !self.bump_fmt() {
106                        return Err(err!(
107                            "invalid format string, expected directive \
108                             after '%.'",
109                        ));
110                    }
111                    // Parse precision settings after the `.`, effectively
112                    // overriding any digits that came before it.
113                    let ext = Extension { width: self.parse_width()?, ..ext };
114                    match self.f() {
115                        b'f' => self
116                            .fmt_dot_fractional(ext)
117                            .context("%.f failed")?,
118                        unk => {
119                            return Err(err!(
120                                "found unrecognized directive %{unk} \
121                                 following %.",
122                                unk = escape::Byte(unk),
123                            ));
124                        }
125                    }
126                }
127                unk => {
128                    return Err(err!(
129                        "found unrecognized specifier directive %{unk}",
130                        unk = escape::Byte(unk),
131                    ));
132                }
133            }
134            self.bump_fmt();
135        }
136        Ok(())
137    }
138
139    /// Returns the byte at the current position of the format string.
140    ///
141    /// # Panics
142    ///
143    /// This panics when the entire format string has been consumed.
144    fn f(&self) -> u8 {
145        self.fmt[0]
146    }
147
148    /// Bumps the position of the format string.
149    ///
150    /// This returns true in precisely the cases where `self.f()` will not
151    /// panic. i.e., When the end of the format string hasn't been reached yet.
152    fn bump_fmt(&mut self) -> bool {
153        self.fmt = &self.fmt[1..];
154        !self.fmt.is_empty()
155    }
156
157    /// Decodes a Unicode scalar value from the beginning of `fmt` and advances
158    /// the parser accordingly.
159    ///
160    /// If a Unicode scalar value could not be decoded, then an error is
161    /// returned.
162    ///
163    /// It would be nice to just pass through bytes as-is instead of doing
164    /// actual UTF-8 decoding, but since the `Write` trait only represents
165    /// Unicode-accepting buffers, we need to actually do decoding here.
166    ///
167    /// # Panics
168    ///
169    /// When `self.fmt` is empty. i.e., Only call this when you know there is
170    /// some remaining bytes to parse.
171    #[inline(never)]
172    fn utf8_decode_and_bump(&mut self) -> Result<char, Error> {
173        match utf8::decode(self.fmt).expect("non-empty fmt") {
174            Ok(ch) => {
175                self.fmt = &self.fmt[ch.len_utf8()..];
176                return Ok(ch);
177            }
178            Err(invalid) => Err(err!(
179                "found invalid UTF-8 byte {byte:?} in format \
180                 string (format strings must be valid UTF-8)",
181                byte = escape::Byte(invalid),
182            )),
183        }
184    }
185
186    /// Parses optional extensions before a specifier directive. That is, right
187    /// after the `%`. If any extensions are parsed, the parser is bumped
188    /// to the next byte. (If no next byte exists, then an error is returned.)
189    fn parse_extension(&mut self) -> Result<Extension, Error> {
190        let flag = self.parse_flag()?;
191        let width = self.parse_width()?;
192        Ok(Extension { flag, width })
193    }
194
195    /// Parses an optional flag. And if one is parsed, the parser is bumped
196    /// to the next byte. (If no next byte exists, then an error is returned.)
197    fn parse_flag(&mut self) -> Result<Option<Flag>, Error> {
198        let (flag, fmt) = Extension::parse_flag(self.fmt)?;
199        self.fmt = fmt;
200        Ok(flag)
201    }
202
203    /// Parses an optional width that comes after a (possibly absent) flag and
204    /// before the specifier directive itself. And if a width is parsed, the
205    /// parser is bumped to the next byte. (If no next byte exists, then an
206    /// error is returned.)
207    ///
208    /// Note that this is also used to parse precision settings for `%f` and
209    /// `%.f`. In the former case, the width is just re-interpreted as a
210    /// precision setting. In the latter case, something like `%5.9f` is
211    /// technically valid, but the `5` is ignored.
212    fn parse_width(&mut self) -> Result<Option<u8>, Error> {
213        let (width, fmt) = Extension::parse_width(self.fmt)?;
214        self.fmt = fmt;
215        Ok(width)
216    }
217
218    // These are the formatting functions. They are pretty much responsible
219    // for getting what they need for the broken down time and reporting a
220    // decent failure mode if what they need couldn't be found. And then,
221    // of course, doing the actual formatting.
222
223    /// %P
224    fn fmt_ampm_lower(&mut self, ext: Extension) -> Result<(), Error> {
225        let hour = self
226            .tm
227            .hour
228            .ok_or_else(|| err!("requires time to format AM/PM"))?
229            .get();
230        ext.write_str(
231            Case::AsIs,
232            if hour < 12 { "am" } else { "pm" },
233            self.wtr,
234        )
235    }
236
237    /// %p
238    fn fmt_ampm_upper(&mut self, ext: Extension) -> Result<(), Error> {
239        let hour = self
240            .tm
241            .hour
242            .ok_or_else(|| err!("requires time to format AM/PM"))?
243            .get();
244        ext.write_str(
245            Case::Upper,
246            if hour < 12 { "AM" } else { "PM" },
247            self.wtr,
248        )
249    }
250
251    /// %D
252    fn fmt_american_date(&mut self, ext: Extension) -> Result<(), Error> {
253        self.fmt_month(ext)?;
254        self.wtr.write_char('/')?;
255        self.fmt_day_zero(ext)?;
256        self.wtr.write_char('/')?;
257        self.fmt_year2(ext)?;
258        Ok(())
259    }
260
261    /// %R
262    fn fmt_clock_nosecs(&mut self, ext: Extension) -> Result<(), Error> {
263        self.fmt_hour24_zero(ext)?;
264        self.wtr.write_char(':')?;
265        self.fmt_minute(ext)?;
266        Ok(())
267    }
268
269    /// %T
270    fn fmt_clock_secs(&mut self, ext: Extension) -> Result<(), Error> {
271        self.fmt_hour24_zero(ext)?;
272        self.wtr.write_char(':')?;
273        self.fmt_minute(ext)?;
274        self.wtr.write_char(':')?;
275        self.fmt_second(ext)?;
276        Ok(())
277    }
278
279    /// %d
280    fn fmt_day_zero(&mut self, ext: Extension) -> Result<(), Error> {
281        let day = self
282            .tm
283            .day
284            .or_else(|| self.tm.to_date().ok().map(|d| d.day_ranged()))
285            .ok_or_else(|| err!("requires date to format day"))?
286            .get();
287        ext.write_int(b'0', Some(2), day, self.wtr)
288    }
289
290    /// %e
291    fn fmt_day_space(&mut self, ext: Extension) -> Result<(), Error> {
292        let day = self
293            .tm
294            .day
295            .or_else(|| self.tm.to_date().ok().map(|d| d.day_ranged()))
296            .ok_or_else(|| err!("requires date to format day"))?
297            .get();
298        ext.write_int(b' ', Some(2), day, self.wtr)
299    }
300
301    /// %I
302    fn fmt_hour12_zero(&mut self, ext: Extension) -> Result<(), Error> {
303        let mut hour = self
304            .tm
305            .hour
306            .ok_or_else(|| err!("requires time to format hour"))?
307            .get();
308        if hour == 0 {
309            hour = 12;
310        } else if hour > 12 {
311            hour -= 12;
312        }
313        ext.write_int(b'0', Some(2), hour, self.wtr)
314    }
315
316    /// %H
317    fn fmt_hour24_zero(&mut self, ext: Extension) -> Result<(), Error> {
318        let hour = self
319            .tm
320            .hour
321            .ok_or_else(|| err!("requires time to format hour"))?
322            .get();
323        ext.write_int(b'0', Some(2), hour, self.wtr)
324    }
325
326    /// %l
327    fn fmt_hour12_space(&mut self, ext: Extension) -> Result<(), Error> {
328        let mut hour = self
329            .tm
330            .hour
331            .ok_or_else(|| err!("requires time to format hour"))?
332            .get();
333        if hour == 0 {
334            hour = 12;
335        } else if hour > 12 {
336            hour -= 12;
337        }
338        ext.write_int(b' ', Some(2), hour, self.wtr)
339    }
340
341    /// %k
342    fn fmt_hour24_space(&mut self, ext: Extension) -> Result<(), Error> {
343        let hour = self
344            .tm
345            .hour
346            .ok_or_else(|| err!("requires time to format hour"))?
347            .get();
348        ext.write_int(b' ', Some(2), hour, self.wtr)
349    }
350
351    /// %F
352    fn fmt_iso_date(&mut self, ext: Extension) -> Result<(), Error> {
353        self.fmt_year(ext)?;
354        self.wtr.write_char('-')?;
355        self.fmt_month(ext)?;
356        self.wtr.write_char('-')?;
357        self.fmt_day_zero(ext)?;
358        Ok(())
359    }
360
361    /// %M
362    fn fmt_minute(&mut self, ext: Extension) -> Result<(), Error> {
363        let minute = self
364            .tm
365            .minute
366            .ok_or_else(|| err!("requires time to format minute"))?
367            .get();
368        ext.write_int(b'0', Some(2), minute, self.wtr)
369    }
370
371    /// %m
372    fn fmt_month(&mut self, ext: Extension) -> Result<(), Error> {
373        let month = self
374            .tm
375            .month
376            .or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged()))
377            .ok_or_else(|| err!("requires date to format month"))?
378            .get();
379        ext.write_int(b'0', Some(2), month, self.wtr)
380    }
381
382    /// %B
383    fn fmt_month_full(&mut self, ext: Extension) -> Result<(), Error> {
384        let month = self
385            .tm
386            .month
387            .or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged()))
388            .ok_or_else(|| err!("requires date to format month"))?;
389        ext.write_str(Case::AsIs, month_name_full(month), self.wtr)
390    }
391
392    /// %b, %h
393    fn fmt_month_abbrev(&mut self, ext: Extension) -> Result<(), Error> {
394        let month = self
395            .tm
396            .month
397            .or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged()))
398            .ok_or_else(|| err!("requires date to format month"))?;
399        ext.write_str(Case::AsIs, month_name_abbrev(month), self.wtr)
400    }
401
402    /// %Q
403    fn fmt_iana_nocolon(&mut self) -> Result<(), Error> {
404        let Some(iana) = self.tm.iana_time_zone() else {
405            let offset = self.tm.offset.ok_or_else(|| {
406                err!(
407                    "requires IANA time zone identifier or time \
408                     zone offset, but none were present"
409                )
410            })?;
411            return write_offset(offset, false, &mut self.wtr);
412        };
413        self.wtr.write_str(iana)?;
414        Ok(())
415    }
416
417    /// %:Q
418    fn fmt_iana_colon(&mut self) -> Result<(), Error> {
419        let Some(iana) = self.tm.iana_time_zone() else {
420            let offset = self.tm.offset.ok_or_else(|| {
421                err!(
422                    "requires IANA time zone identifier or time \
423                     zone offset, but none were present"
424                )
425            })?;
426            return write_offset(offset, true, &mut self.wtr);
427        };
428        self.wtr.write_str(iana)?;
429        Ok(())
430    }
431
432    /// %z
433    fn fmt_offset_nocolon(&mut self) -> Result<(), Error> {
434        let offset = self.tm.offset.ok_or_else(|| {
435            err!("requires offset to format time zone offset")
436        })?;
437        write_offset(offset, false, self.wtr)
438    }
439
440    /// %:z
441    fn fmt_offset_colon(&mut self) -> Result<(), Error> {
442        let offset = self.tm.offset.ok_or_else(|| {
443            err!("requires offset to format time zone offset")
444        })?;
445        write_offset(offset, true, self.wtr)
446    }
447
448    /// %S
449    fn fmt_second(&mut self, ext: Extension) -> Result<(), Error> {
450        let second = self
451            .tm
452            .second
453            .ok_or_else(|| err!("requires time to format second"))?
454            .get();
455        ext.write_int(b'0', Some(2), second, self.wtr)
456    }
457
458    /// %s
459    fn fmt_timestamp(&mut self, ext: Extension) -> Result<(), Error> {
460        let timestamp = self.tm.to_timestamp().map_err(|_| {
461            err!(
462                "requires instant (a date, time and offset) \
463                 to format Unix timestamp",
464            )
465        })?;
466        ext.write_int(b' ', None, timestamp.as_second(), self.wtr)
467    }
468
469    /// %f
470    fn fmt_fractional(&mut self, ext: Extension) -> Result<(), Error> {
471        let subsec = self.tm.subsec.ok_or_else(|| {
472            err!("requires time to format subsecond nanoseconds")
473        })?;
474        // For %f, we always want to emit at least one digit. The only way we
475        // wouldn't is if our fractional component is zero. One exception to
476        // this is when the width is `0` (which looks like `%00f`), in which
477        // case, we emit an error. We could allow it to emit an empty string,
478        // but this seems very odd. And an empty string cannot be parsed by
479        // `%f`.
480        if ext.width == Some(0) {
481            return Err(err!("zero precision with %f is not allowed"));
482        }
483        if subsec == C(0) && ext.width.is_none() {
484            self.wtr.write_str("0")?;
485            return Ok(());
486        }
487        ext.write_fractional_seconds(subsec, self.wtr)?;
488        Ok(())
489    }
490
491    /// %.f
492    fn fmt_dot_fractional(&mut self, ext: Extension) -> Result<(), Error> {
493        let Some(subsec) = self.tm.subsec else { return Ok(()) };
494        if subsec == C(0) && ext.width.is_none() || ext.width == Some(0) {
495            return Ok(());
496        }
497        ext.write_str(Case::AsIs, ".", self.wtr)?;
498        ext.write_fractional_seconds(subsec, self.wtr)?;
499        Ok(())
500    }
501
502    /// %Z
503    fn fmt_tzabbrev(&mut self, ext: Extension) -> Result<(), Error> {
504        let tzabbrev = self.tm.tzabbrev.as_ref().ok_or_else(|| {
505            err!("requires time zone abbreviation in broken down time")
506        })?;
507        ext.write_str(Case::Upper, tzabbrev.as_str(), self.wtr)
508    }
509
510    /// %A
511    fn fmt_weekday_full(&mut self, ext: Extension) -> Result<(), Error> {
512        let weekday = self
513            .tm
514            .weekday
515            .or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
516            .ok_or_else(|| err!("requires date to format weekday"))?;
517        ext.write_str(Case::AsIs, weekday_name_full(weekday), self.wtr)
518    }
519
520    /// %a
521    fn fmt_weekday_abbrev(&mut self, ext: Extension) -> Result<(), Error> {
522        let weekday = self
523            .tm
524            .weekday
525            .or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
526            .ok_or_else(|| err!("requires date to format weekday"))?;
527        ext.write_str(Case::AsIs, weekday_name_abbrev(weekday), self.wtr)
528    }
529
530    /// %u
531    fn fmt_weekday_mon(&mut self, ext: Extension) -> Result<(), Error> {
532        let weekday = self
533            .tm
534            .weekday
535            .or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
536            .ok_or_else(|| err!("requires date to format weekday number"))?;
537        ext.write_int(b' ', None, weekday.to_monday_one_offset(), self.wtr)
538    }
539
540    /// %w
541    fn fmt_weekday_sun(&mut self, ext: Extension) -> Result<(), Error> {
542        let weekday = self
543            .tm
544            .weekday
545            .or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
546            .ok_or_else(|| err!("requires date to format weekday number"))?;
547        ext.write_int(b' ', None, weekday.to_sunday_zero_offset(), self.wtr)
548    }
549
550    /// %U
551    fn fmt_week_sun(&mut self, ext: Extension) -> Result<(), Error> {
552        // Short circuit if the week number was explicitly set.
553        if let Some(weeknum) = self.tm.week_sun {
554            return ext.write_int(b'0', Some(2), weeknum, self.wtr);
555        }
556        let day = self
557            .tm
558            .day_of_year
559            .map(|day| day.get())
560            .or_else(|| self.tm.to_date().ok().map(|d| d.day_of_year()))
561            .ok_or_else(|| {
562                err!("requires date to format Sunday-based week number")
563            })?;
564        let weekday = self
565            .tm
566            .weekday
567            .or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
568            .ok_or_else(|| {
569                err!("requires date to format Sunday-based week number")
570            })?
571            .to_sunday_zero_offset();
572        // Example: 2025-01-05 is the first Sunday in 2025, and thus the start
573        // of week 1. This means that 2025-01-04 (Saturday) is in week 0.
574        //
575        // So for 2025-01-05, day=5 and weekday=0. Thus we get 11/7 = 1.
576        // For 2025-01-04, day=4 and weekday=6. Thus we get 4/7 = 0.
577        let weeknum = (day + 6 - i16::from(weekday)) / 7;
578        ext.write_int(b'0', Some(2), weeknum, self.wtr)
579    }
580
581    /// %V
582    fn fmt_week_iso(&mut self, ext: Extension) -> Result<(), Error> {
583        let weeknum = self
584            .tm
585            .iso_week
586            .or_else(|| {
587                self.tm.to_date().ok().map(|d| d.iso_week_date().week_ranged())
588            })
589            .ok_or_else(|| {
590                err!("requires date to format ISO 8601 week number")
591            })?;
592        ext.write_int(b'0', Some(2), weeknum, self.wtr)
593    }
594
595    /// %W
596    fn fmt_week_mon(&mut self, ext: Extension) -> Result<(), Error> {
597        // Short circuit if the week number was explicitly set.
598        if let Some(weeknum) = self.tm.week_mon {
599            return ext.write_int(b'0', Some(2), weeknum, self.wtr);
600        }
601        let day = self
602            .tm
603            .day_of_year
604            .map(|day| day.get())
605            .or_else(|| self.tm.to_date().ok().map(|d| d.day_of_year()))
606            .ok_or_else(|| {
607                err!("requires date to format Monday-based week number")
608            })?;
609        let weekday = self
610            .tm
611            .weekday
612            .or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
613            .ok_or_else(|| {
614                err!("requires date to format Monday-based week number")
615            })?
616            .to_sunday_zero_offset();
617        // Example: 2025-01-06 is the first Monday in 2025, and thus the start
618        // of week 1. This means that 2025-01-05 (Sunday) is in week 0.
619        //
620        // So for 2025-01-06, day=6 and weekday=1. Thus we get 12/7 = 1.
621        // For 2025-01-05, day=5 and weekday=7. Thus we get 5/7 = 0.
622        let weeknum = (day + 6 - ((i16::from(weekday) + 6) % 7)) / 7;
623        ext.write_int(b'0', Some(2), weeknum, self.wtr)
624    }
625
626    /// %Y
627    fn fmt_year(&mut self, ext: Extension) -> Result<(), Error> {
628        let year = self
629            .tm
630            .year
631            .or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged()))
632            .ok_or_else(|| err!("requires date to format year"))?
633            .get();
634        ext.write_int(b'0', Some(4), year, self.wtr)
635    }
636
637    /// %y
638    fn fmt_year2(&mut self, ext: Extension) -> Result<(), Error> {
639        let year = self
640            .tm
641            .year
642            .or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged()))
643            .ok_or_else(|| err!("requires date to format year (2-digit)"))?
644            .get();
645        if !(1969 <= year && year <= 2068) {
646            return Err(err!(
647                "formatting a 2-digit year requires that it be in \
648                 the inclusive range 1969 to 2068, but got {year}",
649            ));
650        }
651        let year = year % 100;
652        ext.write_int(b'0', Some(2), year, self.wtr)
653    }
654
655    /// %C
656    fn fmt_century(&mut self, ext: Extension) -> Result<(), Error> {
657        let year = self
658            .tm
659            .year
660            .or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged()))
661            .ok_or_else(|| err!("requires date to format century (2-digit)"))?
662            .get();
663        let century = year / 100;
664        ext.write_int(b' ', None, century, self.wtr)
665    }
666
667    /// %G
668    fn fmt_iso_week_year(&mut self, ext: Extension) -> Result<(), Error> {
669        let year = self
670            .tm
671            .iso_week_year
672            .or_else(|| {
673                self.tm.to_date().ok().map(|d| d.iso_week_date().year_ranged())
674            })
675            .ok_or_else(|| {
676                err!("requires date to format ISO 8601 week-based year")
677            })?
678            .get();
679        ext.write_int(b'0', Some(4), year, self.wtr)
680    }
681
682    /// %g
683    fn fmt_iso_week_year2(&mut self, ext: Extension) -> Result<(), Error> {
684        let year = self
685            .tm
686            .iso_week_year
687            .or_else(|| {
688                self.tm.to_date().ok().map(|d| d.iso_week_date().year_ranged())
689            })
690            .ok_or_else(|| {
691                err!(
692                    "requires date to format \
693                     ISO 8601 week-based year (2-digit)"
694                )
695            })?
696            .get();
697        if !(1969 <= year && year <= 2068) {
698            return Err(err!(
699                "formatting a 2-digit ISO 8601 week-based year \
700                 requires that it be in \
701                 the inclusive range 1969 to 2068, but got {year}",
702            ));
703        }
704        let year = year % 100;
705        ext.write_int(b'0', Some(2), year, self.wtr)
706    }
707
708    /// %j
709    fn fmt_day_of_year(&mut self, ext: Extension) -> Result<(), Error> {
710        let day = self
711            .tm
712            .day_of_year
713            .map(|day| day.get())
714            .or_else(|| self.tm.to_date().ok().map(|d| d.day_of_year()))
715            .ok_or_else(|| err!("requires date to format day of year"))?;
716        ext.write_int(b'0', Some(3), day, self.wtr)
717    }
718
719    /// %n, %t
720    fn fmt_literal(&mut self, literal: &str) -> Result<(), Error> {
721        self.wtr.write_str(literal)
722    }
723}
724
725/// Writes the given time zone offset to the writer.
726///
727/// When `colon` is true, the hour, minute and optional second components are
728/// delimited by a colon. Otherwise, no delimiter is used.
729fn write_offset<W: Write>(
730    offset: Offset,
731    colon: bool,
732    wtr: &mut W,
733) -> Result<(), Error> {
734    static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2);
735
736    let hours = offset.part_hours_ranged().abs().get();
737    let minutes = offset.part_minutes_ranged().abs().get();
738    let seconds = offset.part_seconds_ranged().abs().get();
739
740    wtr.write_str(if offset.is_negative() { "-" } else { "+" })?;
741    wtr.write_int(&FMT_TWO, hours)?;
742    if colon {
743        wtr.write_str(":")?;
744    }
745    wtr.write_int(&FMT_TWO, minutes)?;
746    if seconds != 0 {
747        if colon {
748            wtr.write_str(":")?;
749        }
750        wtr.write_int(&FMT_TWO, seconds)?;
751    }
752    Ok(())
753}
754
755impl Extension {
756    /// Writes the given string using the default case rule provided, unless
757    /// an option in this extension config overrides the default case.
758    fn write_str<W: Write>(
759        self,
760        default: Case,
761        string: &str,
762        wtr: &mut W,
763    ) -> Result<(), Error> {
764        let case = match self.flag {
765            Some(Flag::Uppercase) => Case::Upper,
766            Some(Flag::Swapcase) => default.swap(),
767            _ => default,
768        };
769        match case {
770            Case::AsIs => {
771                wtr.write_str(string)?;
772            }
773            Case::Upper => {
774                for ch in string.chars() {
775                    for ch in ch.to_uppercase() {
776                        wtr.write_char(ch)?;
777                    }
778                }
779            }
780            Case::Lower => {
781                for ch in string.chars() {
782                    for ch in ch.to_lowercase() {
783                        wtr.write_char(ch)?;
784                    }
785                }
786            }
787        }
788        Ok(())
789    }
790
791    /// Writes the given integer using the given padding width and byte, unless
792    /// an option in this extension config overrides a default setting.
793    fn write_int<W: Write>(
794        self,
795        pad_byte: u8,
796        pad_width: Option<u8>,
797        number: impl Into<i64>,
798        wtr: &mut W,
799    ) -> Result<(), Error> {
800        let number = number.into();
801        let pad_byte = match self.flag {
802            Some(Flag::PadZero) => b'0',
803            Some(Flag::PadSpace) => b' ',
804            _ => pad_byte,
805        };
806        let pad_width = if matches!(self.flag, Some(Flag::NoPad)) {
807            None
808        } else {
809            self.width.or(pad_width)
810        };
811
812        let mut formatter = DecimalFormatter::new().padding_byte(pad_byte);
813        if let Some(width) = pad_width {
814            formatter = formatter.padding(width);
815        }
816        wtr.write_int(&formatter, number)
817    }
818
819    /// Writes the given number of nanoseconds as a fractional component of
820    /// a second. This does not include the leading `.`.
821    ///
822    /// The `width` setting on `Extension` is treated as a precision setting.
823    fn write_fractional_seconds<W: Write>(
824        self,
825        number: impl Into<i64>,
826        wtr: &mut W,
827    ) -> Result<(), Error> {
828        let number = number.into();
829
830        let formatter = FractionalFormatter::new().precision(self.width);
831        wtr.write_fraction(&formatter, number)
832    }
833}
834
835/// The case to use when printing a string like weekday or TZ abbreviation.
836#[derive(Clone, Copy, Debug)]
837enum Case {
838    AsIs,
839    Upper,
840    Lower,
841}
842
843impl Case {
844    /// Swap upper to lowercase, and lower to uppercase.
845    fn swap(self) -> Case {
846        match self {
847            Case::AsIs => Case::AsIs,
848            Case::Upper => Case::Lower,
849            Case::Lower => Case::Upper,
850        }
851    }
852}
853
854#[cfg(feature = "alloc")]
855#[cfg(test)]
856mod tests {
857    use crate::{
858        civil::{date, time, Date, DateTime, Time},
859        fmt::strtime::format,
860        Timestamp, Zoned,
861    };
862
863    #[test]
864    fn ok_format_american_date() {
865        let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
866
867        insta::assert_snapshot!(f("%D", date(2024, 7, 9)), @"07/09/24");
868        insta::assert_snapshot!(f("%-D", date(2024, 7, 9)), @"7/9/24");
869        insta::assert_snapshot!(f("%3D", date(2024, 7, 9)), @"007/009/024");
870        insta::assert_snapshot!(f("%03D", date(2024, 7, 9)), @"007/009/024");
871    }
872
873    #[test]
874    fn ok_format_ampm() {
875        let f = |fmt: &str, time: Time| format(fmt, time).unwrap();
876
877        insta::assert_snapshot!(f("%H%P", time(9, 0, 0, 0)), @"09am");
878        insta::assert_snapshot!(f("%H%P", time(11, 0, 0, 0)), @"11am");
879        insta::assert_snapshot!(f("%H%P", time(23, 0, 0, 0)), @"23pm");
880        insta::assert_snapshot!(f("%H%P", time(0, 0, 0, 0)), @"00am");
881
882        insta::assert_snapshot!(f("%H%p", time(9, 0, 0, 0)), @"09AM");
883        insta::assert_snapshot!(f("%H%p", time(11, 0, 0, 0)), @"11AM");
884        insta::assert_snapshot!(f("%H%p", time(23, 0, 0, 0)), @"23PM");
885        insta::assert_snapshot!(f("%H%p", time(0, 0, 0, 0)), @"00AM");
886
887        insta::assert_snapshot!(f("%H%#p", time(9, 0, 0, 0)), @"09am");
888    }
889
890    #[test]
891    fn ok_format_clock() {
892        let f = |fmt: &str, time: Time| format(fmt, time).unwrap();
893
894        insta::assert_snapshot!(f("%R", time(23, 59, 8, 0)), @"23:59");
895        insta::assert_snapshot!(f("%T", time(23, 59, 8, 0)), @"23:59:08");
896    }
897
898    #[test]
899    fn ok_format_day() {
900        let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
901
902        insta::assert_snapshot!(f("%d", date(2024, 7, 9)), @"09");
903        insta::assert_snapshot!(f("%0d", date(2024, 7, 9)), @"09");
904        insta::assert_snapshot!(f("%-d", date(2024, 7, 9)), @"9");
905        insta::assert_snapshot!(f("%_d", date(2024, 7, 9)), @" 9");
906
907        insta::assert_snapshot!(f("%e", date(2024, 7, 9)), @" 9");
908        insta::assert_snapshot!(f("%0e", date(2024, 7, 9)), @"09");
909        insta::assert_snapshot!(f("%-e", date(2024, 7, 9)), @"9");
910        insta::assert_snapshot!(f("%_e", date(2024, 7, 9)), @" 9");
911    }
912
913    #[test]
914    fn ok_format_iso_date() {
915        let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
916
917        insta::assert_snapshot!(f("%F", date(2024, 7, 9)), @"2024-07-09");
918        insta::assert_snapshot!(f("%-F", date(2024, 7, 9)), @"2024-7-9");
919        insta::assert_snapshot!(f("%3F", date(2024, 7, 9)), @"2024-007-009");
920        insta::assert_snapshot!(f("%03F", date(2024, 7, 9)), @"2024-007-009");
921    }
922
923    #[test]
924    fn ok_format_hour() {
925        let f = |fmt: &str, time: Time| format(fmt, time).unwrap();
926
927        insta::assert_snapshot!(f("%H", time(9, 0, 0, 0)), @"09");
928        insta::assert_snapshot!(f("%H", time(11, 0, 0, 0)), @"11");
929        insta::assert_snapshot!(f("%H", time(23, 0, 0, 0)), @"23");
930        insta::assert_snapshot!(f("%H", time(0, 0, 0, 0)), @"00");
931
932        insta::assert_snapshot!(f("%I", time(9, 0, 0, 0)), @"09");
933        insta::assert_snapshot!(f("%I", time(11, 0, 0, 0)), @"11");
934        insta::assert_snapshot!(f("%I", time(23, 0, 0, 0)), @"11");
935        insta::assert_snapshot!(f("%I", time(0, 0, 0, 0)), @"12");
936
937        insta::assert_snapshot!(f("%k", time(9, 0, 0, 0)), @" 9");
938        insta::assert_snapshot!(f("%k", time(11, 0, 0, 0)), @"11");
939        insta::assert_snapshot!(f("%k", time(23, 0, 0, 0)), @"23");
940        insta::assert_snapshot!(f("%k", time(0, 0, 0, 0)), @" 0");
941
942        insta::assert_snapshot!(f("%l", time(9, 0, 0, 0)), @" 9");
943        insta::assert_snapshot!(f("%l", time(11, 0, 0, 0)), @"11");
944        insta::assert_snapshot!(f("%l", time(23, 0, 0, 0)), @"11");
945        insta::assert_snapshot!(f("%l", time(0, 0, 0, 0)), @"12");
946    }
947
948    #[test]
949    fn ok_format_minute() {
950        let f = |fmt: &str, time: Time| format(fmt, time).unwrap();
951
952        insta::assert_snapshot!(f("%M", time(0, 9, 0, 0)), @"09");
953        insta::assert_snapshot!(f("%M", time(0, 11, 0, 0)), @"11");
954        insta::assert_snapshot!(f("%M", time(0, 23, 0, 0)), @"23");
955        insta::assert_snapshot!(f("%M", time(0, 0, 0, 0)), @"00");
956    }
957
958    #[test]
959    fn ok_format_month() {
960        let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
961
962        insta::assert_snapshot!(f("%m", date(2024, 7, 14)), @"07");
963        insta::assert_snapshot!(f("%m", date(2024, 12, 14)), @"12");
964        insta::assert_snapshot!(f("%0m", date(2024, 7, 14)), @"07");
965        insta::assert_snapshot!(f("%0m", date(2024, 12, 14)), @"12");
966        insta::assert_snapshot!(f("%-m", date(2024, 7, 14)), @"7");
967        insta::assert_snapshot!(f("%-m", date(2024, 12, 14)), @"12");
968        insta::assert_snapshot!(f("%_m", date(2024, 7, 14)), @" 7");
969        insta::assert_snapshot!(f("%_m", date(2024, 12, 14)), @"12");
970    }
971
972    #[test]
973    fn ok_format_month_name() {
974        let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
975
976        insta::assert_snapshot!(f("%B", date(2024, 7, 14)), @"July");
977        insta::assert_snapshot!(f("%b", date(2024, 7, 14)), @"Jul");
978        insta::assert_snapshot!(f("%h", date(2024, 7, 14)), @"Jul");
979
980        insta::assert_snapshot!(f("%#B", date(2024, 7, 14)), @"July");
981        insta::assert_snapshot!(f("%^B", date(2024, 7, 14)), @"JULY");
982    }
983
984    #[test]
985    fn ok_format_offset() {
986        if crate::tz::db().is_definitively_empty() {
987            return;
988        }
989
990        let f = |fmt: &str, zdt: &Zoned| format(fmt, zdt).unwrap();
991
992        let zdt = date(2024, 7, 14)
993            .at(22, 24, 0, 0)
994            .in_tz("America/New_York")
995            .unwrap();
996        insta::assert_snapshot!(f("%z", &zdt), @"-0400");
997        insta::assert_snapshot!(f("%:z", &zdt), @"-04:00");
998
999        let zdt = zdt.checked_add(crate::Span::new().months(5)).unwrap();
1000        insta::assert_snapshot!(f("%z", &zdt), @"-0500");
1001        insta::assert_snapshot!(f("%:z", &zdt), @"-05:00");
1002    }
1003
1004    #[test]
1005    fn ok_format_second() {
1006        let f = |fmt: &str, time: Time| format(fmt, time).unwrap();
1007
1008        insta::assert_snapshot!(f("%S", time(0, 0, 9, 0)), @"09");
1009        insta::assert_snapshot!(f("%S", time(0, 0, 11, 0)), @"11");
1010        insta::assert_snapshot!(f("%S", time(0, 0, 23, 0)), @"23");
1011        insta::assert_snapshot!(f("%S", time(0, 0, 0, 0)), @"00");
1012    }
1013
1014    #[test]
1015    fn ok_format_subsec_nanosecond() {
1016        let f = |fmt: &str, time: Time| format(fmt, time).unwrap();
1017        let mk = |subsec| time(0, 0, 0, subsec);
1018
1019        insta::assert_snapshot!(f("%f", mk(123_000_000)), @"123");
1020        insta::assert_snapshot!(f("%f", mk(0)), @"0");
1021        insta::assert_snapshot!(f("%3f", mk(0)), @"000");
1022        insta::assert_snapshot!(f("%3f", mk(123_000_000)), @"123");
1023        insta::assert_snapshot!(f("%6f", mk(123_000_000)), @"123000");
1024        insta::assert_snapshot!(f("%9f", mk(123_000_000)), @"123000000");
1025        insta::assert_snapshot!(f("%255f", mk(123_000_000)), @"123000000");
1026
1027        insta::assert_snapshot!(f("%.f", mk(123_000_000)), @".123");
1028        insta::assert_snapshot!(f("%.f", mk(0)), @"");
1029        insta::assert_snapshot!(f("%3.f", mk(0)), @"");
1030        insta::assert_snapshot!(f("%.3f", mk(0)), @".000");
1031        insta::assert_snapshot!(f("%.3f", mk(123_000_000)), @".123");
1032        insta::assert_snapshot!(f("%.6f", mk(123_000_000)), @".123000");
1033        insta::assert_snapshot!(f("%.9f", mk(123_000_000)), @".123000000");
1034        insta::assert_snapshot!(f("%.255f", mk(123_000_000)), @".123000000");
1035
1036        insta::assert_snapshot!(f("%3f", mk(123_456_789)), @"123");
1037        insta::assert_snapshot!(f("%6f", mk(123_456_789)), @"123456");
1038        insta::assert_snapshot!(f("%9f", mk(123_456_789)), @"123456789");
1039
1040        insta::assert_snapshot!(f("%.0f", mk(123_456_789)), @"");
1041        insta::assert_snapshot!(f("%.3f", mk(123_456_789)), @".123");
1042        insta::assert_snapshot!(f("%.6f", mk(123_456_789)), @".123456");
1043        insta::assert_snapshot!(f("%.9f", mk(123_456_789)), @".123456789");
1044    }
1045
1046    #[test]
1047    fn ok_format_tzabbrev() {
1048        if crate::tz::db().is_definitively_empty() {
1049            return;
1050        }
1051
1052        let f = |fmt: &str, zdt: &Zoned| format(fmt, zdt).unwrap();
1053
1054        let zdt = date(2024, 7, 14)
1055            .at(22, 24, 0, 0)
1056            .in_tz("America/New_York")
1057            .unwrap();
1058        insta::assert_snapshot!(f("%Z", &zdt), @"EDT");
1059        insta::assert_snapshot!(f("%^Z", &zdt), @"EDT");
1060        insta::assert_snapshot!(f("%#Z", &zdt), @"edt");
1061
1062        let zdt = zdt.checked_add(crate::Span::new().months(5)).unwrap();
1063        insta::assert_snapshot!(f("%Z", &zdt), @"EST");
1064    }
1065
1066    #[test]
1067    fn ok_format_iana() {
1068        if crate::tz::db().is_definitively_empty() {
1069            return;
1070        }
1071
1072        let f = |fmt: &str, zdt: &Zoned| format(fmt, zdt).unwrap();
1073
1074        let zdt = date(2024, 7, 14)
1075            .at(22, 24, 0, 0)
1076            .in_tz("America/New_York")
1077            .unwrap();
1078        insta::assert_snapshot!(f("%Q", &zdt), @"America/New_York");
1079        insta::assert_snapshot!(f("%:Q", &zdt), @"America/New_York");
1080
1081        let zdt = date(2024, 7, 14)
1082            .at(22, 24, 0, 0)
1083            .to_zoned(crate::tz::offset(-4).to_time_zone())
1084            .unwrap();
1085        insta::assert_snapshot!(f("%Q", &zdt), @"-0400");
1086        insta::assert_snapshot!(f("%:Q", &zdt), @"-04:00");
1087
1088        let zdt = date(2024, 7, 14)
1089            .at(22, 24, 0, 0)
1090            .to_zoned(crate::tz::TimeZone::UTC)
1091            .unwrap();
1092        insta::assert_snapshot!(f("%Q", &zdt), @"UTC");
1093        insta::assert_snapshot!(f("%:Q", &zdt), @"UTC");
1094    }
1095
1096    #[test]
1097    fn ok_format_weekday_name() {
1098        let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1099
1100        insta::assert_snapshot!(f("%A", date(2024, 7, 14)), @"Sunday");
1101        insta::assert_snapshot!(f("%a", date(2024, 7, 14)), @"Sun");
1102
1103        insta::assert_snapshot!(f("%#A", date(2024, 7, 14)), @"Sunday");
1104        insta::assert_snapshot!(f("%^A", date(2024, 7, 14)), @"SUNDAY");
1105
1106        insta::assert_snapshot!(f("%u", date(2024, 7, 14)), @"7");
1107        insta::assert_snapshot!(f("%w", date(2024, 7, 14)), @"0");
1108    }
1109
1110    #[test]
1111    fn ok_format_year() {
1112        let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1113
1114        insta::assert_snapshot!(f("%Y", date(2024, 7, 14)), @"2024");
1115        insta::assert_snapshot!(f("%Y", date(24, 7, 14)), @"0024");
1116        insta::assert_snapshot!(f("%Y", date(-24, 7, 14)), @"-0024");
1117
1118        insta::assert_snapshot!(f("%C", date(2024, 7, 14)), @"20");
1119        insta::assert_snapshot!(f("%C", date(1815, 7, 14)), @"18");
1120        insta::assert_snapshot!(f("%C", date(915, 7, 14)), @"9");
1121        insta::assert_snapshot!(f("%C", date(1, 7, 14)), @"0");
1122        insta::assert_snapshot!(f("%C", date(0, 7, 14)), @"0");
1123        insta::assert_snapshot!(f("%C", date(-1, 7, 14)), @"0");
1124        insta::assert_snapshot!(f("%C", date(-2024, 7, 14)), @"-20");
1125        insta::assert_snapshot!(f("%C", date(-1815, 7, 14)), @"-18");
1126        insta::assert_snapshot!(f("%C", date(-915, 7, 14)), @"-9");
1127    }
1128
1129    #[test]
1130    fn ok_format_year_2digit() {
1131        let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1132
1133        insta::assert_snapshot!(f("%y", date(2024, 7, 14)), @"24");
1134        insta::assert_snapshot!(f("%y", date(2001, 7, 14)), @"01");
1135        insta::assert_snapshot!(f("%-y", date(2001, 7, 14)), @"1");
1136        insta::assert_snapshot!(f("%5y", date(2001, 7, 14)), @"00001");
1137        insta::assert_snapshot!(f("%-5y", date(2001, 7, 14)), @"1");
1138        insta::assert_snapshot!(f("%05y", date(2001, 7, 14)), @"00001");
1139        insta::assert_snapshot!(f("%_y", date(2001, 7, 14)), @" 1");
1140        insta::assert_snapshot!(f("%_5y", date(2001, 7, 14)), @"    1");
1141    }
1142
1143    #[test]
1144    fn ok_format_iso_week_year() {
1145        let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1146
1147        insta::assert_snapshot!(f("%G", date(2019, 11, 30)), @"2019");
1148        insta::assert_snapshot!(f("%G", date(19, 11, 30)), @"0019");
1149        insta::assert_snapshot!(f("%G", date(-19, 11, 30)), @"-0019");
1150
1151        // tricksy
1152        insta::assert_snapshot!(f("%G", date(2019, 12, 30)), @"2020");
1153    }
1154
1155    #[test]
1156    fn ok_format_week_num() {
1157        let f = |fmt: &str, date: Date| format(fmt, date).unwrap();
1158
1159        insta::assert_snapshot!(f("%U", date(2025, 1, 4)), @"00");
1160        insta::assert_snapshot!(f("%U", date(2025, 1, 5)), @"01");
1161
1162        insta::assert_snapshot!(f("%W", date(2025, 1, 5)), @"00");
1163        insta::assert_snapshot!(f("%W", date(2025, 1, 6)), @"01");
1164    }
1165
1166    #[test]
1167    fn ok_format_timestamp() {
1168        let f = |fmt: &str, ts: Timestamp| format(fmt, ts).unwrap();
1169
1170        let ts = "1970-01-01T00:00Z".parse().unwrap();
1171        insta::assert_snapshot!(f("%s", ts), @"0");
1172        insta::assert_snapshot!(f("%3s", ts), @"  0");
1173        insta::assert_snapshot!(f("%03s", ts), @"000");
1174
1175        let ts = "2025-01-20T13:09-05[US/Eastern]".parse().unwrap();
1176        insta::assert_snapshot!(f("%s", ts), @"1737396540");
1177    }
1178
1179    #[test]
1180    fn err_format_subsec_nanosecond() {
1181        let f = |fmt: &str, time: Time| format(fmt, time).unwrap_err();
1182        let mk = |subsec| time(0, 0, 0, subsec);
1183
1184        insta::assert_snapshot!(
1185            f("%00f", mk(123_456_789)),
1186            @"strftime formatting failed: %f failed: zero precision with %f is not allowed",
1187        );
1188    }
1189
1190    #[test]
1191    fn err_format_timestamp() {
1192        let f = |fmt: &str, dt: DateTime| format(fmt, dt).unwrap_err();
1193
1194        let dt = date(2025, 1, 20).at(13, 9, 0, 0);
1195        insta::assert_snapshot!(
1196            f("%s", dt),
1197            @"strftime formatting failed: %s failed: requires instant (a date, time and offset) to format Unix timestamp",
1198        );
1199    }
1200}