Skip to main content

time/formatting/
mod.rs

1//! Formatting for various types.
2
3mod component_provider;
4pub(crate) mod formattable;
5mod iso8601;
6
7use core::num::NonZero;
8use std::io;
9
10use num_conv::prelude::*;
11
12use self::component_provider::ComponentProvider;
13pub use self::formattable::Formattable;
14use crate::ext::DigitCount;
15use crate::format_description::{Component, Period, modifier};
16use crate::internal_macros::try_likely_ok;
17use crate::{Month, Weekday, error};
18
19const MONTH_NAMES: [&[u8]; 12] = [
20    b"January",
21    b"February",
22    b"March",
23    b"April",
24    b"May",
25    b"June",
26    b"July",
27    b"August",
28    b"September",
29    b"October",
30    b"November",
31    b"December",
32];
33
34const WEEKDAY_NAMES: [&[u8]; 7] = [
35    b"Monday",
36    b"Tuesday",
37    b"Wednesday",
38    b"Thursday",
39    b"Friday",
40    b"Saturday",
41    b"Sunday",
42];
43
44/// Write all bytes to the output, returning the number of bytes written.
45#[inline]
46pub(crate) fn write(output: &mut (impl io::Write + ?Sized), bytes: &[u8]) -> io::Result<usize> {
47    output.write_all(bytes)?;
48    Ok(bytes.len())
49}
50
51/// If `pred` is true, write all bytes to the output, returning the number of bytes written.
52#[inline]
53pub(crate) fn write_if(
54    output: &mut (impl io::Write + ?Sized),
55    pred: bool,
56    bytes: &[u8],
57) -> io::Result<usize> {
58    if pred { write(output, bytes) } else { Ok(0) }
59}
60
61/// If `pred` is true, write `true_bytes` to the output. Otherwise, write `false_bytes`.
62#[inline]
63pub(crate) fn write_if_else(
64    output: &mut (impl io::Write + ?Sized),
65    pred: bool,
66    true_bytes: &[u8],
67    false_bytes: &[u8],
68) -> io::Result<usize> {
69    write(output, if pred { true_bytes } else { false_bytes })
70}
71
72/// Helper function to obtain 10^x, guaranteeing determinism for x ≤ 9. For these cases, the
73/// function optimizes to a lookup table. For x ≥ 10, it falls back to `10_f64.powi(x)`. The only
74/// situation where this would occur is if the user explicitly requests such precision when
75/// configuring the ISO 8601 well known format. All other possibilities max out at nine digits.
76#[inline]
77fn f64_10_pow_x(x: NonZero<u8>) -> f64 {
78    match x.get() {
79        1 => 10.,
80        2 => 100.,
81        3 => 1_000.,
82        4 => 10_000.,
83        5 => 100_000.,
84        6 => 1_000_000.,
85        7 => 10_000_000.,
86        8 => 100_000_000.,
87        9 => 1_000_000_000.,
88        x => 10_f64.powi(x.cast_signed().extend()),
89    }
90}
91
92/// Write the floating point number to the output, returning the number of bytes written.
93///
94/// This method accepts the number of digits before and after the decimal. The value will be padded
95/// with zeroes to the left if necessary.
96#[inline]
97pub(crate) fn format_float(
98    output: &mut (impl io::Write + ?Sized),
99    mut value: f64,
100    digits_before_decimal: u8,
101    digits_after_decimal: Option<NonZero<u8>>,
102) -> io::Result<usize> {
103    match digits_after_decimal {
104        Some(digits_after_decimal) => {
105            // If the precision is less than nine digits after the decimal point, truncate the
106            // value. This avoids rounding up and causing the value to exceed the maximum permitted
107            // value (as in #678). If the precision is at least nine, then we don't truncate so as
108            // to avoid having an off-by-one error (as in #724). The latter is necessary
109            // because floating point values are inherently imprecise with decimal
110            // values, so a minuscule error can be amplified easily.
111            //
112            // Note that this is largely an issue for second values, as for minute and hour decimals
113            // the value is divided by 60 or 3,600, neither of which divide evenly into 10^x.
114            //
115            // While not a perfect approach, this addresses the bugs that have been reported so far
116            // without being overly complex.
117            if digits_after_decimal.get() < 9 {
118                let trunc_num = f64_10_pow_x(digits_after_decimal);
119                value = f64::trunc(value * trunc_num) / trunc_num;
120            }
121
122            let digits_after_decimal = digits_after_decimal.get().extend();
123            let width = digits_before_decimal.extend::<usize>() + 1 + digits_after_decimal;
124            write!(output, "{value:0>width$.digits_after_decimal$}")?;
125            Ok(width)
126        }
127        None => {
128            let value = value.trunc() as u64;
129            let width = digits_before_decimal.extend();
130            write!(output, "{value:0>width$}")?;
131            Ok(width)
132        }
133    }
134}
135
136/// Format a number with the provided padding and width.
137///
138/// The sign must be written by the caller.
139#[inline]
140pub(crate) fn format_number<const WIDTH: u8>(
141    output: &mut (impl io::Write + ?Sized),
142    value: impl itoa::Integer + DigitCount + Copy,
143    padding: modifier::Padding,
144) -> Result<usize, io::Error> {
145    match padding {
146        modifier::Padding::Space => format_number_pad_space::<WIDTH>(output, value),
147        modifier::Padding::Zero => format_number_pad_zero::<WIDTH>(output, value),
148        modifier::Padding::None => format_number_pad_none(output, value),
149    }
150}
151
152/// Format a number with the provided width and spaces as padding.
153///
154/// The sign must be written by the caller.
155#[inline]
156pub(crate) fn format_number_pad_space<const WIDTH: u8>(
157    output: &mut (impl io::Write + ?Sized),
158    value: impl itoa::Integer + DigitCount + Copy,
159) -> Result<usize, io::Error> {
160    let mut bytes = 0;
161    for _ in 0..(WIDTH.saturating_sub(value.num_digits())) {
162        bytes += write(output, b" ")?;
163    }
164    bytes += write(output, itoa::Buffer::new().format(value).as_bytes())?;
165    Ok(bytes)
166}
167
168/// Format a number with the provided width and zeros as padding.
169///
170/// The sign must be written by the caller.
171#[inline]
172pub(crate) fn format_number_pad_zero<const WIDTH: u8>(
173    output: &mut (impl io::Write + ?Sized),
174    value: impl itoa::Integer + DigitCount + Copy,
175) -> Result<usize, io::Error> {
176    let mut bytes = 0;
177    for _ in 0..(WIDTH.saturating_sub(value.num_digits())) {
178        bytes += write(output, b"0")?;
179    }
180    bytes += write(output, itoa::Buffer::new().format(value).as_bytes())?;
181    Ok(bytes)
182}
183
184/// Format a number with no padding.
185///
186/// If the sign is mandatory, the sign must be written by the caller.
187#[inline]
188pub(crate) fn format_number_pad_none(
189    output: &mut (impl io::Write + ?Sized),
190    value: impl itoa::Integer + Copy,
191) -> Result<usize, io::Error> {
192    write(output, itoa::Buffer::new().format(value).as_bytes())
193}
194
195/// Format the provided component into the designated output. An `Err` will be returned if the
196/// component requires information that it does not provide or if the value cannot be output to the
197/// stream.
198#[inline]
199pub(crate) fn format_component<V>(
200    output: &mut (impl io::Write + ?Sized),
201    component: Component,
202    value: &V,
203    state: &mut V::State,
204) -> Result<usize, error::Format>
205where
206    V: ComponentProvider,
207{
208    use Component::*;
209    Ok(match component {
210        Day(modifier) if V::SUPPLIES_DATE => {
211            try_likely_ok!(fmt_day(output, value.day(state), modifier))
212        }
213        Month(modifier) if V::SUPPLIES_DATE => {
214            try_likely_ok!(fmt_month(output, value.month(state), modifier))
215        }
216        Ordinal(modifier) if V::SUPPLIES_DATE => {
217            try_likely_ok!(fmt_ordinal(output, value.ordinal(state), modifier))
218        }
219        Weekday(modifier) if V::SUPPLIES_DATE => {
220            try_likely_ok!(fmt_weekday(output, value.weekday(state), modifier))
221        }
222        WeekNumber(modifier) if V::SUPPLIES_DATE => try_likely_ok!(fmt_week_number(
223            output,
224            match modifier.repr {
225                modifier::WeekNumberRepr::Iso => value.iso_week_number(state),
226                modifier::WeekNumberRepr::Sunday => value.sunday_based_week(state),
227                modifier::WeekNumberRepr::Monday => value.monday_based_week(state),
228            },
229            modifier,
230        )),
231        Year(modifier) if V::SUPPLIES_DATE => try_likely_ok!(fmt_year(
232            output,
233            if modifier.iso_week_based {
234                value.iso_year(state)
235            } else {
236                value.calendar_year(state)
237            },
238            modifier,
239        )),
240        Hour(modifier) if V::SUPPLIES_TIME => {
241            try_likely_ok!(fmt_hour(output, value.hour(state), modifier))
242        }
243        Minute(modifier) if V::SUPPLIES_TIME => {
244            try_likely_ok!(fmt_minute(output, value.minute(state), modifier))
245        }
246        Period(modifier) if V::SUPPLIES_TIME => {
247            try_likely_ok!(fmt_period(output, value.period(state), modifier))
248        }
249        Second(modifier) if V::SUPPLIES_TIME => {
250            try_likely_ok!(fmt_second(output, value.second(state), modifier))
251        }
252        Subsecond(modifier) if V::SUPPLIES_TIME => {
253            try_likely_ok!(fmt_subsecond(output, value.nanosecond(state), modifier))
254        }
255        OffsetHour(modifier) if V::SUPPLIES_OFFSET => try_likely_ok!(fmt_offset_hour(
256            output,
257            value.offset_is_negative(state),
258            value.offset_hour(state),
259            modifier,
260        )),
261        OffsetMinute(modifier) if V::SUPPLIES_OFFSET => try_likely_ok!(fmt_offset_minute(
262            output,
263            value.offset_minute(state),
264            modifier
265        )),
266        OffsetSecond(modifier) if V::SUPPLIES_OFFSET => try_likely_ok!(fmt_offset_second(
267            output,
268            value.offset_second(state),
269            modifier
270        )),
271        Ignore(_) => 0,
272        UnixTimestamp(modifier) if V::SUPPLIES_TIMESTAMP => match modifier.precision {
273            modifier::UnixTimestampPrecision::Second => try_likely_ok!(fmt_unix_timestamp_seconds(
274                output,
275                value.unix_timestamp_seconds(state),
276                modifier,
277            )),
278            modifier::UnixTimestampPrecision::Millisecond => {
279                try_likely_ok!(fmt_unix_timestamp_milliseconds(
280                    output,
281                    value.unix_timestamp_milliseconds(state),
282                    modifier,
283                ))
284            }
285            modifier::UnixTimestampPrecision::Microsecond => {
286                try_likely_ok!(fmt_unix_timestamp_microseconds(
287                    output,
288                    value.unix_timestamp_microseconds(state),
289                    modifier,
290                ))
291            }
292            modifier::UnixTimestampPrecision::Nanosecond => {
293                try_likely_ok!(fmt_unix_timestamp_nanoseconds(
294                    output,
295                    value.unix_timestamp_nanoseconds(state),
296                    modifier,
297                ))
298            }
299        },
300        End(modifier::End { trailing_input: _ }) => 0,
301
302        // This is functionally the same as a wildcard arm, but it will cause an error if a new
303        // component is added. This is to avoid a bug where a new component, the code compiles, and
304        // formatting fails.
305        // Allow unreachable patterns because some branches may be fully matched above.
306        #[allow(unreachable_patterns)]
307        Day(_) | Month(_) | Ordinal(_) | Weekday(_) | WeekNumber(_) | Year(_) | Hour(_)
308        | Minute(_) | Period(_) | Second(_) | Subsecond(_) | OffsetHour(_) | OffsetMinute(_)
309        | OffsetSecond(_) | Ignore(_) | UnixTimestamp(_) | End(_) => {
310            return Err(error::Format::InsufficientTypeInformation);
311        }
312    })
313}
314
315/// Format the day into the designated output.
316#[inline]
317fn fmt_day(
318    output: &mut (impl io::Write + ?Sized),
319    day: u8,
320    modifier::Day { padding }: modifier::Day,
321) -> Result<usize, io::Error> {
322    format_number::<2>(output, day, padding)
323}
324
325/// Format the month into the designated output.
326#[inline]
327fn fmt_month(
328    output: &mut (impl io::Write + ?Sized),
329    month: Month,
330    modifier::Month {
331        padding,
332        repr,
333        case_sensitive: _, // no effect on formatting
334    }: modifier::Month,
335) -> Result<usize, io::Error> {
336    match repr {
337        modifier::MonthRepr::Numerical => format_number::<2>(output, u8::from(month), padding),
338        modifier::MonthRepr::Long => {
339            write(output, MONTH_NAMES[u8::from(month).extend::<usize>() - 1])
340        }
341        // Safety: All month names are at least three bytes long.
342        modifier::MonthRepr::Short => write(output, unsafe {
343            MONTH_NAMES[u8::from(month).extend::<usize>() - 1].get_unchecked(..3)
344        }),
345    }
346}
347
348/// Format the ordinal into the designated output.
349#[inline]
350fn fmt_ordinal(
351    output: &mut (impl io::Write + ?Sized),
352    ordinal: u16,
353    modifier::Ordinal { padding }: modifier::Ordinal,
354) -> Result<usize, io::Error> {
355    format_number::<3>(output, ordinal, padding)
356}
357
358/// Format the weekday into the designated output.
359#[inline]
360fn fmt_weekday(
361    output: &mut (impl io::Write + ?Sized),
362    weekday: Weekday,
363    modifier::Weekday {
364        repr,
365        one_indexed,
366        case_sensitive: _, // no effect on formatting
367    }: modifier::Weekday,
368) -> Result<usize, io::Error> {
369    match repr {
370        // Safety: All weekday names are at least three bytes long.
371        modifier::WeekdayRepr::Short => write(output, unsafe {
372            WEEKDAY_NAMES[weekday.number_days_from_monday().extend::<usize>()].get_unchecked(..3)
373        }),
374        modifier::WeekdayRepr::Long => write(
375            output,
376            WEEKDAY_NAMES[weekday.number_days_from_monday().extend::<usize>()],
377        ),
378        modifier::WeekdayRepr::Sunday => format_number::<1>(
379            output,
380            weekday.number_days_from_sunday() + u8::from(one_indexed),
381            modifier::Padding::None,
382        ),
383        modifier::WeekdayRepr::Monday => format_number::<1>(
384            output,
385            weekday.number_days_from_monday() + u8::from(one_indexed),
386            modifier::Padding::None,
387        ),
388    }
389}
390
391/// Format the week number into the designated output.
392#[inline]
393fn fmt_week_number(
394    output: &mut (impl io::Write + ?Sized),
395    week_number: u8,
396    modifier::WeekNumber { padding, repr: _ }: modifier::WeekNumber,
397) -> Result<usize, io::Error> {
398    format_number::<2>(output, week_number, padding)
399}
400
401/// Format the year into the designated output.
402fn fmt_year(
403    output: &mut (impl io::Write + ?Sized),
404    full_year: i32,
405    modifier::Year {
406        padding,
407        repr,
408        range,
409        iso_week_based: _,
410        sign_is_mandatory,
411    }: modifier::Year,
412) -> Result<usize, error::Format> {
413    let value = match repr {
414        modifier::YearRepr::Full => full_year,
415        modifier::YearRepr::Century => full_year / 100,
416        modifier::YearRepr::LastTwo => (full_year % 100).abs(),
417    };
418    let format_number = if cfg!(feature = "large-dates") && range == modifier::YearRange::Extended {
419        match repr {
420            modifier::YearRepr::Full if value.abs() >= 100_000 => format_number::<6>,
421            modifier::YearRepr::Full if value.abs() >= 10_000 => format_number::<5>,
422            modifier::YearRepr::Full => format_number::<4>,
423            modifier::YearRepr::Century if value.abs() >= 1_000 => format_number::<4>,
424            modifier::YearRepr::Century if value.abs() >= 100 => format_number::<3>,
425            modifier::YearRepr::Century => format_number::<2>,
426            modifier::YearRepr::LastTwo => format_number::<2>,
427        }
428    } else {
429        match repr {
430            modifier::YearRepr::Full | modifier::YearRepr::Century if full_year.abs() >= 10_000 => {
431                return Err(error::ComponentRange::conditional("year").into());
432            }
433            _ => {}
434        }
435        match repr {
436            modifier::YearRepr::Full => format_number::<4>,
437            modifier::YearRepr::Century => format_number::<2>,
438            modifier::YearRepr::LastTwo => format_number::<2>,
439        }
440    };
441    let mut bytes = 0;
442    if repr != modifier::YearRepr::LastTwo {
443        if full_year < 0 {
444            bytes += write(output, b"-")?;
445        } else if sign_is_mandatory || cfg!(feature = "large-dates") && full_year >= 10_000 {
446            bytes += write(output, b"+")?;
447        }
448    }
449    bytes += format_number(output, value.unsigned_abs(), padding)?;
450    Ok(bytes)
451}
452
453/// Format the hour into the designated output.
454#[inline]
455fn fmt_hour(
456    output: &mut (impl io::Write + ?Sized),
457    hour: u8,
458    modifier::Hour {
459        padding,
460        is_12_hour_clock,
461    }: modifier::Hour,
462) -> Result<usize, io::Error> {
463    let value = match (hour, is_12_hour_clock) {
464        (hour, false) => hour,
465        (0 | 12, true) => 12,
466        (hour, true) if hour < 12 => hour,
467        (hour, true) => hour - 12,
468    };
469    format_number::<2>(output, value, padding)
470}
471
472/// Format the minute into the designated output.
473#[inline]
474fn fmt_minute(
475    output: &mut (impl io::Write + ?Sized),
476    minute: u8,
477    modifier::Minute { padding }: modifier::Minute,
478) -> Result<usize, io::Error> {
479    format_number::<2>(output, minute, padding)
480}
481
482/// Format the period into the designated output.
483#[inline]
484fn fmt_period(
485    output: &mut (impl io::Write + ?Sized),
486    period: Period,
487    modifier::Period {
488        is_uppercase,
489        case_sensitive: _, // no effect on formatting
490    }: modifier::Period,
491) -> Result<usize, io::Error> {
492    write(
493        output,
494        match (period, is_uppercase) {
495            (Period::Am, false) => b"am",
496            (Period::Am, true) => b"AM",
497            (Period::Pm, false) => b"pm",
498            (Period::Pm, true) => b"PM",
499        },
500    )
501}
502
503/// Format the second into the designated output.
504#[inline]
505fn fmt_second(
506    output: &mut (impl io::Write + ?Sized),
507    second: u8,
508    modifier::Second { padding }: modifier::Second,
509) -> Result<usize, io::Error> {
510    format_number::<2>(output, second, padding)
511}
512
513/// Format the subsecond into the designated output.
514#[inline]
515fn fmt_subsecond(
516    output: &mut (impl io::Write + ?Sized),
517    nanos: u32,
518    modifier::Subsecond { digits }: modifier::Subsecond,
519) -> Result<usize, io::Error> {
520    use modifier::SubsecondDigits::*;
521    if digits == Nine || (digits == OneOrMore && !nanos.is_multiple_of(10)) {
522        format_number_pad_zero::<9>(output, nanos)
523    } else if digits == Eight || (digits == OneOrMore && !(nanos / 10).is_multiple_of(10)) {
524        format_number_pad_zero::<8>(output, nanos / 10)
525    } else if digits == Seven || (digits == OneOrMore && !(nanos / 100).is_multiple_of(10)) {
526        format_number_pad_zero::<7>(output, nanos / 100)
527    } else if digits == Six || (digits == OneOrMore && !(nanos / 1_000).is_multiple_of(10)) {
528        format_number_pad_zero::<6>(output, nanos / 1_000)
529    } else if digits == Five || (digits == OneOrMore && !(nanos / 10_000).is_multiple_of(10)) {
530        format_number_pad_zero::<5>(output, nanos / 10_000)
531    } else if digits == Four || (digits == OneOrMore && !(nanos / 100_000).is_multiple_of(10)) {
532        format_number_pad_zero::<4>(output, nanos / 100_000)
533    } else if digits == Three || (digits == OneOrMore && !(nanos / 1_000_000).is_multiple_of(10)) {
534        format_number_pad_zero::<3>(output, nanos / 1_000_000)
535    } else if digits == Two || (digits == OneOrMore && !(nanos / 10_000_000).is_multiple_of(10)) {
536        format_number_pad_zero::<2>(output, nanos / 10_000_000)
537    } else {
538        format_number_pad_zero::<1>(output, nanos / 100_000_000)
539    }
540}
541
542#[inline]
543fn fmt_offset_sign(
544    output: &mut (impl io::Write + ?Sized),
545    is_negative: bool,
546    sign_is_mandatory: bool,
547) -> Result<usize, io::Error> {
548    if is_negative {
549        write(output, b"-")
550    } else if sign_is_mandatory {
551        write(output, b"+")
552    } else {
553        Ok(0)
554    }
555}
556
557/// Format the offset hour into the designated output.
558#[inline]
559fn fmt_offset_hour(
560    output: &mut (impl io::Write + ?Sized),
561    is_negative: bool,
562    hour: i8,
563    modifier::OffsetHour {
564        padding,
565        sign_is_mandatory,
566    }: modifier::OffsetHour,
567) -> Result<usize, io::Error> {
568    let mut bytes = 0;
569    bytes += fmt_offset_sign(output, is_negative, sign_is_mandatory)?;
570    bytes += format_number::<2>(output, hour.unsigned_abs(), padding)?;
571    Ok(bytes)
572}
573
574/// Format the offset minute into the designated output.
575#[inline]
576fn fmt_offset_minute(
577    output: &mut (impl io::Write + ?Sized),
578    offset_minute: i8,
579    modifier::OffsetMinute { padding }: modifier::OffsetMinute,
580) -> Result<usize, io::Error> {
581    format_number::<2>(output, offset_minute.unsigned_abs(), padding)
582}
583
584/// Format the offset second into the designated output.
585#[inline]
586fn fmt_offset_second(
587    output: &mut (impl io::Write + ?Sized),
588    offset_second: i8,
589    modifier::OffsetSecond { padding }: modifier::OffsetSecond,
590) -> Result<usize, io::Error> {
591    format_number::<2>(output, offset_second.unsigned_abs(), padding)
592}
593
594/// Format the Unix timestamp (in seconds) into the designated output.
595#[inline]
596fn fmt_unix_timestamp_seconds(
597    output: &mut (impl io::Write + ?Sized),
598    timestamp: i64,
599    modifier::UnixTimestamp {
600        precision,
601        sign_is_mandatory,
602    }: modifier::UnixTimestamp,
603) -> Result<usize, io::Error> {
604    debug_assert_eq!(precision, modifier::UnixTimestampPrecision::Second);
605
606    let mut bytes = 0;
607    bytes += fmt_offset_sign(output, timestamp < 0, sign_is_mandatory)?;
608    bytes += format_number_pad_none(output, timestamp.unsigned_abs())?;
609    Ok(bytes)
610}
611
612/// Format the Unix timestamp (in milliseconds) into the designated output.
613#[inline]
614fn fmt_unix_timestamp_milliseconds(
615    output: &mut (impl io::Write + ?Sized),
616    timestamp_millis: i64,
617    modifier::UnixTimestamp {
618        precision,
619        sign_is_mandatory,
620    }: modifier::UnixTimestamp,
621) -> Result<usize, io::Error> {
622    debug_assert_eq!(precision, modifier::UnixTimestampPrecision::Millisecond);
623
624    let mut bytes = 0;
625    bytes += fmt_offset_sign(output, timestamp_millis < 0, sign_is_mandatory)?;
626    bytes += format_number_pad_none(output, timestamp_millis.unsigned_abs())?;
627    Ok(bytes)
628}
629
630/// Format the Unix timestamp into the designated output.
631#[inline]
632fn fmt_unix_timestamp_microseconds(
633    output: &mut (impl io::Write + ?Sized),
634    timestamp_micros: i128,
635    modifier::UnixTimestamp {
636        precision,
637        sign_is_mandatory,
638    }: modifier::UnixTimestamp,
639) -> Result<usize, io::Error> {
640    debug_assert_eq!(precision, modifier::UnixTimestampPrecision::Microsecond);
641
642    let mut bytes = 0;
643    bytes += fmt_offset_sign(output, timestamp_micros < 0, sign_is_mandatory)?;
644    bytes += format_number_pad_none(output, timestamp_micros.unsigned_abs())?;
645    Ok(bytes)
646}
647
648/// Format the Unix timestamp into the designated output.
649#[inline]
650fn fmt_unix_timestamp_nanoseconds(
651    output: &mut (impl io::Write + ?Sized),
652    timestamp_nanos: i128,
653    modifier::UnixTimestamp {
654        precision,
655        sign_is_mandatory,
656    }: modifier::UnixTimestamp,
657) -> Result<usize, io::Error> {
658    debug_assert_eq!(precision, modifier::UnixTimestampPrecision::Nanosecond);
659
660    let mut bytes = 0;
661    bytes += fmt_offset_sign(output, timestamp_nanos < 0, sign_is_mandatory)?;
662    bytes += format_number_pad_none(output, timestamp_nanos.unsigned_abs())?;
663    Ok(bytes)
664}