1 | use std::ops::RangeInclusive; |
2 | |
3 | use crate::parser::errors::CustomError; |
4 | use crate::parser::prelude::*; |
5 | use crate::parser::trivia::from_utf8_unchecked; |
6 | |
7 | use toml_datetime::*; |
8 | use winnow::combinator::alt; |
9 | use winnow::combinator::cut_err; |
10 | use winnow::combinator::opt; |
11 | use winnow::combinator::preceded; |
12 | use winnow::token::one_of; |
13 | use 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 |
23 | pub(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 |
51 | pub(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] |
58 | pub(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 |
75 | pub(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 |
98 | pub(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 |
105 | pub(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 |
119 | pub(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 |
133 | pub(crate) fn time_delim(input: Input<'_>) -> IResult<Input<'_>, u8, ParserError<'_>> { |
134 | one_of(TIME_DELIM).parse_next(input) |
135 | } |
136 | |
137 | const TIME_DELIM: (u8, u8, u8) = (b'T' , b't' , b' ' ); |
138 | |
139 | // time-hour = 2DIGIT ; 00-23 |
140 | pub(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 |
154 | pub(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 |
168 | pub(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 |
182 | pub(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 | |
217 | pub(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 |
226 | const DIGIT: RangeInclusive<u8> = b'0' ..=b'9' ; |
227 | |
228 | #[cfg (test)] |
229 | mod 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 | |