time/formatting/
mod.rs

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