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