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;
14
15// ;; Date and Time (as defined in RFC 3339)
16
17// date-time = offset-date-time / local-date-time / local-date / local-time
18// offset-date-time = full-date time-delim full-time
19// local-date-time = full-date time-delim partial-time
20// local-date = full-date
21// local-time = partial-time
22// full-time = partial-time time-offset
23pub(crate) fn date_time(input: Input<'_>) -> IResult<Input<'_>, Datetime, ParserError<'_>> {
24 alt((
25 (full_date, opt((time_delim, partial_time, opt(time_offset))))
26 .map(|(date, opt)| {
27 match opt {
28 // Offset Date-Time
29 Some((_, time, offset)) => Datetime {
30 date: Some(date),
31 time: Some(time),
32 offset,
33 },
34 // Local Date
35 None => Datetime {
36 date: Some(date),
37 time: None,
38 offset: None,
39 },
40 }
41 })
42 .context(Context::Expression("date-time")),
43 partial_time
44 .map(|t| t.into())
45 .context(Context::Expression("time")),
46 ))
47 .parse_next(input)
48}
49
50// full-date = date-fullyear "-" date-month "-" date-mday
51pub(crate) fn full_date(input: Input<'_>) -> IResult<Input<'_>, Date, ParserError<'_>> {
52 (date_fullyear, b'-', cut_err((date_month, b'-', date_mday)))
53 .map(|(year: u16, _, (month: u8, _, day: u8))| Date { year, month, day })
54 .parse_next(input)
55}
56
57// partial-time = time-hour ":" time-minute ":" time-second [time-secfrac]
58pub(crate) fn partial_time(input: Input<'_>) -> IResult<Input<'_>, Time, ParserError<'_>> {
59 (
60 time_hour,
61 b':',
62 cut_err((time_minute, b':', time_second, opt(time_secfrac))),
63 )
64 .map(|(hour: u8, _, (minute: u8, _, second: u8, nanosecond: Option))| Time {
65 hour,
66 minute,
67 second,
68 nanosecond: nanosecond.unwrap_or_default(),
69 })
70 .parse_next(input)
71}
72
73// time-offset = "Z" / time-numoffset
74// time-numoffset = ( "+" / "-" ) time-hour ":" time-minute
75pub(crate) fn time_offset(input: Input<'_>) -> IResult<Input<'_>, Offset, ParserError<'_>> {
76 altContext, …>, …, …, …, …>((
77 one_of((b'Z', b'z')).value(val:Offset::Z),
78 (
79 one_of((b'+', b'-')),
80 cut_err((time_hour, b':', time_minute)),
81 )
82 .map(|(sign, (hours, _, minutes))| {
83 let sign = match sign {
84 b'+' => 1,
85 b'-' => -1,
86 _ => unreachable!("Parser prevents this"),
87 };
88 sign * (hours as i16 * 60 + minutes as i16)
89 })
90 .verify(|minutes: &i16| ((-24 * 60)..=(24 * 60)).contains(item:minutes))
91 .map(|minutes: i16| Offset::Custom { minutes }),
92 ))
93 .context(Context::Expression("time offset"))
94 .parse_next(input)
95}
96
97// date-fullyear = 4DIGIT
98pub(crate) fn date_fullyear(input: Input<'_>) -> IResult<Input<'_>, u16, ParserError<'_>> {
99 unsigned_digitsMap(…) -> …, …, …, …, …, …>::<4, 4>
100 .map(|s: &str| s.parse::<u16>().expect(msg:"4DIGIT should match u8"))
101 .parse_next(input)
102}
103
104// date-month = 2DIGIT ; 01-12
105pub(crate) fn date_month(input: Input<'_>) -> IResult<Input<'_>, u8, ParserError<'_>> {
106 unsigned_digitsTryMap(…) -> …, …, …, …, …, …, …>::<2, 2>
107 .try_map(|s: &str| {
108 let d: u8 = s.parse::<u8>().expect(msg:"2DIGIT should match u8");
109 if (1..=12).contains(&d) {
110 Ok(d)
111 } else {
112 Err(CustomError::OutOfRange)
113 }
114 })
115 .parse_next(input)
116}
117
118// date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on month/year
119pub(crate) fn date_mday(input: Input<'_>) -> IResult<Input<'_>, u8, ParserError<'_>> {
120 unsigned_digitsTryMap(…) -> …, …, …, …, …, …, …>::<2, 2>
121 .try_map(|s: &str| {
122 let d: u8 = s.parse::<u8>().expect(msg:"2DIGIT should match u8");
123 if (1..=31).contains(&d) {
124 Ok(d)
125 } else {
126 Err(CustomError::OutOfRange)
127 }
128 })
129 .parse_next(input)
130}
131
132// time-delim = "T" / %x20 ; T, t, or space
133pub(crate) fn time_delim(input: Input<'_>) -> IResult<Input<'_>, u8, ParserError<'_>> {
134 one_of(TIME_DELIM).parse_next(input)
135}
136
137const TIME_DELIM: (u8, u8, u8) = (b'T', b't', b' ');
138
139// time-hour = 2DIGIT ; 00-23
140pub(crate) fn time_hour(input: Input<'_>) -> IResult<Input<'_>, u8, ParserError<'_>> {
141 unsigned_digitsTryMap(…) -> …, …, …, …, …, …, …>::<2, 2>
142 .try_map(|s: &str| {
143 let d: u8 = s.parse::<u8>().expect(msg:"2DIGIT should match u8");
144 if (0..=23).contains(&d) {
145 Ok(d)
146 } else {
147 Err(CustomError::OutOfRange)
148 }
149 })
150 .parse_next(input)
151}
152
153// time-minute = 2DIGIT ; 00-59
154pub(crate) fn time_minute(input: Input<'_>) -> IResult<Input<'_>, u8, ParserError<'_>> {
155 unsigned_digitsTryMap(…) -> …, …, …, …, …, …, …>::<2, 2>
156 .try_map(|s: &str| {
157 let d: u8 = s.parse::<u8>().expect(msg:"2DIGIT should match u8");
158 if (0..=59).contains(&d) {
159 Ok(d)
160 } else {
161 Err(CustomError::OutOfRange)
162 }
163 })
164 .parse_next(input)
165}
166
167// time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second rules
168pub(crate) fn time_second(input: Input<'_>) -> IResult<Input<'_>, u8, ParserError<'_>> {
169 unsigned_digitsTryMap(…) -> …, …, …, …, …, …, …>::<2, 2>
170 .try_map(|s: &str| {
171 let d: u8 = s.parse::<u8>().expect(msg:"2DIGIT should match u8");
172 if (0..=60).contains(&d) {
173 Ok(d)
174 } else {
175 Err(CustomError::OutOfRange)
176 }
177 })
178 .parse_next(input)
179}
180
181// time-secfrac = "." 1*DIGIT
182pub(crate) fn time_secfrac(input: Input<'_>) -> IResult<Input<'_>, u32, ParserError<'_>> {
183 static SCALE: [u32; 10] = [
184 0,
185 100_000_000,
186 10_000_000,
187 1_000_000,
188 100_000,
189 10_000,
190 1_000,
191 100,
192 10,
193 1,
194 ];
195 const INF: usize = usize::MAX;
196 preceded(b'.', unsigned_digits::<1, INF>)
197 .try_map(|mut repr: &str| -> Result<u32, CustomError> {
198 let max_digits = SCALE.len() - 1;
199 if max_digits < repr.len() {
200 // Millisecond precision is required. Further precision of fractional seconds is
201 // implementation-specific. If the value contains greater precision than the
202 // implementation can support, the additional precision must be truncated, not rounded.
203 repr = &repr[0..max_digits];
204 }
205
206 let v = repr.parse::<u32>().map_err(|_| CustomError::OutOfRange)?;
207 let num_digits = repr.len();
208
209 // scale the number accordingly.
210 let scale = SCALE.get(num_digits).ok_or(CustomError::OutOfRange)?;
211 let v = v.checked_mul(*scale).ok_or(CustomError::OutOfRange)?;
212 Ok(v)
213 })
214 .parse_next(input)
215}
216
217pub(crate) fn unsigned_digits<const MIN: usize, const MAX: usize>(
218 input: Input<'_>,
219) -> IResult<Input<'_>, &str, ParserError<'_>> {
220 take_whileMap, …>, …, …, …, …, …>(MIN..=MAX, DIGIT)
221 .map(|b: &[u8]| unsafe { from_utf8_unchecked(bytes:b, safety_justification:"`is_ascii_digit` filters out on-ASCII") })
222 .parse_next(input)
223}
224
225// DIGIT = %x30-39 ; 0-9
226const DIGIT: RangeInclusive<u8> = b'0'..=b'9';
227
228#[cfg(test)]
229mod test {
230 use super::*;
231
232 #[test]
233 fn offset_date_time() {
234 let inputs = [
235 (
236 "1979-05-27T07:32:00Z",
237 Datetime {
238 date: Some(Date {
239 year: 1979,
240 month: 5,
241 day: 27,
242 }),
243 time: Some(Time {
244 hour: 7,
245 minute: 32,
246 second: 0,
247 nanosecond: 0,
248 }),
249 offset: Some(Offset::Z),
250 },
251 ),
252 (
253 "1979-05-27T00:32:00-07:00",
254 Datetime {
255 date: Some(Date {
256 year: 1979,
257 month: 5,
258 day: 27,
259 }),
260 time: Some(Time {
261 hour: 0,
262 minute: 32,
263 second: 0,
264 nanosecond: 0,
265 }),
266 offset: Some(Offset::Custom { minutes: -7 * 60 }),
267 },
268 ),
269 (
270 "1979-05-27T00:32:00-00:36",
271 Datetime {
272 date: Some(Date {
273 year: 1979,
274 month: 5,
275 day: 27,
276 }),
277 time: Some(Time {
278 hour: 0,
279 minute: 32,
280 second: 0,
281 nanosecond: 0,
282 }),
283 offset: Some(Offset::Custom { minutes: -36 }),
284 },
285 ),
286 (
287 "1979-05-27T00:32:00.999999",
288 Datetime {
289 date: Some(Date {
290 year: 1979,
291 month: 5,
292 day: 27,
293 }),
294 time: Some(Time {
295 hour: 0,
296 minute: 32,
297 second: 0,
298 nanosecond: 999999000,
299 }),
300 offset: None,
301 },
302 ),
303 ];
304 for (input, expected) in inputs {
305 dbg!(input);
306 let actual = date_time.parse(new_input(input)).unwrap();
307 assert_eq!(expected, actual);
308 }
309 }
310
311 #[test]
312 fn local_date_time() {
313 let inputs = [
314 (
315 "1979-05-27T07:32:00",
316 Datetime {
317 date: Some(Date {
318 year: 1979,
319 month: 5,
320 day: 27,
321 }),
322 time: Some(Time {
323 hour: 7,
324 minute: 32,
325 second: 0,
326 nanosecond: 0,
327 }),
328 offset: None,
329 },
330 ),
331 (
332 "1979-05-27T00:32:00.999999",
333 Datetime {
334 date: Some(Date {
335 year: 1979,
336 month: 5,
337 day: 27,
338 }),
339 time: Some(Time {
340 hour: 0,
341 minute: 32,
342 second: 0,
343 nanosecond: 999999000,
344 }),
345 offset: None,
346 },
347 ),
348 ];
349 for (input, expected) in inputs {
350 dbg!(input);
351 let actual = date_time.parse(new_input(input)).unwrap();
352 assert_eq!(expected, actual);
353 }
354 }
355
356 #[test]
357 fn local_date() {
358 let inputs = [
359 (
360 "1979-05-27",
361 Datetime {
362 date: Some(Date {
363 year: 1979,
364 month: 5,
365 day: 27,
366 }),
367 time: None,
368 offset: None,
369 },
370 ),
371 (
372 "2017-07-20",
373 Datetime {
374 date: Some(Date {
375 year: 2017,
376 month: 7,
377 day: 20,
378 }),
379 time: None,
380 offset: None,
381 },
382 ),
383 ];
384 for (input, expected) in inputs {
385 dbg!(input);
386 let actual = date_time.parse(new_input(input)).unwrap();
387 assert_eq!(expected, actual);
388 }
389 }
390
391 #[test]
392 fn local_time() {
393 let inputs = [
394 (
395 "07:32:00",
396 Datetime {
397 date: None,
398 time: Some(Time {
399 hour: 7,
400 minute: 32,
401 second: 0,
402 nanosecond: 0,
403 }),
404 offset: None,
405 },
406 ),
407 (
408 "00:32:00.999999",
409 Datetime {
410 date: None,
411 time: Some(Time {
412 hour: 0,
413 minute: 32,
414 second: 0,
415 nanosecond: 999999000,
416 }),
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 time_fraction_truncated() {
430 let input = "1987-07-05T17:45:00.123456789012345Z";
431 date_time.parse(new_input(input)).unwrap();
432 }
433}
434