time/formatting/
iso8601.rs

1//! Helpers for implementing formatting for ISO 8601.
2
3use std::io;
4
5use num_conv::prelude::*;
6
7use crate::convert::*;
8use crate::format_description::well_known::iso8601::{
9    DateKind, EncodedConfig, OffsetPrecision, TimePrecision,
10};
11use crate::format_description::well_known::Iso8601;
12use crate::formatting::{format_float, format_number_pad_zero, write, write_if, write_if_else};
13use crate::{error, Date, Time, UtcOffset};
14
15/// Format the date portion of ISO 8601.
16pub(super) fn format_date<const CONFIG: EncodedConfig>(
17    output: &mut (impl io::Write + ?Sized),
18    date: Date,
19) -> Result<usize, error::Format> {
20    let mut bytes = 0;
21
22    match Iso8601::<CONFIG>::DATE_KIND {
23        DateKind::Calendar => {
24            let (year, month, day) = date.to_calendar_date();
25            if Iso8601::<CONFIG>::YEAR_IS_SIX_DIGITS {
26                bytes += write_if_else(output, year < 0, b"-", b"+")?;
27                bytes += format_number_pad_zero::<6>(output, year.unsigned_abs())?;
28            } else if !(0..=9999).contains(&year) {
29                return Err(error::Format::InvalidComponent("year"));
30            } else {
31                bytes += format_number_pad_zero::<4>(output, year.cast_unsigned())?;
32            }
33            bytes += write_if(output, Iso8601::<CONFIG>::USE_SEPARATORS, b"-")?;
34            bytes += format_number_pad_zero::<2>(output, u8::from(month))?;
35            bytes += write_if(output, Iso8601::<CONFIG>::USE_SEPARATORS, b"-")?;
36            bytes += format_number_pad_zero::<2>(output, day)?;
37        }
38        DateKind::Week => {
39            let (year, week, day) = date.to_iso_week_date();
40            if Iso8601::<CONFIG>::YEAR_IS_SIX_DIGITS {
41                bytes += write_if_else(output, year < 0, b"-", b"+")?;
42                bytes += format_number_pad_zero::<6>(output, year.unsigned_abs())?;
43            } else if !(0..=9999).contains(&year) {
44                return Err(error::Format::InvalidComponent("year"));
45            } else {
46                bytes += format_number_pad_zero::<4>(output, year.cast_unsigned())?;
47            }
48            bytes += write_if_else(output, Iso8601::<CONFIG>::USE_SEPARATORS, b"-W", b"W")?;
49            bytes += format_number_pad_zero::<2>(output, week)?;
50            bytes += write_if(output, Iso8601::<CONFIG>::USE_SEPARATORS, b"-")?;
51            bytes += format_number_pad_zero::<1>(output, day.number_from_monday())?;
52        }
53        DateKind::Ordinal => {
54            let (year, day) = date.to_ordinal_date();
55            if Iso8601::<CONFIG>::YEAR_IS_SIX_DIGITS {
56                bytes += write_if_else(output, year < 0, b"-", b"+")?;
57                bytes += format_number_pad_zero::<6>(output, year.unsigned_abs())?;
58            } else if !(0..=9999).contains(&year) {
59                return Err(error::Format::InvalidComponent("year"));
60            } else {
61                bytes += format_number_pad_zero::<4>(output, year.cast_unsigned())?;
62            }
63            bytes += write_if(output, Iso8601::<CONFIG>::USE_SEPARATORS, b"-")?;
64            bytes += format_number_pad_zero::<3>(output, day)?;
65        }
66    }
67
68    Ok(bytes)
69}
70
71/// Format the time portion of ISO 8601.
72pub(super) fn format_time<const CONFIG: EncodedConfig>(
73    output: &mut (impl io::Write + ?Sized),
74    time: Time,
75) -> Result<usize, error::Format> {
76    let mut bytes = 0;
77
78    // The "T" can only be omitted in extended format where there is no date being formatted.
79    bytes += write_if(
80        output,
81        Iso8601::<CONFIG>::USE_SEPARATORS || Iso8601::<CONFIG>::FORMAT_DATE,
82        b"T",
83    )?;
84
85    let (hours, minutes, seconds, nanoseconds) = time.as_hms_nano();
86
87    match Iso8601::<CONFIG>::TIME_PRECISION {
88        TimePrecision::Hour { decimal_digits } => {
89            let hours = (hours as f64)
90                + (minutes as f64) / Minute::per(Hour) as f64
91                + (seconds as f64) / Second::per(Hour) as f64
92                + (nanoseconds as f64) / Nanosecond::per(Hour) as f64;
93            format_float(output, hours, 2, decimal_digits)?;
94        }
95        TimePrecision::Minute { decimal_digits } => {
96            bytes += format_number_pad_zero::<2>(output, hours)?;
97            bytes += write_if(output, Iso8601::<CONFIG>::USE_SEPARATORS, b":")?;
98            let minutes = (minutes as f64)
99                + (seconds as f64) / Second::per(Minute) as f64
100                + (nanoseconds as f64) / Nanosecond::per(Minute) as f64;
101            bytes += format_float(output, minutes, 2, decimal_digits)?;
102        }
103        TimePrecision::Second { decimal_digits } => {
104            bytes += format_number_pad_zero::<2>(output, hours)?;
105            bytes += write_if(output, Iso8601::<CONFIG>::USE_SEPARATORS, b":")?;
106            bytes += format_number_pad_zero::<2>(output, minutes)?;
107            bytes += write_if(output, Iso8601::<CONFIG>::USE_SEPARATORS, b":")?;
108            let seconds = (seconds as f64) + (nanoseconds as f64) / Nanosecond::per(Second) as f64;
109            bytes += format_float(output, seconds, 2, decimal_digits)?;
110        }
111    }
112
113    Ok(bytes)
114}
115
116/// Format the UTC offset portion of ISO 8601.
117pub(super) fn format_offset<const CONFIG: EncodedConfig>(
118    output: &mut (impl io::Write + ?Sized),
119    offset: UtcOffset,
120) -> Result<usize, error::Format> {
121    if Iso8601::<CONFIG>::FORMAT_TIME && offset.is_utc() {
122        return Ok(write(output, b"Z")?);
123    }
124
125    let mut bytes = 0;
126
127    let (hours, minutes, seconds) = offset.as_hms();
128    if seconds != 0 {
129        return Err(error::Format::InvalidComponent("offset_second"));
130    }
131    bytes += write_if_else(output, offset.is_negative(), b"-", b"+")?;
132    bytes += format_number_pad_zero::<2>(output, hours.unsigned_abs())?;
133
134    if Iso8601::<CONFIG>::OFFSET_PRECISION == OffsetPrecision::Hour && minutes != 0 {
135        return Err(error::Format::InvalidComponent("offset_minute"));
136    } else if Iso8601::<CONFIG>::OFFSET_PRECISION == OffsetPrecision::Minute {
137        bytes += write_if(output, Iso8601::<CONFIG>::USE_SEPARATORS, b":")?;
138        bytes += format_number_pad_zero::<2>(output, minutes.unsigned_abs())?;
139    }
140
141    Ok(bytes)
142}