time/formatting/
mod.rs

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