toml_edit/parser/
datetime.rs

1use std::ops::RangeInclusive;
2
3use crate::parser::errors::CustomError;
4use crate::parser::prelude::*;
5use crate::parser::trivia::from_utf8_unchecked;
6
7use toml_datetime::*;
8use winnow::combinator::alt;
9use winnow::combinator::cut_err;
10use winnow::combinator::opt;
11use winnow::combinator::preceded;
12use winnow::token::one_of;
13use winnow::token::take_while;
14use winnow::trace::trace;
15
16// ;; Date and Time (as defined in RFC 3339)
17
18// date-time = offset-date-time / local-date-time / local-date / local-time
19// offset-date-time = full-date time-delim full-time
20// local-date-time = full-date time-delim partial-time
21// local-date = full-date
22// local-time = partial-time
23// full-time = partial-time time-offset
24pub(crate) fn date_time(input: &mut Input<'_>) -> PResult<Datetime> {
25    trace(
26        "date-time",
27        alt((
28            (full_date, opt((time_delim, partial_time, opt(time_offset))))
29                .map(|(date, opt)| {
30                    match opt {
31                        // Offset Date-Time
32                        Some((_, time, offset)) => Datetime {
33                            date: Some(date),
34                            time: Some(time),
35                            offset,
36                        },
37                        // Local Date
38                        None => Datetime {
39                            date: Some(date),
40                            time: None,
41                            offset: None,
42                        },
43                    }
44                })
45                .context(StrContext::Label("date-time")),
46            partial_time
47                .map(|t| t.into())
48                .context(StrContext::Label("time")),
49        )),
50    )
51    .parse_next(input)
52}
53
54// full-date      = date-fullyear "-" date-month "-" date-mday
55pub(crate) fn full_date(input: &mut Input<'_>) -> PResult<Date> {
56    trace(
57        "full-date",
58        (date_fullyear, b'-', cut_err((date_month, b'-', date_mday)))
59            .map(|(year, _, (month, _, day))| Date { year, month, day }),
60    )
61    .parse_next(input)
62}
63
64// partial-time   = time-hour ":" time-minute ":" time-second [time-secfrac]
65pub(crate) fn partial_time(input: &mut Input<'_>) -> PResult<Time> {
66    trace(
67        "partial-time",
68        (
69            time_hour,
70            b':',
71            cut_err((time_minute, b':', time_second, opt(time_secfrac))),
72        )
73            .map(|(hour, _, (minute, _, second, nanosecond))| Time {
74                hour,
75                minute,
76                second,
77                nanosecond: nanosecond.unwrap_or_default(),
78            }),
79    )
80    .parse_next(input)
81}
82
83// time-offset    = "Z" / time-numoffset
84// time-numoffset = ( "+" / "-" ) time-hour ":" time-minute
85pub(crate) fn time_offset(input: &mut Input<'_>) -> PResult<Offset> {
86    trace(
87        "time-offset",
88        alt((
89            one_of((b'Z', b'z')).value(Offset::Z),
90            (
91                one_of((b'+', b'-')),
92                cut_err((time_hour, b':', time_minute)),
93            )
94                .map(|(sign, (hours, _, minutes))| {
95                    let sign = match sign {
96                        b'+' => 1,
97                        b'-' => -1,
98                        _ => unreachable!("Parser prevents this"),
99                    };
100                    sign * (hours as i16 * 60 + minutes as i16)
101                })
102                .verify(|minutes| ((-24 * 60)..=(24 * 60)).contains(minutes))
103                .map(|minutes| Offset::Custom { minutes }),
104        ))
105        .context(StrContext::Label("time offset")),
106    )
107    .parse_next(input)
108}
109
110// date-fullyear  = 4DIGIT
111pub(crate) fn date_fullyear(input: &mut Input<'_>) -> PResult<u16> {
112    unsigned_digits::<4, 4>
113        .map(|s: &str| s.parse::<u16>().expect("4DIGIT should match u8"))
114        .parse_next(input)
115}
116
117// date-month     = 2DIGIT  ; 01-12
118pub(crate) fn date_month(input: &mut Input<'_>) -> PResult<u8> {
119    unsigned_digits::<2, 2>
120        .try_map(|s: &str| {
121            let d = s.parse::<u8>().expect("2DIGIT should match u8");
122            if (1..=12).contains(&d) {
123                Ok(d)
124            } else {
125                Err(CustomError::OutOfRange)
126            }
127        })
128        .parse_next(input)
129}
130
131// date-mday      = 2DIGIT  ; 01-28, 01-29, 01-30, 01-31 based on month/year
132pub(crate) fn date_mday(input: &mut Input<'_>) -> PResult<u8> {
133    unsigned_digits::<2, 2>
134        .try_map(|s: &str| {
135            let d = s.parse::<u8>().expect("2DIGIT should match u8");
136            if (1..=31).contains(&d) {
137                Ok(d)
138            } else {
139                Err(CustomError::OutOfRange)
140            }
141        })
142        .parse_next(input)
143}
144
145// time-delim     = "T" / %x20 ; T, t, or space
146pub(crate) fn time_delim(input: &mut Input<'_>) -> PResult<u8> {
147    one_of(TIME_DELIM).parse_next(input)
148}
149
150const TIME_DELIM: (u8, u8, u8) = (b'T', b't', b' ');
151
152// time-hour      = 2DIGIT  ; 00-23
153pub(crate) fn time_hour(input: &mut Input<'_>) -> PResult<u8> {
154    unsigned_digits::<2, 2>
155        .try_map(|s: &str| {
156            let d = s.parse::<u8>().expect("2DIGIT should match u8");
157            if (0..=23).contains(&d) {
158                Ok(d)
159            } else {
160                Err(CustomError::OutOfRange)
161            }
162        })
163        .parse_next(input)
164}
165
166// time-minute    = 2DIGIT  ; 00-59
167pub(crate) fn time_minute(input: &mut Input<'_>) -> PResult<u8> {
168    unsigned_digits::<2, 2>
169        .try_map(|s: &str| {
170            let d = s.parse::<u8>().expect("2DIGIT should match u8");
171            if (0..=59).contains(&d) {
172                Ok(d)
173            } else {
174                Err(CustomError::OutOfRange)
175            }
176        })
177        .parse_next(input)
178}
179
180// time-second    = 2DIGIT  ; 00-58, 00-59, 00-60 based on leap second rules
181pub(crate) fn time_second(input: &mut Input<'_>) -> PResult<u8> {
182    unsigned_digits::<2, 2>
183        .try_map(|s: &str| {
184            let d = s.parse::<u8>().expect("2DIGIT should match u8");
185            if (0..=60).contains(&d) {
186                Ok(d)
187            } else {
188                Err(CustomError::OutOfRange)
189            }
190        })
191        .parse_next(input)
192}
193
194// time-secfrac   = "." 1*DIGIT
195pub(crate) fn time_secfrac(input: &mut Input<'_>) -> PResult<u32> {
196    static SCALE: [u32; 10] = [
197        0,
198        100_000_000,
199        10_000_000,
200        1_000_000,
201        100_000,
202        10_000,
203        1_000,
204        100,
205        10,
206        1,
207    ];
208    const INF: usize = usize::MAX;
209    preceded(b'.', unsigned_digits::<1, INF>)
210        .try_map(|mut repr: &str| -> Result<u32, CustomError> {
211            let max_digits = SCALE.len() - 1;
212            if max_digits < repr.len() {
213                // Millisecond precision is required. Further precision of fractional seconds is
214                // implementation-specific. If the value contains greater precision than the
215                // implementation can support, the additional precision must be truncated, not rounded.
216                repr = &repr[0..max_digits];
217            }
218
219            let v = repr.parse::<u32>().map_err(|_| CustomError::OutOfRange)?;
220            let num_digits = repr.len();
221
222            // scale the number accordingly.
223            let scale = SCALE.get(num_digits).ok_or(CustomError::OutOfRange)?;
224            let v = v.checked_mul(*scale).ok_or(CustomError::OutOfRange)?;
225            Ok(v)
226        })
227        .parse_next(input)
228}
229
230pub(crate) fn unsigned_digits<'i, const MIN: usize, const MAX: usize>(
231    input: &mut Input<'i>,
232) -> PResult<&'i str> {
233    take_while(MIN..=MAX, DIGIT)
234        .map(|b: &[u8]| unsafe { from_utf8_unchecked(b, "`is_ascii_digit` filters out on-ASCII") })
235        .parse_next(input)
236}
237
238// DIGIT = %x30-39 ; 0-9
239const DIGIT: RangeInclusive<u8> = b'0'..=b'9';
240
241#[cfg(test)]
242mod test {
243    use super::*;
244
245    #[test]
246    fn offset_date_time() {
247        let inputs = [
248            (
249                "1979-05-27T07:32:00Z",
250                Datetime {
251                    date: Some(Date {
252                        year: 1979,
253                        month: 5,
254                        day: 27,
255                    }),
256                    time: Some(Time {
257                        hour: 7,
258                        minute: 32,
259                        second: 0,
260                        nanosecond: 0,
261                    }),
262                    offset: Some(Offset::Z),
263                },
264            ),
265            (
266                "1979-05-27T00:32:00-07:00",
267                Datetime {
268                    date: Some(Date {
269                        year: 1979,
270                        month: 5,
271                        day: 27,
272                    }),
273                    time: Some(Time {
274                        hour: 0,
275                        minute: 32,
276                        second: 0,
277                        nanosecond: 0,
278                    }),
279                    offset: Some(Offset::Custom { minutes: -7 * 60 }),
280                },
281            ),
282            (
283                "1979-05-27T00:32:00-00:36",
284                Datetime {
285                    date: Some(Date {
286                        year: 1979,
287                        month: 5,
288                        day: 27,
289                    }),
290                    time: Some(Time {
291                        hour: 0,
292                        minute: 32,
293                        second: 0,
294                        nanosecond: 0,
295                    }),
296                    offset: Some(Offset::Custom { minutes: -36 }),
297                },
298            ),
299            (
300                "1979-05-27T00:32:00.999999",
301                Datetime {
302                    date: Some(Date {
303                        year: 1979,
304                        month: 5,
305                        day: 27,
306                    }),
307                    time: Some(Time {
308                        hour: 0,
309                        minute: 32,
310                        second: 0,
311                        nanosecond: 999999000,
312                    }),
313                    offset: None,
314                },
315            ),
316        ];
317        for (input, expected) in inputs {
318            dbg!(input);
319            let actual = date_time.parse(new_input(input)).unwrap();
320            assert_eq!(expected, actual);
321        }
322    }
323
324    #[test]
325    fn local_date_time() {
326        let inputs = [
327            (
328                "1979-05-27T07:32:00",
329                Datetime {
330                    date: Some(Date {
331                        year: 1979,
332                        month: 5,
333                        day: 27,
334                    }),
335                    time: Some(Time {
336                        hour: 7,
337                        minute: 32,
338                        second: 0,
339                        nanosecond: 0,
340                    }),
341                    offset: None,
342                },
343            ),
344            (
345                "1979-05-27T00:32:00.999999",
346                Datetime {
347                    date: Some(Date {
348                        year: 1979,
349                        month: 5,
350                        day: 27,
351                    }),
352                    time: Some(Time {
353                        hour: 0,
354                        minute: 32,
355                        second: 0,
356                        nanosecond: 999999000,
357                    }),
358                    offset: None,
359                },
360            ),
361        ];
362        for (input, expected) in inputs {
363            dbg!(input);
364            let actual = date_time.parse(new_input(input)).unwrap();
365            assert_eq!(expected, actual);
366        }
367    }
368
369    #[test]
370    fn local_date() {
371        let inputs = [
372            (
373                "1979-05-27",
374                Datetime {
375                    date: Some(Date {
376                        year: 1979,
377                        month: 5,
378                        day: 27,
379                    }),
380                    time: None,
381                    offset: None,
382                },
383            ),
384            (
385                "2017-07-20",
386                Datetime {
387                    date: Some(Date {
388                        year: 2017,
389                        month: 7,
390                        day: 20,
391                    }),
392                    time: None,
393                    offset: None,
394                },
395            ),
396        ];
397        for (input, expected) in inputs {
398            dbg!(input);
399            let actual = date_time.parse(new_input(input)).unwrap();
400            assert_eq!(expected, actual);
401        }
402    }
403
404    #[test]
405    fn local_time() {
406        let inputs = [
407            (
408                "07:32:00",
409                Datetime {
410                    date: None,
411                    time: Some(Time {
412                        hour: 7,
413                        minute: 32,
414                        second: 0,
415                        nanosecond: 0,
416                    }),
417                    offset: None,
418                },
419            ),
420            (
421                "00:32:00.999999",
422                Datetime {
423                    date: None,
424                    time: Some(Time {
425                        hour: 0,
426                        minute: 32,
427                        second: 0,
428                        nanosecond: 999999000,
429                    }),
430                    offset: None,
431                },
432            ),
433        ];
434        for (input, expected) in inputs {
435            dbg!(input);
436            let actual = date_time.parse(new_input(input)).unwrap();
437            assert_eq!(expected, actual);
438        }
439    }
440
441    #[test]
442    fn time_fraction_truncated() {
443        let input = "1987-07-05T17:45:00.123456789012345Z";
444        date_time.parse(new_input(input)).unwrap();
445    }
446}