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::*;
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)]
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}
473