Skip to main content

time/formatting/
iso8601.rs

1//! Helpers for implementing formatting for ISO 8601.
2
3use std::io;
4
5use deranged::{ru8, ru16, ru32};
6use num_conv::prelude::*;
7
8use crate::error;
9use crate::format_description::modifier::Padding;
10use crate::format_description::well_known::Iso8601;
11use crate::format_description::well_known::iso8601::{
12    DateKind, EncodedConfig, OffsetPrecision, TimePrecision,
13};
14use crate::formatting::{
15    ComponentProvider, format_float, format_four_digits_pad_zero, format_single_digit,
16    format_six_digits_pad_zero, format_three_digits, format_two_digits, write, write_if,
17    write_if_else,
18};
19use crate::unit::*;
20
21/// Format the date portion of ISO 8601.
22pub(super) fn format_date<V, const CONFIG: EncodedConfig>(
23    output: &mut (impl io::Write + ?Sized),
24    value: &V,
25    state: &mut V::State,
26) -> Result<usize, error::Format>
27where
28    V: ComponentProvider,
29{
30    let mut bytes = 0;
31
32    match Iso8601::<CONFIG>::DATE_KIND {
33        DateKind::Calendar => {
34            let year = value.calendar_year(state).get();
35
36            if Iso8601::<CONFIG>::YEAR_IS_SIX_DIGITS {
37                bytes += write_if_else(output, year < 0, "-", "+")?;
38                // Safety: `calendar_year` returns a value whose absolute value is guaranteed to be
39                // less than 1,000,000.
40                bytes += format_six_digits_pad_zero(output, unsafe {
41                    ru32::new_unchecked(year.unsigned_abs())
42                })?;
43            } else {
44                let year = ru16::new(year.cast_unsigned().truncate())
45                    .ok_or(error::Format::InvalidComponent("year"))?;
46                bytes += format_four_digits_pad_zero(output, year)?;
47            }
48            bytes += write_if(output, Iso8601::<CONFIG>::USE_SEPARATORS, "-")?;
49            // Safety: `month` is guaranteed to be in the range `1..=12`.
50            bytes += format_two_digits(
51                output,
52                unsafe { ru8::new_unchecked(u8::from(value.month(state))) },
53                Padding::Zero,
54            )?;
55            bytes += write_if(output, Iso8601::<CONFIG>::USE_SEPARATORS, "-")?;
56            bytes += format_two_digits(output, value.day(state).expand(), Padding::Zero)?;
57        }
58        DateKind::Week => {
59            let year = value.iso_year(state).get();
60
61            if Iso8601::<CONFIG>::YEAR_IS_SIX_DIGITS {
62                bytes += write_if_else(output, year < 0, "-", "+")?;
63                // Safety: `iso_year` returns a value whose absolute value is guaranteed to be less
64                // than 1,000,000.
65                bytes += format_six_digits_pad_zero(output, unsafe {
66                    ru32::new_unchecked(year.unsigned_abs())
67                })?;
68            } else {
69                let year = ru16::new(year.cast_unsigned().truncate())
70                    .ok_or(error::Format::InvalidComponent("year"))?;
71                bytes += format_four_digits_pad_zero(output, year)?;
72            }
73            bytes += write_if_else(output, Iso8601::<CONFIG>::USE_SEPARATORS, "-W", "W")?;
74            bytes +=
75                format_two_digits(output, value.iso_week_number(state).expand(), Padding::Zero)?;
76            bytes += write_if(output, Iso8601::<CONFIG>::USE_SEPARATORS, "-")?;
77            // Safety: The value is in the range `1..=7`.
78            bytes += format_single_digit(output, unsafe {
79                ru8::new_unchecked(value.weekday(state).number_from_monday())
80            })?;
81        }
82        DateKind::Ordinal => {
83            let year = value.calendar_year(state).get();
84
85            if Iso8601::<CONFIG>::YEAR_IS_SIX_DIGITS {
86                bytes += write_if_else(output, year < 0, "-", "+")?;
87                // Safety: `calendar_year` returns a value whose absolute value is guaranteed to be
88                // less than 1,000,000.
89                bytes += format_six_digits_pad_zero(output, unsafe {
90                    ru32::new_unchecked(year.unsigned_abs())
91                })?;
92            } else {
93                let year = ru16::new(year.cast_unsigned().truncate())
94                    .ok_or(error::Format::InvalidComponent("year"))?;
95                bytes += format_four_digits_pad_zero(output, year)?;
96            }
97            bytes += write_if(output, Iso8601::<CONFIG>::USE_SEPARATORS, "-")?;
98            bytes += format_three_digits(output, value.ordinal(state).expand(), Padding::Zero)?;
99        }
100    }
101
102    Ok(bytes)
103}
104
105/// Format the time portion of ISO 8601.
106#[inline]
107pub(super) fn format_time<V, const CONFIG: EncodedConfig>(
108    output: &mut (impl io::Write + ?Sized),
109    value: &V,
110    state: &mut V::State,
111) -> Result<usize, error::Format>
112where
113    V: ComponentProvider,
114{
115    let mut bytes = 0;
116
117    // The "T" can only be omitted in extended format where there is no date being formatted.
118    bytes += write_if(
119        output,
120        !Iso8601::<CONFIG>::USE_SEPARATORS || Iso8601::<CONFIG>::FORMAT_DATE,
121        "T",
122    )?;
123
124    match Iso8601::<CONFIG>::TIME_PRECISION {
125        TimePrecision::Hour { decimal_digits } => {
126            let hours = (value.hour(state).get() as f64)
127                + (value.minute(state).get() as f64) / Minute::per_t::<f64>(Hour)
128                + (value.second(state).get() as f64) / Second::per_t::<f64>(Hour)
129                + (value.nanosecond(state).get() as f64) / Nanosecond::per_t::<f64>(Hour);
130            format_float(output, hours, 2, decimal_digits)?;
131        }
132        TimePrecision::Minute { decimal_digits } => {
133            bytes += format_two_digits(output, value.hour(state).expand(), Padding::Zero)?;
134            bytes += write_if(output, Iso8601::<CONFIG>::USE_SEPARATORS, ":")?;
135            let minutes = (value.minute(state).get() as f64)
136                + (value.second(state).get() as f64) / Second::per_t::<f64>(Minute)
137                + (value.nanosecond(state).get() as f64) / Nanosecond::per_t::<f64>(Minute);
138            bytes += format_float(output, minutes, 2, decimal_digits)?;
139        }
140        TimePrecision::Second { decimal_digits } => {
141            bytes += format_two_digits(output, value.hour(state).expand(), Padding::Zero)?;
142            bytes += write_if(output, Iso8601::<CONFIG>::USE_SEPARATORS, ":")?;
143            bytes += format_two_digits(output, value.minute(state).expand(), Padding::Zero)?;
144            bytes += write_if(output, Iso8601::<CONFIG>::USE_SEPARATORS, ":")?;
145            let seconds = (value.second(state).get() as f64)
146                + (value.nanosecond(state).get() as f64) / Nanosecond::per_t::<f64>(Second);
147            bytes += format_float(output, seconds, 2, decimal_digits)?;
148        }
149    }
150
151    Ok(bytes)
152}
153
154/// Format the UTC offset portion of ISO 8601.
155#[inline]
156pub(super) fn format_offset<V, const CONFIG: EncodedConfig>(
157    output: &mut (impl io::Write + ?Sized),
158    value: &V,
159    state: &mut V::State,
160) -> Result<usize, error::Format>
161where
162    V: ComponentProvider,
163{
164    if Iso8601::<CONFIG>::FORMAT_TIME && value.offset_is_utc(state) {
165        return Ok(write(output, "Z")?);
166    }
167
168    let mut bytes = 0;
169
170    if value.offset_second(state).get() != 0 {
171        return Err(error::Format::InvalidComponent("offset_second"));
172    }
173    bytes += write_if_else(output, value.offset_is_negative(state), "-", "+")?;
174    // Safety: The value is in the range `-25..=25`.
175    bytes += format_two_digits(
176        output,
177        unsafe { ru8::new_unchecked(value.offset_hour(state).get().unsigned_abs()) },
178        Padding::Zero,
179    )?;
180
181    let minutes = value.offset_minute(state);
182
183    if Iso8601::<CONFIG>::OFFSET_PRECISION == OffsetPrecision::Hour && minutes.get() != 0 {
184        return Err(error::Format::InvalidComponent("offset_minute"));
185    } else if Iso8601::<CONFIG>::OFFSET_PRECISION == OffsetPrecision::Minute {
186        bytes += write_if(output, Iso8601::<CONFIG>::USE_SEPARATORS, ":")?;
187        // Safety: The value is in the range `0..=59`.
188        bytes += format_two_digits(
189            output,
190            unsafe { ru8::new_unchecked(minutes.get().unsigned_abs()) },
191            Padding::Zero,
192        )?;
193    }
194
195    Ok(bytes)
196}