time/formatting/
iso8601.rs

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