Skip to main content

time/parsing/
iso8601.rs

1//! Parse parts of an ISO 8601-formatted value.
2
3use crate::convert::*;
4use crate::error;
5use crate::error::ParseFromDescription::{InvalidComponent, InvalidLiteral};
6use crate::format_description::well_known::Iso8601;
7use crate::format_description::well_known::iso8601::EncodedConfig;
8use crate::internal_macros::try_likely_ok;
9use crate::parsing::combinator::rfc::iso8601::{
10    ExtendedKind, day, dayk, dayo, float, hour, min, month, week, year,
11};
12use crate::parsing::combinator::{Sign, ascii_char, sign};
13use crate::parsing::{Parsed, ParsedItem};
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            if date_is_present {
126                input =
127                    try_likely_ok!(ascii_char::<b'T'>(input).ok_or(InvalidLiteral)).into_inner();
128            }
129
130            let ParsedItem(mut input, hour) =
131                try_likely_ok!(float(input).ok_or(InvalidComponent("hour")));
132            match hour {
133                (hour, None) => {
134                    try_likely_ok!(parsed.set_hour_24(hour).ok_or(InvalidComponent("hour")))
135                }
136                (hour, Some(fractional_part)) => {
137                    *parsed = try_likely_ok!(
138                        try_likely_ok!(
139                            try_likely_ok!(
140                                try_likely_ok!(
141                                    parsed.with_hour_24(hour).ok_or(InvalidComponent("hour"))
142                                )
143                                .with_minute((fractional_part * Second::per_t::<f64>(Minute)) as u8)
144                                .ok_or(InvalidComponent("minute"))
145                            )
146                            .with_second(
147                                (fractional_part * Second::per_t::<f64>(Hour)
148                                    % Minute::per_t::<f64>(Hour))
149                                    as u8,
150                            )
151                            .ok_or(InvalidComponent("second"))
152                        )
153                        .with_subsecond(
154                            (fractional_part * Nanosecond::per_t::<f64>(Hour)
155                                % Nanosecond::per_t::<f64>(Second))
156                                as u32,
157                        )
158                        .ok_or(InvalidComponent("subsecond"))
159                    );
160                    return Ok(input);
161                }
162            };
163
164            if let Some(ParsedItem(new_input, ())) = ascii_char::<b':'>(input) {
165                try_likely_ok!(
166                    extended_kind
167                        .coerce_extended()
168                        .ok_or(InvalidComponent("minute"))
169                );
170                input = new_input;
171            };
172
173            let mut input = match float(input) {
174                Some(ParsedItem(input, (minute, None))) => {
175                    extended_kind.coerce_basic();
176                    try_likely_ok!(parsed.set_minute(minute).ok_or(InvalidComponent("minute")));
177                    input
178                }
179                Some(ParsedItem(input, (minute, Some(fractional_part)))) => {
180                    // `None` is valid behavior, so don't error if this fails.
181                    extended_kind.coerce_basic();
182                    *parsed = try_likely_ok!(
183                        try_likely_ok!(
184                            try_likely_ok!(
185                                parsed.with_minute(minute).ok_or(InvalidComponent("minute"))
186                            )
187                            .with_second((fractional_part * Second::per_t::<f64>(Minute)) as u8)
188                            .ok_or(InvalidComponent("second"))
189                        )
190                        .with_subsecond(
191                            (fractional_part * Nanosecond::per_t::<f64>(Minute)
192                                % Nanosecond::per_t::<f64>(Second))
193                                as u32,
194                        )
195                        .ok_or(InvalidComponent("subsecond"))
196                    );
197                    return Ok(input);
198                }
199                // colon was present, so minutes are required
200                None if extended_kind.is_extended() => {
201                    return Err(error::Parse::ParseFromDescription(InvalidComponent(
202                        "minute",
203                    )));
204                }
205                None => {
206                    // Missing components are assumed to be zero.
207                    *parsed = try_likely_ok!(
208                        try_likely_ok!(
209                            try_likely_ok!(parsed.with_minute(0).ok_or(InvalidComponent("minute")))
210                                .with_second(0)
211                                .ok_or(InvalidComponent("second"))
212                        )
213                        .with_subsecond(0)
214                        .ok_or(InvalidComponent("subsecond"))
215                    );
216                    return Ok(input);
217                }
218            };
219
220            if extended_kind.is_extended() {
221                match ascii_char::<b':'>(input) {
222                    Some(ParsedItem(new_input, ())) => input = new_input,
223                    None => {
224                        *parsed = try_likely_ok!(
225                            try_likely_ok!(parsed.with_second(0).ok_or(InvalidComponent("second")))
226                                .with_subsecond(0)
227                                .ok_or(InvalidComponent("subsecond"))
228                        );
229                        return Ok(input);
230                    }
231                }
232            }
233
234            let (input, second, subsecond) = match float(input) {
235                Some(ParsedItem(input, (second, None))) => (input, second, 0),
236                Some(ParsedItem(input, (second, Some(fractional_part)))) => (
237                    input,
238                    second,
239                    round(fractional_part * Nanosecond::per_t::<f64>(Second)) as u32,
240                ),
241                None if extended_kind.is_extended() => {
242                    return Err(error::Parse::ParseFromDescription(InvalidComponent(
243                        "second",
244                    )));
245                }
246                // Missing components are assumed to be zero.
247                None => (input, 0, 0),
248            };
249            *parsed = try_likely_ok!(
250                try_likely_ok!(parsed.with_second(second).ok_or(InvalidComponent("second")))
251                    .with_subsecond(subsecond)
252                    .ok_or(InvalidComponent("subsecond"))
253            );
254
255            Ok(input)
256        }
257    }
258
259    // Basic: [±][hour][min] or ["Z"]
260    // Extended: [±][hour][":"][min] or ["Z"]
261    // Reduced precision: [±][hour] or ["Z"]
262    /// Parse a UTC offset in the basic or extended format. Reduced precision is supported.
263    pub(crate) fn parse_offset<'a>(
264        parsed: &'a mut Parsed,
265        extended_kind: &'a mut ExtendedKind,
266    ) -> impl FnMut(&[u8]) -> Result<&[u8], error::Parse> + use<'a, CONFIG> {
267        move |input| {
268            if let Some(ParsedItem(input, ())) = ascii_char::<b'Z'>(input) {
269                *parsed = try_likely_ok!(
270                    try_likely_ok!(
271                        try_likely_ok!(
272                            parsed
273                                .with_offset_hour(0)
274                                .ok_or(InvalidComponent("offset hour"))
275                        )
276                        .with_offset_minute_signed(0)
277                        .ok_or(InvalidComponent("offset minute"))
278                    )
279                    .with_offset_second_signed(0)
280                    .ok_or(InvalidComponent("offset second"))
281                );
282                return Ok(input);
283            }
284
285            let ParsedItem(input, sign) =
286                try_likely_ok!(sign(input).ok_or(InvalidComponent("offset hour")));
287            let mut input = try_likely_ok!(
288                hour(input)
289                    .and_then(|parsed_item| {
290                        parsed_item.consume_value(|hour| {
291                            parsed.set_offset_hour(match sign {
292                                Sign::Negative => -hour.cast_signed(),
293                                Sign::Positive => hour.cast_signed(),
294                            })
295                        })
296                    })
297                    .ok_or(InvalidComponent("offset hour"))
298            );
299
300            if extended_kind.maybe_extended()
301                && let Some(ParsedItem(new_input, ())) = ascii_char::<b':'>(input)
302            {
303                try_likely_ok!(
304                    extended_kind
305                        .coerce_extended()
306                        .ok_or(InvalidComponent("offset minute"))
307                );
308                input = new_input;
309            };
310
311            match min(input) {
312                Some(ParsedItem(new_input, min)) => {
313                    input = new_input;
314                    try_likely_ok!(
315                        parsed
316                            .set_offset_minute_signed(match sign {
317                                Sign::Negative => -min.cast_signed(),
318                                Sign::Positive => min.cast_signed(),
319                            })
320                            .ok_or(InvalidComponent("offset minute"))
321                    );
322                }
323                None => {
324                    // Omitted offset minute is assumed to be zero.
325                    parsed.set_offset_minute_signed(0);
326                }
327            }
328
329            // If `:` was present, the format has already been set to extended. As such, this call
330            // will do nothing in that case. If there wasn't `:` but minutes were
331            // present, we know it's the basic format. Do not use `?` on the call, as
332            // returning `None` is valid behavior.
333            extended_kind.coerce_basic();
334
335            Ok(input)
336        }
337    }
338}
339
340/// Round wrapper that uses hardware implementation if `std` is available, falling back to manual
341/// implementation for `no_std`
342#[inline]
343fn round(value: f64) -> f64 {
344    #[cfg(feature = "std")]
345    {
346        value.round()
347    }
348    #[cfg(not(feature = "std"))]
349    {
350        debug_assert!(value.is_sign_positive() && !value.is_nan());
351
352        let f = value % 1.;
353        if f < 0.5 { value - f } else { value - f + 1. }
354    }
355}