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