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