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 nom8::branch::alt;
8use nom8::bytes::one_of;
9use nom8::bytes::take_while_m_n;
10use nom8::combinator::cut;
11use nom8::combinator::opt;
12use nom8::sequence::preceded;
13use toml_datetime::*;
14
15// ;; Date and Time (as defined in RFC 3339)
16
17// date-time = offset-date-time / local-date-time / local-date / local-time
18// offset-date-time = full-date time-delim full-time
19// local-date-time = full-date time-delim partial-time
20// local-date = full-date
21// local-time = partial-time
22// full-time = partial-time time-offset
23pub(crate) fn date_time(input: Input<'_>) -> IResult<Input<'_>, Datetime, ParserError<'_>> {
24    alt((
25        (full_date, opt((time_delim, partial_time, opt(time_offset))))
26            .map(|(date, opt)| {
27                match opt {
28                    // Offset Date-Time
29                    Some((_, time, offset)) => Datetime {
30                        date: Some(date),
31                        time: Some(time),
32                        offset,
33                    },
34                    // Local Date
35                    None => Datetime {
36                        date: Some(date),
37                        time: None,
38                        offset: None,
39                    },
40                }
41            })
42            .context(Context::Expression("date-time")),
43        partial_time
44            .map(|t| t.into())
45            .context(Context::Expression("time")),
46    ))
47    .parse(input)
48}
49
50// full-date      = date-fullyear "-" date-month "-" date-mday
51pub(crate) fn full_date(input: Input<'_>) -> IResult<Input<'_>, Date, ParserError<'_>> {
52    (date_fullyear, b'-', cut((date_month, b'-', date_mday)))
53        .map(|(year, _, (month, _, day))| Date { year, month, day })
54        .parse(input)
55}
56
57// partial-time   = time-hour ":" time-minute ":" time-second [time-secfrac]
58pub(crate) fn partial_time(input: Input<'_>) -> IResult<Input<'_>, Time, ParserError<'_>> {
59    (
60        time_hour,
61        b':',
62        cut((time_minute, b':', time_second, opt(time_secfrac))),
63    )
64        .map(|(hour, _, (minute, _, second, nanosecond))| Time {
65            hour,
66            minute,
67            second,
68            nanosecond: nanosecond.unwrap_or_default(),
69        })
70        .parse(input)
71}
72
73// time-offset    = "Z" / time-numoffset
74// time-numoffset = ( "+" / "-" ) time-hour ":" time-minute
75pub(crate) fn time_offset(input: Input<'_>) -> IResult<Input<'_>, Offset, ParserError<'_>> {
76    alt((
77        one_of((b'Z', b'z')).value(Offset::Z),
78        (one_of((b'+', b'-')), cut((time_hour, b':', time_minute))).map(
79            |(sign, (hours, _, minutes))| {
80                let hours = hours as i8;
81                let hours = match sign {
82                    b'+' => hours,
83                    b'-' => -hours,
84                    _ => unreachable!("Parser prevents this"),
85                };
86                Offset::Custom { hours, minutes }
87            },
88        ),
89    ))
90    .context(Context::Expression("time offset"))
91    .parse(input)
92}
93
94// date-fullyear  = 4DIGIT
95pub(crate) fn date_fullyear(input: Input<'_>) -> IResult<Input<'_>, u16, ParserError<'_>> {
96    unsigned_digits::<4, 4>
97        .map(|s: &str| s.parse::<u16>().expect("4DIGIT should match u8"))
98        .parse(input)
99}
100
101// date-month     = 2DIGIT  ; 01-12
102pub(crate) fn date_month(input: Input<'_>) -> IResult<Input<'_>, u8, ParserError<'_>> {
103    unsigned_digits::<2, 2>
104        .map_res(|s: &str| {
105            let d = s.parse::<u8>().expect("2DIGIT should match u8");
106            if (1..=12).contains(&d) {
107                Ok(d)
108            } else {
109                Err(CustomError::OutOfRange)
110            }
111        })
112        .parse(input)
113}
114
115// date-mday      = 2DIGIT  ; 01-28, 01-29, 01-30, 01-31 based on month/year
116pub(crate) fn date_mday(input: Input<'_>) -> IResult<Input<'_>, u8, ParserError<'_>> {
117    unsigned_digits::<2, 2>
118        .map_res(|s: &str| {
119            let d = s.parse::<u8>().expect("2DIGIT should match u8");
120            if (1..=31).contains(&d) {
121                Ok(d)
122            } else {
123                Err(CustomError::OutOfRange)
124            }
125        })
126        .parse(input)
127}
128
129// time-delim     = "T" / %x20 ; T, t, or space
130pub(crate) fn time_delim(input: Input<'_>) -> IResult<Input<'_>, u8, ParserError<'_>> {
131    one_of(TIME_DELIM).parse(input)
132}
133
134const TIME_DELIM: (u8, u8, u8) = (b'T', b't', b' ');
135
136// time-hour      = 2DIGIT  ; 00-23
137pub(crate) fn time_hour(input: Input<'_>) -> IResult<Input<'_>, u8, ParserError<'_>> {
138    unsigned_digits::<2, 2>
139        .map_res(|s: &str| {
140            let d = s.parse::<u8>().expect("2DIGIT should match u8");
141            if (0..=23).contains(&d) {
142                Ok(d)
143            } else {
144                Err(CustomError::OutOfRange)
145            }
146        })
147        .parse(input)
148}
149
150// time-minute    = 2DIGIT  ; 00-59
151pub(crate) fn time_minute(input: Input<'_>) -> IResult<Input<'_>, u8, ParserError<'_>> {
152    unsigned_digits::<2, 2>
153        .map_res(|s: &str| {
154            let d = s.parse::<u8>().expect("2DIGIT should match u8");
155            if (0..=59).contains(&d) {
156                Ok(d)
157            } else {
158                Err(CustomError::OutOfRange)
159            }
160        })
161        .parse(input)
162}
163
164// time-second    = 2DIGIT  ; 00-58, 00-59, 00-60 based on leap second rules
165pub(crate) fn time_second(input: Input<'_>) -> IResult<Input<'_>, u8, ParserError<'_>> {
166    unsigned_digits::<2, 2>
167        .map_res(|s: &str| {
168            let d = s.parse::<u8>().expect("2DIGIT should match u8");
169            if (0..=60).contains(&d) {
170                Ok(d)
171            } else {
172                Err(CustomError::OutOfRange)
173            }
174        })
175        .parse(input)
176}
177
178// time-secfrac   = "." 1*DIGIT
179pub(crate) fn time_secfrac(input: Input<'_>) -> IResult<Input<'_>, u32, ParserError<'_>> {
180    static SCALE: [u32; 10] = [
181        0,
182        100_000_000,
183        10_000_000,
184        1_000_000,
185        100_000,
186        10_000,
187        1_000,
188        100,
189        10,
190        1,
191    ];
192    const INF: usize = usize::MAX;
193    preceded(b'.', unsigned_digits::<1, INF>)
194        .map_res(|mut repr: &str| -> Result<u32, CustomError> {
195            let max_digits = SCALE.len() - 1;
196            if max_digits < repr.len() {
197                // Millisecond precision is required. Further precision of fractional seconds is
198                // implementation-specific. If the value contains greater precision than the
199                // implementation can support, the additional precision must be truncated, not rounded.
200                repr = &repr[0..max_digits];
201            }
202
203            let v = repr.parse::<u32>().map_err(|_| CustomError::OutOfRange)?;
204            let num_digits = repr.len();
205
206            // scale the number accordingly.
207            let scale = SCALE.get(num_digits).ok_or(CustomError::OutOfRange)?;
208            let v = v.checked_mul(*scale).ok_or(CustomError::OutOfRange)?;
209            Ok(v)
210        })
211        .parse(input)
212}
213
214pub(crate) fn unsigned_digits<const MIN: usize, const MAX: usize>(
215    input: Input<'_>,
216) -> IResult<Input<'_>, &str, ParserError<'_>> {
217    take_while_m_n(MIN, MAX, DIGIT)
218        .map(|b: &[u8]| unsafe { from_utf8_unchecked(b, "`is_ascii_digit` filters out on-ASCII") })
219        .parse(input)
220}
221
222// DIGIT = %x30-39 ; 0-9
223const DIGIT: RangeInclusive<u8> = b'0'..=b'9';
224
225#[cfg(test)]
226mod test {
227    use super::*;
228
229    #[test]
230    fn offset_date_time() {
231        let inputs = [
232            "1979-05-27T07:32:00Z",
233            "1979-05-27T00:32:00-07:00",
234            "1979-05-27T00:32:00.999999-07:00",
235        ];
236        for input in inputs {
237            dbg!(input);
238            date_time.parse(new_input(input)).finish().unwrap();
239        }
240    }
241
242    #[test]
243    fn local_date_time() {
244        let inputs = ["1979-05-27T07:32:00", "1979-05-27T00:32:00.999999"];
245        for input in inputs {
246            dbg!(input);
247            date_time.parse(new_input(input)).finish().unwrap();
248        }
249    }
250
251    #[test]
252    fn local_date() {
253        let inputs = ["1979-05-27", "2017-07-20"];
254        for input in inputs {
255            dbg!(input);
256            date_time.parse(new_input(input)).finish().unwrap();
257        }
258    }
259
260    #[test]
261    fn local_time() {
262        let inputs = ["07:32:00", "00:32:00.999999"];
263        for input in inputs {
264            dbg!(input);
265            date_time.parse(new_input(input)).finish().unwrap();
266        }
267    }
268
269    #[test]
270    fn time_fraction_truncated() {
271        let input = "1987-07-05T17:45:00.123456789012345Z";
272        date_time.parse(new_input(input)).finish().unwrap();
273    }
274}