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::stream::Stream as _;
13use winnow::token::one_of;
14use winnow::token::take_while;
15use winnow::trace::trace;
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(name:"full-date", parser: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 traceimpl Parser, …>(
91 name:"partial-time",
92 (
93 time_hour,
94 b':',
95 cut_err((time_minute, b':', time_second, opt(time_secfrac))),
96 )
97 .map(|(hour: u8, _, (minute: u8, _, second: u8, nanosecond: Option))| 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 traceimpl Parser, …>(
111 name:"time-offset",
112 parser:alt((
113 one_of((b'Z', b'z')).value(val: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: &i16| ((-24 * 60)..=(24 * 60)).contains(item:minutes))
127 .map(|minutes: i16| 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_digitsMap(…) -> …, …, …, …, …, …>::<4, 4>
137 .map(|s: &str| s.parse::<u16>().expect(msg:"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_digitsTryMap(…) -> …, …, …, …, …, …, …>::<2, 2>
144 .try_map(|s: &str| {
145 let d: u8 = s.parse::<u8>().expect(msg:"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_digitsTryMap(…) -> …, …, …, …, …, …, …>::<2, 2>
158 .try_map(|s: &str| {
159 let d: u8 = s.parse::<u8>().expect(msg:"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_digitsTryMap(…) -> …, …, …, …, …, …, …>::<2, 2>
179 .try_map(|s: &str| {
180 let d: u8 = s.parse::<u8>().expect(msg:"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_digitsTryMap(…) -> …, …, …, …, …, …, …>::<2, 2>
193 .try_map(|s: &str| {
194 let d: u8 = s.parse::<u8>().expect(msg:"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_digitsTryMap(…) -> …, …, …, …, …, …, …>::<2, 2>
207 .try_map(|s: &str| {
208 let d: u8 = s.parse::<u8>().expect(msg:"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_whileMap, …>, …, …, …, …, …>(MIN..=MAX, DIGIT)
258 .map(|b: &[u8]| unsafe { from_utf8_unchecked(bytes:b, safety_justification:"`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)]
266mod test {
267 use super::*;
268
269 #[test]
270 fn offset_date_time() {
271 let inputs = [
272 (
273 "1979-05-27T07:32:00Z",
274 Datetime {
275 date: Some(Date {
276 year: 1979,
277 month: 5,
278 day: 27,
279 }),
280 time: Some(Time {
281 hour: 7,
282 minute: 32,
283 second: 0,
284 nanosecond: 0,
285 }),
286 offset: Some(Offset::Z),
287 },
288 ),
289 (
290 "1979-05-27T00:32:00-07:00",
291 Datetime {
292 date: Some(Date {
293 year: 1979,
294 month: 5,
295 day: 27,
296 }),
297 time: Some(Time {
298 hour: 0,
299 minute: 32,
300 second: 0,
301 nanosecond: 0,
302 }),
303 offset: Some(Offset::Custom { minutes: -7 * 60 }),
304 },
305 ),
306 (
307 "1979-05-27T00:32:00-00:36",
308 Datetime {
309 date: Some(Date {
310 year: 1979,
311 month: 5,
312 day: 27,
313 }),
314 time: Some(Time {
315 hour: 0,
316 minute: 32,
317 second: 0,
318 nanosecond: 0,
319 }),
320 offset: Some(Offset::Custom { minutes: -36 }),
321 },
322 ),
323 (
324 "1979-05-27T00:32:00.999999",
325 Datetime {
326 date: Some(Date {
327 year: 1979,
328 month: 5,
329 day: 27,
330 }),
331 time: Some(Time {
332 hour: 0,
333 minute: 32,
334 second: 0,
335 nanosecond: 999999000,
336 }),
337 offset: None,
338 },
339 ),
340 ];
341 for (input, expected) in inputs {
342 dbg!(input);
343 let actual = date_time.parse(new_input(input)).unwrap();
344 assert_eq!(expected, actual);
345 }
346 }
347
348 #[test]
349 fn local_date_time() {
350 let inputs = [
351 (
352 "1979-05-27T07:32:00",
353 Datetime {
354 date: Some(Date {
355 year: 1979,
356 month: 5,
357 day: 27,
358 }),
359 time: Some(Time {
360 hour: 7,
361 minute: 32,
362 second: 0,
363 nanosecond: 0,
364 }),
365 offset: None,
366 },
367 ),
368 (
369 "1979-05-27T00:32:00.999999",
370 Datetime {
371 date: Some(Date {
372 year: 1979,
373 month: 5,
374 day: 27,
375 }),
376 time: Some(Time {
377 hour: 0,
378 minute: 32,
379 second: 0,
380 nanosecond: 999999000,
381 }),
382 offset: None,
383 },
384 ),
385 ];
386 for (input, expected) in inputs {
387 dbg!(input);
388 let actual = date_time.parse(new_input(input)).unwrap();
389 assert_eq!(expected, actual);
390 }
391 }
392
393 #[test]
394 fn local_date() {
395 let inputs = [
396 (
397 "1979-05-27",
398 Datetime {
399 date: Some(Date {
400 year: 1979,
401 month: 5,
402 day: 27,
403 }),
404 time: None,
405 offset: None,
406 },
407 ),
408 (
409 "2017-07-20",
410 Datetime {
411 date: Some(Date {
412 year: 2017,
413 month: 7,
414 day: 20,
415 }),
416 time: None,
417 offset: None,
418 },
419 ),
420 ];
421 for (input, expected) in inputs {
422 dbg!(input);
423 let actual = date_time.parse(new_input(input)).unwrap();
424 assert_eq!(expected, actual);
425 }
426 }
427
428 #[test]
429 fn local_time() {
430 let inputs = [
431 (
432 "07:32:00",
433 Datetime {
434 date: None,
435 time: Some(Time {
436 hour: 7,
437 minute: 32,
438 second: 0,
439 nanosecond: 0,
440 }),
441 offset: None,
442 },
443 ),
444 (
445 "00:32:00.999999",
446 Datetime {
447 date: None,
448 time: Some(Time {
449 hour: 0,
450 minute: 32,
451 second: 0,
452 nanosecond: 999999000,
453 }),
454 offset: None,
455 },
456 ),
457 ];
458 for (input, expected) in inputs {
459 dbg!(input);
460 let actual = date_time.parse(new_input(input)).unwrap();
461 assert_eq!(expected, actual);
462 }
463 }
464
465 #[test]
466 fn time_fraction_truncated() {
467 let input = "1987-07-05T17:45:00.123456789012345Z";
468 date_time.parse(new_input(input)).unwrap();
469 }
470}
471