Skip to main content

time/parsing/
iso8601.rs

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