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 traceimpl Parser, …>(
57 name:"full-date",
58 (date_fullyear, b'-', cut_err((date_month, b'-', date_mday)))
59 .map(|(year: u16, _, (month: u8, _, day: u8))| 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 traceimpl Parser, …>(
67 name:"partial-time",
68 (
69 time_hour,
70 b':',
71 cut_err((time_minute, b':', time_second, opt(time_secfrac))),
72 )
73 .map(|(hour: u8, _, (minute: u8, _, second: u8, nanosecond: Option))| 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 traceimpl Parser, …>(
87 name:"time-offset",
88 parser:alt((
89 one_of((b'Z', b'z')).value(val: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: &i16| ((-24 * 60)..=(24 * 60)).contains(item:minutes))
103 .map(|minutes: i16| 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_digitsMap(…) -> …, …, …, …, …, …>::<4, 4>
113 .map(|s: &str| s.parse::<u16>().expect(msg:"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_digitsTryMap(…) -> …, …, …, …, …, …, …>::<2, 2>
120 .try_map(|s: &str| {
121 let d: u8 = s.parse::<u8>().expect(msg:"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_digitsTryMap(…) -> …, …, …, …, …, …, …>::<2, 2>
134 .try_map(|s: &str| {
135 let d: u8 = s.parse::<u8>().expect(msg:"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_digitsTryMap(…) -> …, …, …, …, …, …, …>::<2, 2>
155 .try_map(|s: &str| {
156 let d: u8 = s.parse::<u8>().expect(msg:"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_digitsTryMap(…) -> …, …, …, …, …, …, …>::<2, 2>
169 .try_map(|s: &str| {
170 let d: u8 = s.parse::<u8>().expect(msg:"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_digitsTryMap(…) -> …, …, …, …, …, …, …>::<2, 2>
183 .try_map(|s: &str| {
184 let d: u8 = s.parse::<u8>().expect(msg:"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_whileMap, …>, …, …, …, …, …>(MIN..=MAX, DIGIT)
234 .map(|b: &[u8]| unsafe { from_utf8_unchecked(bytes:b, safety_justification:"`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}
447