Skip to main content

time/formatting/
iso8601.rs

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