time/parsing/
iso8601.rs

1//! Parse parts of an ISO 8601-formatted value.
2
3use num_conv::prelude::*;
4
5use crate::convert::*;
6use crate::error;
7use crate::error::ParseFromDescription::{InvalidComponent, InvalidLiteral};
8use crate::format_description::well_known::iso8601::EncodedConfig;
9use crate::format_description::well_known::Iso8601;
10use crate::parsing::combinator::rfc::iso8601::{
11    day, dayk, dayo, float, hour, min, month, week, year, ExtendedKind,
12};
13use crate::parsing::combinator::{ascii_char, sign};
14use crate::parsing::{Parsed, ParsedItem};
15
16impl<const CONFIG: EncodedConfig> Iso8601<CONFIG> {
17    // Basic: [year][month][day]
18    // Extended: [year]["-"][month]["-"][day]
19    // Basic: [year][dayo]
20    // Extended: [year]["-"][dayo]
21    // Basic: [year]["W"][week][dayk]
22    // Extended: [year]["-"]["W"][week]["-"][dayk]
23    /// Parse a date in the basic or extended format. Reduced precision is permitted.
24    pub(crate) fn parse_date<'a>(
25        parsed: &'a mut Parsed,
26        extended_kind: &'a mut ExtendedKind,
27    ) -> impl FnMut(&[u8]) -> Result<&[u8], error::Parse> + 'a {
28        move |input| {
29            // Same for any acceptable format.
30            let ParsedItem(mut input, year) = year(input).ok_or(InvalidComponent("year"))?;
31            *extended_kind = match ascii_char::<b'-'>(input) {
32                Some(ParsedItem(new_input, ())) => {
33                    input = new_input;
34                    ExtendedKind::Extended
35                }
36                None => ExtendedKind::Basic, // no separator before mandatory month/ordinal/week
37            };
38
39            let parsed_month_day = (|| {
40                let ParsedItem(mut input, month) = month(input).ok_or(InvalidComponent("month"))?;
41                if extended_kind.is_extended() {
42                    input = ascii_char::<b'-'>(input)
43                        .ok_or(InvalidLiteral)?
44                        .into_inner();
45                }
46                let ParsedItem(input, day) = day(input).ok_or(InvalidComponent("day"))?;
47                Ok(ParsedItem(input, (month, day)))
48            })();
49            let mut ret_error = match parsed_month_day {
50                Ok(ParsedItem(input, (month, day))) => {
51                    *parsed = parsed
52                        .with_year(year)
53                        .ok_or(InvalidComponent("year"))?
54                        .with_month(month)
55                        .ok_or(InvalidComponent("month"))?
56                        .with_day(day)
57                        .ok_or(InvalidComponent("day"))?;
58                    return Ok(input);
59                }
60                Err(err) => err,
61            };
62
63            // Don't check for `None`, as the error from year-month-day will always take priority.
64            if let Some(ParsedItem(input, ordinal)) = dayo(input) {
65                *parsed = parsed
66                    .with_year(year)
67                    .ok_or(InvalidComponent("year"))?
68                    .with_ordinal(ordinal)
69                    .ok_or(InvalidComponent("ordinal"))?;
70                return Ok(input);
71            }
72
73            let parsed_week_weekday = (|| {
74                let input = ascii_char::<b'W'>(input)
75                    .ok_or((false, InvalidLiteral))?
76                    .into_inner();
77                let ParsedItem(mut input, week) =
78                    week(input).ok_or((true, InvalidComponent("week")))?;
79                if extended_kind.is_extended() {
80                    input = ascii_char::<b'-'>(input)
81                        .ok_or((true, InvalidLiteral))?
82                        .into_inner();
83                }
84                let ParsedItem(input, weekday) =
85                    dayk(input).ok_or((true, InvalidComponent("weekday")))?;
86                Ok(ParsedItem(input, (week, weekday)))
87            })();
88            match parsed_week_weekday {
89                Ok(ParsedItem(input, (week, weekday))) => {
90                    *parsed = parsed
91                        .with_iso_year(year)
92                        .ok_or(InvalidComponent("year"))?
93                        .with_iso_week_number(week)
94                        .ok_or(InvalidComponent("week"))?
95                        .with_weekday(weekday)
96                        .ok_or(InvalidComponent("weekday"))?;
97                    return Ok(input);
98                }
99                Err((false, _err)) => {}
100                // This error is more accurate than the one from year-month-day.
101                Err((true, err)) => ret_error = err,
102            }
103
104            Err(ret_error.into())
105        }
106    }
107
108    // Basic: ["T"][hour][min][sec]
109    // Extended: ["T"][hour][":"][min][":"][sec]
110    // Reduced precision: components after [hour] (including their preceding separator) can be
111    // omitted. ["T"] can be omitted if there is no date present.
112    /// Parse a time in the basic or extended format. Reduced precision is permitted.
113    pub(crate) fn parse_time<'a>(
114        parsed: &'a mut Parsed,
115        extended_kind: &'a mut ExtendedKind,
116        date_is_present: bool,
117    ) -> impl FnMut(&[u8]) -> Result<&[u8], error::Parse> + 'a {
118        move |mut input| {
119            if date_is_present {
120                input = ascii_char::<b'T'>(input)
121                    .ok_or(InvalidLiteral)?
122                    .into_inner();
123            }
124
125            let ParsedItem(mut input, hour) = float(input).ok_or(InvalidComponent("hour"))?;
126            match hour {
127                (hour, None) => parsed.set_hour_24(hour).ok_or(InvalidComponent("hour"))?,
128                (hour, Some(fractional_part)) => {
129                    *parsed = parsed
130                        .with_hour_24(hour)
131                        .ok_or(InvalidComponent("hour"))?
132                        .with_minute((fractional_part * Second::per(Minute) as f64) as u8)
133                        .ok_or(InvalidComponent("minute"))?
134                        .with_second(
135                            (fractional_part * Second::per(Hour) as f64 % Minute::per(Hour) as f64)
136                                as u8,
137                        )
138                        .ok_or(InvalidComponent("second"))?
139                        .with_subsecond(
140                            (fractional_part * Nanosecond::per(Hour) as f64
141                                % Nanosecond::per(Second) as f64)
142                                as u32,
143                        )
144                        .ok_or(InvalidComponent("subsecond"))?;
145                    return Ok(input);
146                }
147            };
148
149            if let Some(ParsedItem(new_input, ())) = ascii_char::<b':'>(input) {
150                extended_kind
151                    .coerce_extended()
152                    .ok_or(InvalidComponent("minute"))?;
153                input = new_input;
154            };
155
156            let mut input = match float(input) {
157                Some(ParsedItem(input, (minute, None))) => {
158                    extended_kind.coerce_basic();
159                    parsed
160                        .set_minute(minute)
161                        .ok_or(InvalidComponent("minute"))?;
162                    input
163                }
164                Some(ParsedItem(input, (minute, Some(fractional_part)))) => {
165                    // `None` is valid behavior, so don't error if this fails.
166                    extended_kind.coerce_basic();
167                    *parsed = parsed
168                        .with_minute(minute)
169                        .ok_or(InvalidComponent("minute"))?
170                        .with_second((fractional_part * Second::per(Minute) as f64) as u8)
171                        .ok_or(InvalidComponent("second"))?
172                        .with_subsecond(
173                            (fractional_part * Nanosecond::per(Minute) as f64
174                                % Nanosecond::per(Second) as f64)
175                                as u32,
176                        )
177                        .ok_or(InvalidComponent("subsecond"))?;
178                    return Ok(input);
179                }
180                // colon was present, so minutes are required
181                None if extended_kind.is_extended() => {
182                    return Err(error::Parse::ParseFromDescription(InvalidComponent(
183                        "minute",
184                    )));
185                }
186                None => {
187                    // Missing components are assumed to be zero.
188                    *parsed = parsed
189                        .with_minute(0)
190                        .ok_or(InvalidComponent("minute"))?
191                        .with_second(0)
192                        .ok_or(InvalidComponent("second"))?
193                        .with_subsecond(0)
194                        .ok_or(InvalidComponent("subsecond"))?;
195                    return Ok(input);
196                }
197            };
198
199            if extended_kind.is_extended() {
200                match ascii_char::<b':'>(input) {
201                    Some(ParsedItem(new_input, ())) => input = new_input,
202                    None => {
203                        *parsed = parsed
204                            .with_second(0)
205                            .ok_or(InvalidComponent("second"))?
206                            .with_subsecond(0)
207                            .ok_or(InvalidComponent("subsecond"))?;
208                        return Ok(input);
209                    }
210                }
211            }
212
213            let (input, second, subsecond) = match float(input) {
214                Some(ParsedItem(input, (second, None))) => (input, second, 0),
215                Some(ParsedItem(input, (second, Some(fractional_part)))) => (
216                    input,
217                    second,
218                    round(fractional_part * Nanosecond::per(Second) as f64) as u32,
219                ),
220                None if extended_kind.is_extended() => {
221                    return Err(error::Parse::ParseFromDescription(InvalidComponent(
222                        "second",
223                    )));
224                }
225                // Missing components are assumed to be zero.
226                None => (input, 0, 0),
227            };
228            *parsed = parsed
229                .with_second(second)
230                .ok_or(InvalidComponent("second"))?
231                .with_subsecond(subsecond)
232                .ok_or(InvalidComponent("subsecond"))?;
233
234            Ok(input)
235        }
236    }
237
238    // Basic: [±][hour][min] or ["Z"]
239    // Extended: [±][hour][":"][min] or ["Z"]
240    // Reduced precision: [±][hour] or ["Z"]
241    /// Parse a UTC offset in the basic or extended format. Reduced precision is supported.
242    pub(crate) fn parse_offset<'a>(
243        parsed: &'a mut Parsed,
244        extended_kind: &'a mut ExtendedKind,
245    ) -> impl FnMut(&[u8]) -> Result<&[u8], error::Parse> + 'a {
246        move |input| {
247            if let Some(ParsedItem(input, ())) = ascii_char::<b'Z'>(input) {
248                *parsed = parsed
249                    .with_offset_hour(0)
250                    .ok_or(InvalidComponent("offset hour"))?
251                    .with_offset_minute_signed(0)
252                    .ok_or(InvalidComponent("offset minute"))?
253                    .with_offset_second_signed(0)
254                    .ok_or(InvalidComponent("offset second"))?;
255                return Ok(input);
256            }
257
258            let ParsedItem(input, sign) = sign(input).ok_or(InvalidComponent("offset hour"))?;
259            let mut input = hour(input)
260                .and_then(|parsed_item| {
261                    parsed_item.consume_value(|hour| {
262                        parsed.set_offset_hour(if sign == b'-' {
263                            -hour.cast_signed()
264                        } else {
265                            hour.cast_signed()
266                        })
267                    })
268                })
269                .ok_or(InvalidComponent("offset hour"))?;
270
271            if extended_kind.maybe_extended() {
272                if let Some(ParsedItem(new_input, ())) = ascii_char::<b':'>(input) {
273                    extended_kind
274                        .coerce_extended()
275                        .ok_or(InvalidComponent("offset minute"))?;
276                    input = new_input;
277                };
278            }
279
280            match min(input) {
281                Some(ParsedItem(new_input, min)) => {
282                    input = new_input;
283                    parsed
284                        .set_offset_minute_signed(if sign == b'-' {
285                            -min.cast_signed()
286                        } else {
287                            min.cast_signed()
288                        })
289                        .ok_or(InvalidComponent("offset minute"))?;
290                }
291                None => {
292                    // Omitted offset minute is assumed to be zero.
293                    parsed.set_offset_minute_signed(0);
294                }
295            }
296
297            // If `:` was present, the format has already been set to extended. As such, this call
298            // will do nothing in that case. If there wasn't `:` but minutes were
299            // present, we know it's the basic format. Do not use `?` on the call, as
300            // returning `None` is valid behavior.
301            extended_kind.coerce_basic();
302
303            Ok(input)
304        }
305    }
306}
307
308/// Round wrapper that uses hardware implementation if `std` is available, falling back to manual
309/// implementation for `no_std`
310fn round(value: f64) -> f64 {
311    #[cfg(feature = "std")]
312    {
313        value.round()
314    }
315    #[cfg(not(feature = "std"))]
316    {
317        round_impl(value)
318    }
319}
320
321#[cfg(not(feature = "std"))]
322#[allow(clippy::missing_docs_in_private_items)]
323fn round_impl(value: f64) -> f64 {
324    debug_assert!(value.is_sign_positive() && !value.is_nan());
325
326    let f = value % 1.;
327    if f < 0.5 {
328        value - f
329    } else {
330        value - f + 1.
331    }
332}