toml_edit/parser/
datetime.rs

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