Coverage Report

Created: 2025-07-11 06:43

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