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