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
17pub(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 Some((_, time, offset)) => Datetime {
34 date: Some(date),
35 time: Some(time),
36 offset,
37 },
38 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
55pub(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
88pub(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
107pub(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
134pub(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
141pub(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
155pub(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
169pub(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
176pub(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
190pub(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
204pub(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
218pub(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 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 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
262const 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}