1 | use crate::{ |
2 | civil::{Date, DateTime, Time}, |
3 | error::{err, Error, ErrorContext}, |
4 | fmt::{ |
5 | offset::{self, ParsedOffset}, |
6 | rfc9557::{self, ParsedAnnotations}, |
7 | temporal::Pieces, |
8 | util::{ |
9 | fractional_time_to_duration, fractional_time_to_span, |
10 | parse_temporal_fraction, |
11 | }, |
12 | Parsed, |
13 | }, |
14 | span::Span, |
15 | tz::{ |
16 | AmbiguousZoned, Disambiguation, Offset, OffsetConflict, TimeZone, |
17 | TimeZoneDatabase, |
18 | }, |
19 | util::{ |
20 | escape, parse, |
21 | t::{self, C}, |
22 | }, |
23 | SignedDuration, Timestamp, Unit, Zoned, |
24 | }; |
25 | |
26 | /// The datetime components parsed from a string. |
27 | #[derive (Debug)] |
28 | pub(super) struct ParsedDateTime<'i> { |
29 | /// The original input that the datetime was parsed from. |
30 | input: escape::Bytes<'i>, |
31 | /// A required civil date. |
32 | date: ParsedDate<'i>, |
33 | /// An optional civil time. |
34 | time: Option<ParsedTime<'i>>, |
35 | /// An optional UTC offset. |
36 | offset: Option<ParsedOffset>, |
37 | /// An optional RFC 9557 annotations parsed. |
38 | /// |
39 | /// An empty `ParsedAnnotations` is valid and possible, so this bakes |
40 | /// optionality into the type and doesn't need to be an `Option` itself. |
41 | annotations: ParsedAnnotations<'i>, |
42 | } |
43 | |
44 | impl<'i> ParsedDateTime<'i> { |
45 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
46 | pub(super) fn to_pieces(&self) -> Result<Pieces<'i>, Error> { |
47 | let mut pieces = Pieces::from(self.date.date); |
48 | if let Some(ref time) = self.time { |
49 | pieces = pieces.with_time(time.time); |
50 | } |
51 | if let Some(ref offset) = self.offset { |
52 | pieces = pieces.with_offset(offset.to_pieces_offset()?); |
53 | } |
54 | if let Some(ann) = self.annotations.to_time_zone_annotation()? { |
55 | pieces = pieces.with_time_zone_annotation(ann); |
56 | } |
57 | Ok(pieces) |
58 | } |
59 | |
60 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
61 | pub(super) fn to_zoned( |
62 | &self, |
63 | db: &TimeZoneDatabase, |
64 | offset_conflict: OffsetConflict, |
65 | disambiguation: Disambiguation, |
66 | ) -> Result<Zoned, Error> { |
67 | self.to_ambiguous_zoned(db, offset_conflict)? |
68 | .disambiguate(disambiguation) |
69 | } |
70 | |
71 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
72 | pub(super) fn to_ambiguous_zoned( |
73 | &self, |
74 | db: &TimeZoneDatabase, |
75 | offset_conflict: OffsetConflict, |
76 | ) -> Result<AmbiguousZoned, Error> { |
77 | let time = self.time.as_ref().map_or(Time::midnight(), |p| p.time); |
78 | let dt = DateTime::from_parts(self.date.date, time); |
79 | |
80 | // We always require a time zone when parsing a zoned instant. |
81 | let tz_annotation = |
82 | self.annotations.to_time_zone_annotation()?.ok_or_else(|| { |
83 | err!( |
84 | "failed to find time zone in square brackets \ |
85 | in {:?}, which is required for parsing a zoned instant" , |
86 | self.input, |
87 | ) |
88 | })?; |
89 | let tz = tz_annotation.to_time_zone_with(db)?; |
90 | |
91 | // If there's no offset, then our only choice, regardless of conflict |
92 | // resolution preference, is to use the time zone. That is, there is no |
93 | // possible conflict. |
94 | let Some(ref parsed_offset) = self.offset else { |
95 | return Ok(tz.into_ambiguous_zoned(dt)); |
96 | }; |
97 | if parsed_offset.is_zulu() { |
98 | // When `Z` is used, that means the offset to local time is not |
99 | // known. In this case, there really can't be a conflict because |
100 | // there is an explicit acknowledgment that the offset could be |
101 | // anything. So we just always accept `Z` as if it were `UTC` and |
102 | // respect that. If we didn't have this special check, we'd fall |
103 | // below and the `Z` would just be treated as `+00:00`, which would |
104 | // likely result in `OffsetConflict::Reject` raising an error. |
105 | // (Unless the actual correct offset at the time is `+00:00` for |
106 | // the time zone parsed.) |
107 | return OffsetConflict::AlwaysOffset |
108 | .resolve(dt, Offset::UTC, tz) |
109 | .with_context(|| { |
110 | err!("parsing {input:?} failed" , input = self.input) |
111 | }); |
112 | } |
113 | let offset = parsed_offset.to_offset()?; |
114 | let is_equal = |parsed: Offset, candidate: Offset| { |
115 | // If they're equal down to the second, then no amount of rounding |
116 | // or whatever should change that. |
117 | if parsed == candidate { |
118 | return true; |
119 | } |
120 | // If the candidate offset we're considering is a whole minute, |
121 | // then we never need rounding. |
122 | if candidate.part_seconds_ranged() == C(0) { |
123 | return parsed == candidate; |
124 | } |
125 | let Ok(candidate) = candidate.round(Unit::Minute) else { |
126 | // This is a degenerate case and this is the only sensible |
127 | // thing to do. |
128 | return parsed == candidate; |
129 | }; |
130 | parsed == candidate |
131 | }; |
132 | offset_conflict.resolve_with(dt, offset, tz, is_equal).with_context( |
133 | || err!("parsing {input:?} failed" , input = self.input), |
134 | ) |
135 | } |
136 | |
137 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
138 | pub(super) fn to_timestamp(&self) -> Result<Timestamp, Error> { |
139 | let time = self.time.as_ref().map(|p| p.time).ok_or_else(|| { |
140 | err!( |
141 | "failed to find time component in {:?}, \ |
142 | which is required for parsing a timestamp" , |
143 | self.input, |
144 | ) |
145 | })?; |
146 | let parsed_offset = self.offset.as_ref().ok_or_else(|| { |
147 | err!( |
148 | "failed to find offset component in {:?}, \ |
149 | which is required for parsing a timestamp" , |
150 | self.input, |
151 | ) |
152 | })?; |
153 | let offset = parsed_offset.to_offset()?; |
154 | let dt = DateTime::from_parts(self.date.date, time); |
155 | let timestamp = offset.to_timestamp(dt).with_context(|| { |
156 | err!( |
157 | "failed to convert civil datetime to timestamp \ |
158 | with offset {offset}" , |
159 | ) |
160 | })?; |
161 | Ok(timestamp) |
162 | } |
163 | |
164 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
165 | pub(super) fn to_datetime(&self) -> Result<DateTime, Error> { |
166 | if self.offset.as_ref().map_or(false, |o| o.is_zulu()) { |
167 | return Err(err!( |
168 | "cannot parse civil date from string with a Zulu \ |
169 | offset, parse as a `Timestamp` and convert to a civil \ |
170 | datetime instead" , |
171 | )); |
172 | } |
173 | Ok(DateTime::from_parts(self.date.date, self.time())) |
174 | } |
175 | |
176 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
177 | pub(super) fn to_date(&self) -> Result<Date, Error> { |
178 | if self.offset.as_ref().map_or(false, |o| o.is_zulu()) { |
179 | return Err(err!( |
180 | "cannot parse civil date from string with a Zulu \ |
181 | offset, parse as a `Timestamp` and convert to a civil \ |
182 | date instead" , |
183 | )); |
184 | } |
185 | Ok(self.date.date) |
186 | } |
187 | |
188 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
189 | fn time(&self) -> Time { |
190 | self.time.as_ref().map(|p| p.time).unwrap_or(Time::midnight()) |
191 | } |
192 | } |
193 | |
194 | impl<'i> core::fmt::Display for ParsedDateTime<'i> { |
195 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
196 | core::fmt::Display::fmt(&self.input, f) |
197 | } |
198 | } |
199 | |
200 | /// The result of parsing a Gregorian calendar civil date. |
201 | #[derive (Debug)] |
202 | pub(super) struct ParsedDate<'i> { |
203 | /// The original input that the date was parsed from. |
204 | input: escape::Bytes<'i>, |
205 | /// The actual parsed date. |
206 | date: Date, |
207 | } |
208 | |
209 | impl<'i> core::fmt::Display for ParsedDate<'i> { |
210 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
211 | core::fmt::Display::fmt(&self.input, f) |
212 | } |
213 | } |
214 | |
215 | /// The result of parsing a 24-hour civil time. |
216 | #[derive (Debug)] |
217 | pub(super) struct ParsedTime<'i> { |
218 | /// The original input that the time was parsed from. |
219 | input: escape::Bytes<'i>, |
220 | /// The actual parsed time. |
221 | time: Time, |
222 | /// Whether the time was parsed in extended format or not. |
223 | extended: bool, |
224 | } |
225 | |
226 | impl<'i> ParsedTime<'i> { |
227 | pub(super) fn to_time(&self) -> Time { |
228 | self.time |
229 | } |
230 | } |
231 | |
232 | impl<'i> core::fmt::Display for ParsedTime<'i> { |
233 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
234 | core::fmt::Display::fmt(&self.input, f) |
235 | } |
236 | } |
237 | |
238 | #[derive (Debug)] |
239 | pub(super) struct ParsedTimeZone<'i> { |
240 | /// The original input that the time zone was parsed from. |
241 | input: escape::Bytes<'i>, |
242 | /// The kind of time zone parsed. |
243 | kind: ParsedTimeZoneKind<'i>, |
244 | } |
245 | |
246 | impl<'i> core::fmt::Display for ParsedTimeZone<'i> { |
247 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
248 | core::fmt::Display::fmt(&self.input, f) |
249 | } |
250 | } |
251 | |
252 | #[derive (Debug)] |
253 | pub(super) enum ParsedTimeZoneKind<'i> { |
254 | Named(&'i str), |
255 | Offset(ParsedOffset), |
256 | #[cfg (feature = "alloc" )] |
257 | Posix(crate::tz::posix::PosixTimeZoneOwned), |
258 | } |
259 | |
260 | impl<'i> ParsedTimeZone<'i> { |
261 | pub(super) fn into_time_zone( |
262 | self, |
263 | db: &TimeZoneDatabase, |
264 | ) -> Result<TimeZone, Error> { |
265 | match self.kind { |
266 | ParsedTimeZoneKind::Named(iana_name) => { |
267 | let tz = db.get(iana_name).with_context(|| { |
268 | err!( |
269 | "parsed apparent IANA time zone identifier \ |
270 | {iana_name} from {input}, but the tzdb lookup \ |
271 | failed" , |
272 | input = self.input, |
273 | ) |
274 | })?; |
275 | Ok(tz) |
276 | } |
277 | ParsedTimeZoneKind::Offset(poff) => { |
278 | let offset = poff.to_offset().with_context(|| { |
279 | err!( |
280 | "offset successfully parsed from {input}, \ |
281 | but failed to convert to numeric `Offset`" , |
282 | input = self.input, |
283 | ) |
284 | })?; |
285 | Ok(TimeZone::fixed(offset)) |
286 | } |
287 | #[cfg (feature = "alloc" )] |
288 | ParsedTimeZoneKind::Posix(posix_tz) => { |
289 | Ok(TimeZone::from_posix_tz(posix_tz)) |
290 | } |
291 | } |
292 | } |
293 | } |
294 | |
295 | /// A parser for Temporal datetimes. |
296 | #[derive (Debug)] |
297 | pub(super) struct DateTimeParser { |
298 | /// There are currently no configuration options for this parser. |
299 | _priv: (), |
300 | } |
301 | |
302 | impl DateTimeParser { |
303 | /// Create a new Temporal datetime parser with the default configuration. |
304 | pub(super) const fn new() -> DateTimeParser { |
305 | DateTimeParser { _priv: () } |
306 | } |
307 | |
308 | // TemporalDateTimeString[Zoned] ::: |
309 | // AnnotatedDateTime[?Zoned] |
310 | // |
311 | // AnnotatedDateTime[Zoned] ::: |
312 | // [~Zoned] DateTime TimeZoneAnnotation[opt] Annotations[opt] |
313 | // [+Zoned] DateTime TimeZoneAnnotation Annotations[opt] |
314 | // |
315 | // DateTime ::: |
316 | // Date |
317 | // Date DateTimeSeparator TimeSpec DateTimeUTCOffset[opt] |
318 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
319 | pub(super) fn parse_temporal_datetime<'i>( |
320 | &self, |
321 | input: &'i [u8], |
322 | ) -> Result<Parsed<'i, ParsedDateTime<'i>>, Error> { |
323 | let mkslice = parse::slicer(input); |
324 | let Parsed { value: date, input } = self.parse_date_spec(input)?; |
325 | if input.is_empty() { |
326 | let value = ParsedDateTime { |
327 | input: escape::Bytes(mkslice(input)), |
328 | date, |
329 | time: None, |
330 | offset: None, |
331 | annotations: ParsedAnnotations::none(), |
332 | }; |
333 | return Ok(Parsed { value, input }); |
334 | } |
335 | let (time, offset, input) = if !matches!(input[0], b' ' | b'T' | b't' ) |
336 | { |
337 | (None, None, input) |
338 | } else { |
339 | let input = &input[1..]; |
340 | // If there's a separator, then we must parse a time and we are |
341 | // *allowed* to parse an offset. But without a separator, we don't |
342 | // support offsets. Just annotations (which are parsed below). |
343 | let Parsed { value: time, input } = self.parse_time_spec(input)?; |
344 | let Parsed { value: offset, input } = self.parse_offset(input)?; |
345 | (Some(time), offset, input) |
346 | }; |
347 | let Parsed { value: annotations, input } = |
348 | self.parse_annotations(input)?; |
349 | let value = ParsedDateTime { |
350 | input: escape::Bytes(mkslice(input)), |
351 | date, |
352 | time, |
353 | offset, |
354 | annotations, |
355 | }; |
356 | Ok(Parsed { value, input }) |
357 | } |
358 | |
359 | // TemporalTimeString ::: |
360 | // AnnotatedTime |
361 | // AnnotatedDateTimeTimeRequired |
362 | // |
363 | // AnnotatedTime ::: |
364 | // TimeDesignator TimeSpec |
365 | // DateTimeUTCOffset[opt] |
366 | // TimeZoneAnnotation[opt] |
367 | // Annotations[opt] |
368 | // TimeSpecWithOptionalOffsetNotAmbiguous TimeZoneAnnotation[opt] |
369 | // Annotations[opt] |
370 | // |
371 | // TimeSpecWithOptionalOffsetNotAmbiguous ::: |
372 | // TimeSpec DateTimeUTCOffsetopt (but not one of ValidMonthDay or DateSpecYearMonth) |
373 | // |
374 | // TimeDesignator ::: one of |
375 | // T t |
376 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
377 | pub(super) fn parse_temporal_time<'i>( |
378 | &self, |
379 | mut input: &'i [u8], |
380 | ) -> Result<Parsed<'i, ParsedTime<'i>>, Error> { |
381 | let mkslice = parse::slicer(input); |
382 | |
383 | if input.starts_with(b"T" ) || input.starts_with(b"t" ) { |
384 | input = &input[1..]; |
385 | let Parsed { value: time, input } = self.parse_time_spec(input)?; |
386 | let Parsed { value: offset, input } = self.parse_offset(input)?; |
387 | if offset.map_or(false, |o| o.is_zulu()) { |
388 | return Err(err!( |
389 | "cannot parse civil time from string with a Zulu \ |
390 | offset, parse as a `Timestamp` and convert to a civil \ |
391 | time instead" , |
392 | )); |
393 | } |
394 | let Parsed { input, .. } = self.parse_annotations(input)?; |
395 | return Ok(Parsed { value: time, input }); |
396 | } |
397 | // We now look for a full datetime and extract the time from that. |
398 | // We do this before looking for a non-T time-only component because |
399 | // otherwise things like `2024-06-01T01:02:03` end up having `2024-06` |
400 | // parsed as a `HHMM-OFFSET` time, and then result in an "ambiguous" |
401 | // error. |
402 | // |
403 | // This is largely a result of us trying to parse a time off of the |
404 | // beginning of the input without assuming that the time must consume |
405 | // the entire input. |
406 | if let Ok(parsed) = self.parse_temporal_datetime(input) { |
407 | let Parsed { value: dt, input } = parsed; |
408 | if dt.offset.map_or(false, |o| o.is_zulu()) { |
409 | return Err(err!( |
410 | "cannot parse plain time from full datetime string with a \ |
411 | Zulu offset, parse as a `Timestamp` and convert to a \ |
412 | plain time instead" , |
413 | )); |
414 | } |
415 | let Some(time) = dt.time else { |
416 | return Err(err!( |
417 | "successfully parsed date from {parsed:?}, but \ |
418 | no time component was found" , |
419 | parsed = dt.input, |
420 | )); |
421 | }; |
422 | return Ok(Parsed { value: time, input }); |
423 | } |
424 | |
425 | // At this point, we look for something that is a time that doesn't |
426 | // start with a `T`. We need to check that it isn't ambiguous with a |
427 | // possible date. |
428 | let Parsed { value: time, input } = self.parse_time_spec(input)?; |
429 | let Parsed { value: offset, input } = self.parse_offset(input)?; |
430 | if offset.map_or(false, |o| o.is_zulu()) { |
431 | return Err(err!( |
432 | "cannot parse plain time from string with a Zulu \ |
433 | offset, parse as a `Timestamp` and convert to a plain \ |
434 | time instead" , |
435 | )); |
436 | } |
437 | // The possible ambiguities occur with the time AND the |
438 | // optional offset, so try to parse what we have so far as |
439 | // either a "month-day" or a "year-month." If either succeeds, |
440 | // then the time is ambiguous and we can report an error. |
441 | // |
442 | // ... but this can only happen when the time was parsed in |
443 | // "basic" mode. i.e., without the `:` separators. |
444 | if !time.extended { |
445 | let possibly_ambiguous = mkslice(input); |
446 | if self.parse_month_day(possibly_ambiguous).is_ok() { |
447 | return Err(err!( |
448 | "parsed time from {parsed:?} is ambiguous \ |
449 | with a month-day date" , |
450 | parsed = escape::Bytes(possibly_ambiguous), |
451 | )); |
452 | } |
453 | if self.parse_year_month(possibly_ambiguous).is_ok() { |
454 | return Err(err!( |
455 | "parsed time from {parsed:?} is ambiguous \ |
456 | with a year-month date" , |
457 | parsed = escape::Bytes(possibly_ambiguous), |
458 | )); |
459 | } |
460 | } |
461 | // OK... carry on. |
462 | let Parsed { input, .. } = self.parse_annotations(input)?; |
463 | Ok(Parsed { value: time, input }) |
464 | } |
465 | |
466 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
467 | pub(super) fn parse_time_zone<'i>( |
468 | &self, |
469 | mut input: &'i [u8], |
470 | ) -> Result<Parsed<'i, ParsedTimeZone<'i>>, Error> { |
471 | let Some(first) = input.first().copied() else { |
472 | return Err(err!("an empty string is not a valid time zone" )); |
473 | }; |
474 | let original = escape::Bytes(input); |
475 | if matches!(first, b'+' | b'-' ) { |
476 | static P: offset::Parser = offset::Parser::new() |
477 | .zulu(false) |
478 | .subminute(true) |
479 | .subsecond(false); |
480 | let Parsed { value: offset, input } = P.parse(input)?; |
481 | let kind = ParsedTimeZoneKind::Offset(offset); |
482 | let value = ParsedTimeZone { input: original, kind }; |
483 | return Ok(Parsed { value, input }); |
484 | } |
485 | |
486 | // Creates a "named" parsed time zone, generally meant to |
487 | // be an IANA time zone identifier. We do this in a couple |
488 | // different cases below, hence the helper function. |
489 | let mknamed = |consumed, remaining| { |
490 | let Ok(tzid) = core::str::from_utf8(consumed) else { |
491 | return Err(err!( |
492 | "found plausible IANA time zone identifier \ |
493 | {input:?}, but it is not valid UTF-8" , |
494 | input = escape::Bytes(consumed), |
495 | )); |
496 | }; |
497 | let kind = ParsedTimeZoneKind::Named(tzid); |
498 | let value = ParsedTimeZone { input: original, kind }; |
499 | Ok(Parsed { value, input: remaining }) |
500 | }; |
501 | // This part get tricky. The common case is absolutely an IANA time |
502 | // zone identifer. So we try to parse something that looks like an IANA |
503 | // tz id. |
504 | // |
505 | // In theory, IANA tz ids can never be valid POSIX TZ strings, since |
506 | // POSIX TZ strings minimally require an offset in them (e.g., `EST5`) |
507 | // and IANA tz ids aren't supposed to contain numbers. But there are |
508 | // some legacy IANA tz ids (`EST5EDT`) that do contain numbers. |
509 | // |
510 | // However, the legacy IANA tz ids, like `EST5EDT`, are pretty much |
511 | // nonsense as POSIX TZ strings since there is no DST transition rule. |
512 | // So in cases of nonsense tz ids, we assume they are IANA tz ids. |
513 | let mkconsumed = parse::slicer(input); |
514 | let mut saw_number = false; |
515 | loop { |
516 | let Some(byte) = input.first().copied() else { break }; |
517 | if byte.is_ascii_whitespace() { |
518 | break; |
519 | } |
520 | saw_number = saw_number || byte.is_ascii_digit(); |
521 | input = &input[1..]; |
522 | } |
523 | let consumed = mkconsumed(input); |
524 | if !saw_number { |
525 | return mknamed(consumed, input); |
526 | } |
527 | #[cfg (not(feature = "alloc" ))] |
528 | { |
529 | Err(err!( |
530 | "cannot parsed time zones other than fixed offsets \ |
531 | without the `alloc` crate feature enabled" , |
532 | )) |
533 | } |
534 | #[cfg (feature = "alloc" )] |
535 | { |
536 | use crate::tz::posix::PosixTimeZone; |
537 | |
538 | match PosixTimeZone::parse_prefix(consumed) { |
539 | Ok((posix_tz, input)) => { |
540 | let kind = ParsedTimeZoneKind::Posix(posix_tz); |
541 | let value = ParsedTimeZone { input: original, kind }; |
542 | Ok(Parsed { value, input }) |
543 | } |
544 | // We get here for invalid POSIX tz strings, or even if |
545 | // they are technically valid according to POSIX but not |
546 | // "reasonable", i.e., `EST5EDT`. Which in that case would |
547 | // end up doing an IANA tz lookup. (And it might hit because |
548 | // `EST5EDT` is a legacy IANA tz id. Lol.) |
549 | Err(_) => mknamed(consumed, input), |
550 | } |
551 | } |
552 | } |
553 | |
554 | // Date ::: |
555 | // DateYear - DateMonth - DateDay |
556 | // DateYear DateMonth DateDay |
557 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
558 | fn parse_date_spec<'i>( |
559 | &self, |
560 | input: &'i [u8], |
561 | ) -> Result<Parsed<'i, ParsedDate<'i>>, Error> { |
562 | let mkslice = parse::slicer(input); |
563 | let original = escape::Bytes(input); |
564 | |
565 | // Parse year component. |
566 | let Parsed { value: year, input } = |
567 | self.parse_year(input).with_context(|| { |
568 | err!("failed to parse year in date {original:?}" ) |
569 | })?; |
570 | let extended = input.starts_with(b"-" ); |
571 | |
572 | // Parse optional separator. |
573 | let Parsed { input, .. } = self |
574 | .parse_date_separator(input, extended) |
575 | .context("failed to parse separator after year" )?; |
576 | |
577 | // Parse month component. |
578 | let Parsed { value: month, input } = |
579 | self.parse_month(input).with_context(|| { |
580 | err!("failed to parse month in date {original:?}" ) |
581 | })?; |
582 | |
583 | // Parse optional separator. |
584 | let Parsed { input, .. } = self |
585 | .parse_date_separator(input, extended) |
586 | .context("failed to parse separator after month" )?; |
587 | |
588 | // Parse day component. |
589 | let Parsed { value: day, input } = |
590 | self.parse_day(input).with_context(|| { |
591 | err!("failed to parse day in date {original:?}" ) |
592 | })?; |
593 | |
594 | let date = Date::new_ranged(year, month, day).with_context(|| { |
595 | err!("date parsed from {original:?} is not valid" ) |
596 | })?; |
597 | let value = ParsedDate { input: escape::Bytes(mkslice(input)), date }; |
598 | Ok(Parsed { value, input }) |
599 | } |
600 | |
601 | // TimeSpec ::: |
602 | // TimeHour |
603 | // TimeHour : TimeMinute |
604 | // TimeHour TimeMinute |
605 | // TimeHour : TimeMinute : TimeSecond TimeFraction[opt] |
606 | // TimeHour TimeMinute TimeSecond TimeFraction[opt] |
607 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
608 | fn parse_time_spec<'i>( |
609 | &self, |
610 | input: &'i [u8], |
611 | ) -> Result<Parsed<'i, ParsedTime<'i>>, Error> { |
612 | let mkslice = parse::slicer(input); |
613 | let original = escape::Bytes(input); |
614 | |
615 | // Parse hour component. |
616 | let Parsed { value: hour, input } = |
617 | self.parse_hour(input).with_context(|| { |
618 | err!("failed to parse hour in time {original:?}" ) |
619 | })?; |
620 | let extended = input.starts_with(b":" ); |
621 | |
622 | // Parse optional minute component. |
623 | let Parsed { value: has_minute, input } = |
624 | self.parse_time_separator(input, extended); |
625 | if !has_minute { |
626 | let time = Time::new_ranged( |
627 | hour, |
628 | t::Minute::N::<0>(), |
629 | t::Second::N::<0>(), |
630 | t::SubsecNanosecond::N::<0>(), |
631 | ); |
632 | let value = ParsedTime { |
633 | input: escape::Bytes(mkslice(input)), |
634 | time, |
635 | extended, |
636 | }; |
637 | return Ok(Parsed { value, input }); |
638 | } |
639 | let Parsed { value: minute, input } = |
640 | self.parse_minute(input).with_context(|| { |
641 | err!("failed to parse minute in time {original:?}" ) |
642 | })?; |
643 | |
644 | // Parse optional second component. |
645 | let Parsed { value: has_second, input } = |
646 | self.parse_time_separator(input, extended); |
647 | if !has_second { |
648 | let time = Time::new_ranged( |
649 | hour, |
650 | minute, |
651 | t::Second::N::<0>(), |
652 | t::SubsecNanosecond::N::<0>(), |
653 | ); |
654 | let value = ParsedTime { |
655 | input: escape::Bytes(mkslice(input)), |
656 | time, |
657 | extended, |
658 | }; |
659 | return Ok(Parsed { value, input }); |
660 | } |
661 | let Parsed { value: second, input } = |
662 | self.parse_second(input).with_context(|| { |
663 | err!("failed to parse second in time {original:?}" ) |
664 | })?; |
665 | |
666 | // Parse an optional fractional component. |
667 | let Parsed { value: nanosecond, input } = |
668 | parse_temporal_fraction(input).with_context(|| { |
669 | err!( |
670 | "failed to parse fractional nanoseconds \ |
671 | in time {original:?}" , |
672 | ) |
673 | })?; |
674 | |
675 | let time = Time::new_ranged( |
676 | hour, |
677 | minute, |
678 | second, |
679 | nanosecond.unwrap_or(t::SubsecNanosecond::N::<0>()), |
680 | ); |
681 | let value = ParsedTime { |
682 | input: escape::Bytes(mkslice(input)), |
683 | time, |
684 | extended, |
685 | }; |
686 | Ok(Parsed { value, input }) |
687 | } |
688 | |
689 | // ValidMonthDay ::: |
690 | // DateMonth -[opt] 0 NonZeroDigit |
691 | // DateMonth -[opt] 1 DecimalDigit |
692 | // DateMonth -[opt] 2 DecimalDigit |
693 | // DateMonth -[opt] 30 but not one of 0230 or 02-30 |
694 | // DateMonthWithThirtyOneDays -opt 31 |
695 | // |
696 | // DateMonthWithThirtyOneDays ::: one of |
697 | // 01 03 05 07 08 10 12 |
698 | // |
699 | // NOTE: Jiff doesn't have a "month-day" type, but we still have a parsing |
700 | // function for it so that we can detect ambiguous time strings. |
701 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
702 | fn parse_month_day<'i>( |
703 | &self, |
704 | input: &'i [u8], |
705 | ) -> Result<Parsed<'i, ()>, Error> { |
706 | let original = escape::Bytes(input); |
707 | |
708 | // Parse month component. |
709 | let Parsed { value: month, mut input } = |
710 | self.parse_month(input).with_context(|| { |
711 | err!("failed to parse month in month-day {original:?}" ) |
712 | })?; |
713 | |
714 | // Skip over optional separator. |
715 | if input.starts_with(b"-" ) { |
716 | input = &input[1..]; |
717 | } |
718 | |
719 | // Parse day component. |
720 | let Parsed { value: day, input } = |
721 | self.parse_day(input).with_context(|| { |
722 | err!("failed to parse day in month-day {original:?}" ) |
723 | })?; |
724 | |
725 | // Check that the month-day is valid. Since Temporal's month-day |
726 | // permits 02-29, we use a leap year. The error message here is |
727 | // probably confusing, but these errors should never be exposed to the |
728 | // user. |
729 | let year = t::Year::N::<2024>(); |
730 | let _ = Date::new_ranged(year, month, day).with_context(|| { |
731 | err!("month-day parsed from {original:?} is not valid" ) |
732 | })?; |
733 | |
734 | // We have a valid year-month. But we don't return it because we just |
735 | // need to check validity. |
736 | Ok(Parsed { value: (), input }) |
737 | } |
738 | |
739 | // DateSpecYearMonth ::: |
740 | // DateYear -[opt] DateMonth |
741 | // |
742 | // NOTE: Jiff doesn't have a "year-month" type, but we still have a parsing |
743 | // function for it so that we can detect ambiguous time strings. |
744 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
745 | fn parse_year_month<'i>( |
746 | &self, |
747 | input: &'i [u8], |
748 | ) -> Result<Parsed<'i, ()>, Error> { |
749 | let original = escape::Bytes(input); |
750 | |
751 | // Parse year component. |
752 | let Parsed { value: year, mut input } = |
753 | self.parse_year(input).with_context(|| { |
754 | err!("failed to parse year in date {original:?}" ) |
755 | })?; |
756 | |
757 | // Skip over optional separator. |
758 | if input.starts_with(b"-" ) { |
759 | input = &input[1..]; |
760 | } |
761 | |
762 | // Parse month component. |
763 | let Parsed { value: month, input } = |
764 | self.parse_month(input).with_context(|| { |
765 | err!("failed to parse month in month-day {original:?}" ) |
766 | })?; |
767 | |
768 | // Check that the year-month is valid. We just use a day of 1, since |
769 | // every month in every year must have a day 1. |
770 | let day = t::Day::N::<1>(); |
771 | let _ = Date::new_ranged(year, month, day).with_context(|| { |
772 | err!("year-month parsed from {original:?} is not valid" ) |
773 | })?; |
774 | |
775 | // We have a valid year-month. But we don't return it because we just |
776 | // need to check validity. |
777 | Ok(Parsed { value: (), input }) |
778 | } |
779 | |
780 | // DateYear ::: |
781 | // DecimalDigit DecimalDigit DecimalDigit DecimalDigit |
782 | // TemporalSign DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit |
783 | // |
784 | // NOTE: I don't really like the fact that in order to write a negative |
785 | // year, you need to use the six digit variant. Like, why not allow |
786 | // `-0001`? I'm not sure why, so for Chesterton's fence reasons, I'm |
787 | // sticking with the Temporal spec. But I may loosen this in the future. We |
788 | // should be careful not to introduce any possible ambiguities, though, I |
789 | // don't think there are any? |
790 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
791 | fn parse_year<'i>( |
792 | &self, |
793 | input: &'i [u8], |
794 | ) -> Result<Parsed<'i, t::Year>, Error> { |
795 | let Parsed { value: sign, input } = self.parse_year_sign(input); |
796 | if let Some(sign) = sign { |
797 | let (year, input) = parse::split(input, 6).ok_or_else(|| { |
798 | err!( |
799 | "expected six digit year (because of a leading sign), \ |
800 | but found end of input" , |
801 | ) |
802 | })?; |
803 | let year = parse::i64(year).with_context(|| { |
804 | err!( |
805 | "failed to parse {year:?} as year (a six digit integer)" , |
806 | year = escape::Bytes(year), |
807 | ) |
808 | })?; |
809 | let year = |
810 | t::Year::try_new("year" , year).context("year is not valid" )?; |
811 | if year == C(0) && sign < C(0) { |
812 | return Err(err!( |
813 | "year zero must be written without a sign or a \ |
814 | positive sign, but not a negative sign" , |
815 | )); |
816 | } |
817 | Ok(Parsed { value: year * sign, input }) |
818 | } else { |
819 | let (year, input) = parse::split(input, 4).ok_or_else(|| { |
820 | err!( |
821 | "expected four digit year (or leading sign for \ |
822 | six digit year), but found end of input" , |
823 | ) |
824 | })?; |
825 | let year = parse::i64(year).with_context(|| { |
826 | err!( |
827 | "failed to parse {year:?} as year (a four digit integer)" , |
828 | year = escape::Bytes(year), |
829 | ) |
830 | })?; |
831 | let year = |
832 | t::Year::try_new("year" , year).context("year is not valid" )?; |
833 | Ok(Parsed { value: year, input }) |
834 | } |
835 | } |
836 | |
837 | // DateMonth ::: |
838 | // 0 NonZeroDigit |
839 | // 10 |
840 | // 11 |
841 | // 12 |
842 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
843 | fn parse_month<'i>( |
844 | &self, |
845 | input: &'i [u8], |
846 | ) -> Result<Parsed<'i, t::Month>, Error> { |
847 | let (month, input) = parse::split(input, 2).ok_or_else(|| { |
848 | err!("expected two digit month, but found end of input" ) |
849 | })?; |
850 | let month = parse::i64(month).with_context(|| { |
851 | err!( |
852 | "failed to parse {month:?} as month (a two digit integer)" , |
853 | month = escape::Bytes(month), |
854 | ) |
855 | })?; |
856 | let month = |
857 | t::Month::try_new("month" , month).context("month is not valid" )?; |
858 | Ok(Parsed { value: month, input }) |
859 | } |
860 | |
861 | // DateDay ::: |
862 | // 0 NonZeroDigit |
863 | // 1 DecimalDigit |
864 | // 2 DecimalDigit |
865 | // 30 |
866 | // 31 |
867 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
868 | fn parse_day<'i>( |
869 | &self, |
870 | input: &'i [u8], |
871 | ) -> Result<Parsed<'i, t::Day>, Error> { |
872 | let (day, input) = parse::split(input, 2).ok_or_else(|| { |
873 | err!("expected two digit day, but found end of input" ) |
874 | })?; |
875 | let day = parse::i64(day).with_context(|| { |
876 | err!( |
877 | "failed to parse {day:?} as day (a two digit integer)" , |
878 | day = escape::Bytes(day), |
879 | ) |
880 | })?; |
881 | let day = t::Day::try_new("day" , day).context("day is not valid" )?; |
882 | Ok(Parsed { value: day, input }) |
883 | } |
884 | |
885 | // TimeHour ::: |
886 | // Hour |
887 | // |
888 | // Hour ::: |
889 | // 0 DecimalDigit |
890 | // 1 DecimalDigit |
891 | // 20 |
892 | // 21 |
893 | // 22 |
894 | // 23 |
895 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
896 | fn parse_hour<'i>( |
897 | &self, |
898 | input: &'i [u8], |
899 | ) -> Result<Parsed<'i, t::Hour>, Error> { |
900 | let (hour, input) = parse::split(input, 2).ok_or_else(|| { |
901 | err!("expected two digit hour, but found end of input" ) |
902 | })?; |
903 | let hour = parse::i64(hour).with_context(|| { |
904 | err!( |
905 | "failed to parse {hour:?} as hour (a two digit integer)" , |
906 | hour = escape::Bytes(hour), |
907 | ) |
908 | })?; |
909 | let hour = |
910 | t::Hour::try_new("hour" , hour).context("hour is not valid" )?; |
911 | Ok(Parsed { value: hour, input }) |
912 | } |
913 | |
914 | // TimeMinute ::: |
915 | // MinuteSecond |
916 | // |
917 | // MinuteSecond ::: |
918 | // 0 DecimalDigit |
919 | // 1 DecimalDigit |
920 | // 2 DecimalDigit |
921 | // 3 DecimalDigit |
922 | // 4 DecimalDigit |
923 | // 5 DecimalDigit |
924 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
925 | fn parse_minute<'i>( |
926 | &self, |
927 | input: &'i [u8], |
928 | ) -> Result<Parsed<'i, t::Minute>, Error> { |
929 | let (minute, input) = parse::split(input, 2).ok_or_else(|| { |
930 | err!("expected two digit minute, but found end of input" ) |
931 | })?; |
932 | let minute = parse::i64(minute).with_context(|| { |
933 | err!( |
934 | "failed to parse {minute:?} as minute (a two digit integer)" , |
935 | minute = escape::Bytes(minute), |
936 | ) |
937 | })?; |
938 | let minute = t::Minute::try_new("minute" , minute) |
939 | .context("minute is not valid" )?; |
940 | Ok(Parsed { value: minute, input }) |
941 | } |
942 | |
943 | // TimeSecond ::: |
944 | // MinuteSecond |
945 | // 60 |
946 | // |
947 | // MinuteSecond ::: |
948 | // 0 DecimalDigit |
949 | // 1 DecimalDigit |
950 | // 2 DecimalDigit |
951 | // 3 DecimalDigit |
952 | // 4 DecimalDigit |
953 | // 5 DecimalDigit |
954 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
955 | fn parse_second<'i>( |
956 | &self, |
957 | input: &'i [u8], |
958 | ) -> Result<Parsed<'i, t::Second>, Error> { |
959 | let (second, input) = parse::split(input, 2).ok_or_else(|| { |
960 | err!("expected two digit second, but found end of input" ,) |
961 | })?; |
962 | let mut second = parse::i64(second).with_context(|| { |
963 | err!( |
964 | "failed to parse {second:?} as second (a two digit integer)" , |
965 | second = escape::Bytes(second), |
966 | ) |
967 | })?; |
968 | // NOTE: I believe Temporal allows one to make this configurable. That |
969 | // is, to reject it. But for now, we just always clamp a leap second. |
970 | if second == 60 { |
971 | second = 59; |
972 | } |
973 | let second = t::Second::try_new("second" , second) |
974 | .context("second is not valid" )?; |
975 | Ok(Parsed { value: second, input }) |
976 | } |
977 | |
978 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
979 | fn parse_offset<'i>( |
980 | &self, |
981 | input: &'i [u8], |
982 | ) -> Result<Parsed<'i, Option<ParsedOffset>>, Error> { |
983 | const P: offset::Parser = |
984 | offset::Parser::new().zulu(true).subminute(true); |
985 | P.parse_optional(input) |
986 | } |
987 | |
988 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
989 | fn parse_annotations<'i>( |
990 | &self, |
991 | input: &'i [u8], |
992 | ) -> Result<Parsed<'i, ParsedAnnotations<'i>>, Error> { |
993 | const P: rfc9557::Parser = rfc9557::Parser::new(); |
994 | if input.is_empty() || input[0] != b'[' { |
995 | let value = ParsedAnnotations::none(); |
996 | return Ok(Parsed { input, value }); |
997 | } |
998 | P.parse(input) |
999 | } |
1000 | |
1001 | /// Parses the separator that is expected to appear between |
1002 | /// date components. |
1003 | /// |
1004 | /// When in extended mode, a `-` is expected. When not in extended mode, |
1005 | /// no input is consumed and this routine never fails. |
1006 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
1007 | fn parse_date_separator<'i>( |
1008 | &self, |
1009 | mut input: &'i [u8], |
1010 | extended: bool, |
1011 | ) -> Result<Parsed<'i, ()>, Error> { |
1012 | if !extended { |
1013 | // If we see a '-' when not in extended mode, then we can report |
1014 | // a better error message than, e.g., "-3 isn't a valid day." |
1015 | if input.starts_with(b"-" ) { |
1016 | return Err(err!( |
1017 | "expected no separator after month since none was \ |
1018 | found after the year, but found a '-' separator" , |
1019 | )); |
1020 | } |
1021 | return Ok(Parsed { value: (), input }); |
1022 | } |
1023 | if input.is_empty() { |
1024 | return Err(err!( |
1025 | "expected '-' separator, but found end of input" |
1026 | )); |
1027 | } |
1028 | if input[0] != b'-' { |
1029 | return Err(err!( |
1030 | "expected '-' separator, but found {found:?} instead" , |
1031 | found = escape::Byte(input[0]), |
1032 | )); |
1033 | } |
1034 | input = &input[1..]; |
1035 | Ok(Parsed { value: (), input }) |
1036 | } |
1037 | |
1038 | /// Parses the separator that is expected to appear between time |
1039 | /// components. When `true` is returned, we expect to parse the next |
1040 | /// component. When `false` is returned, then no separator was found and |
1041 | /// there is no expectation of finding another component. |
1042 | /// |
1043 | /// When in extended mode, true is returned if and only if a separator is |
1044 | /// found. |
1045 | /// |
1046 | /// When in basic mode (not extended), then a subsequent component is only |
1047 | /// expected when `input` begins with two ASCII digits. |
1048 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
1049 | fn parse_time_separator<'i>( |
1050 | &self, |
1051 | mut input: &'i [u8], |
1052 | extended: bool, |
1053 | ) -> Parsed<'i, bool> { |
1054 | if !extended { |
1055 | let expected = |
1056 | input.len() >= 2 && input[..2].iter().all(u8::is_ascii_digit); |
1057 | return Parsed { value: expected, input }; |
1058 | } |
1059 | let is_separator = input.get(0).map_or(false, |&b| b == b':' ); |
1060 | if is_separator { |
1061 | input = &input[1..]; |
1062 | } |
1063 | Parsed { value: is_separator, input } |
1064 | } |
1065 | |
1066 | // TemporalSign ::: |
1067 | // ASCIISign |
1068 | // <MINUS> |
1069 | // |
1070 | // ASCIISign ::: one of |
1071 | // + - |
1072 | // |
1073 | // NOTE: We specifically only support ASCII signs. I think Temporal needs |
1074 | // to support `<MINUS>` because of other things in ECMA script that |
1075 | // require it?[1] |
1076 | // |
1077 | // [1]: https://github.com/tc39/proposal-temporal/issues/2843 |
1078 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
1079 | fn parse_year_sign<'i>( |
1080 | &self, |
1081 | mut input: &'i [u8], |
1082 | ) -> Parsed<'i, Option<t::Sign>> { |
1083 | let Some(sign) = input.get(0).copied() else { |
1084 | return Parsed { value: None, input }; |
1085 | }; |
1086 | let sign = if sign == b'+' { |
1087 | t::Sign::N::<1>() |
1088 | } else if sign == b'-' { |
1089 | t::Sign::N::<-1>() |
1090 | } else { |
1091 | return Parsed { value: None, input }; |
1092 | }; |
1093 | input = &input[1..]; |
1094 | Parsed { value: Some(sign), input } |
1095 | } |
1096 | } |
1097 | |
1098 | /// A parser for Temporal spans. |
1099 | /// |
1100 | /// Note that in Temporal, a "span" is called a "duration." |
1101 | #[derive (Debug)] |
1102 | pub(super) struct SpanParser { |
1103 | /// There are currently no configuration options for this parser. |
1104 | _priv: (), |
1105 | } |
1106 | |
1107 | impl SpanParser { |
1108 | /// Create a new Temporal span parser with the default configuration. |
1109 | pub(super) const fn new() -> SpanParser { |
1110 | SpanParser { _priv: () } |
1111 | } |
1112 | |
1113 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
1114 | pub(super) fn parse_temporal_duration<'i>( |
1115 | &self, |
1116 | input: &'i [u8], |
1117 | ) -> Result<Parsed<'i, Span>, Error> { |
1118 | self.parse_span(input).context( |
1119 | "failed to parse ISO 8601 \ |
1120 | duration string into `Span`" , |
1121 | ) |
1122 | } |
1123 | |
1124 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
1125 | pub(super) fn parse_signed_duration<'i>( |
1126 | &self, |
1127 | input: &'i [u8], |
1128 | ) -> Result<Parsed<'i, SignedDuration>, Error> { |
1129 | self.parse_duration(input).context( |
1130 | "failed to parse ISO 8601 \ |
1131 | duration string into `SignedDuration`" , |
1132 | ) |
1133 | } |
1134 | |
1135 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
1136 | fn parse_span<'i>( |
1137 | &self, |
1138 | input: &'i [u8], |
1139 | ) -> Result<Parsed<'i, Span>, Error> { |
1140 | let original = escape::Bytes(input); |
1141 | let Parsed { value: sign, input } = self.parse_sign(input); |
1142 | let Parsed { input, .. } = self.parse_duration_designator(input)?; |
1143 | let Parsed { value: (mut span, parsed_any_date), input } = |
1144 | self.parse_date_units(input, Span::new())?; |
1145 | let Parsed { value: has_time, mut input } = |
1146 | self.parse_time_designator(input); |
1147 | if has_time { |
1148 | let parsed = self.parse_time_units(input, span)?; |
1149 | input = parsed.input; |
1150 | |
1151 | let (time_span, parsed_any_time) = parsed.value; |
1152 | if !parsed_any_time { |
1153 | return Err(err!( |
1154 | "found a time designator (T or t) in an ISO 8601 \ |
1155 | duration string in {original:?}, but did not find \ |
1156 | any time units" , |
1157 | )); |
1158 | } |
1159 | span = time_span; |
1160 | } else if !parsed_any_date { |
1161 | return Err(err!( |
1162 | "found the start of a ISO 8601 duration string \ |
1163 | in {original:?}, but did not find any units" , |
1164 | )); |
1165 | } |
1166 | if sign < C(0) { |
1167 | span = span.negate(); |
1168 | } |
1169 | Ok(Parsed { value: span, input }) |
1170 | } |
1171 | |
1172 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
1173 | fn parse_duration<'i>( |
1174 | &self, |
1175 | input: &'i [u8], |
1176 | ) -> Result<Parsed<'i, SignedDuration>, Error> { |
1177 | let Parsed { value: sign, input } = self.parse_sign(input); |
1178 | let Parsed { input, .. } = self.parse_duration_designator(input)?; |
1179 | let Parsed { value: has_time, input } = |
1180 | self.parse_time_designator(input); |
1181 | if !has_time { |
1182 | return Err(err!( |
1183 | "parsing ISO 8601 duration into SignedDuration requires \ |
1184 | that the duration contain a time component and no \ |
1185 | components of days or greater" , |
1186 | )); |
1187 | } |
1188 | let Parsed { value: dur, input } = |
1189 | self.parse_time_units_duration(input, sign == C(-1))?; |
1190 | Ok(Parsed { value: dur, input }) |
1191 | } |
1192 | |
1193 | /// Parses consecutive date units from an ISO 8601 duration string into the |
1194 | /// span given. |
1195 | /// |
1196 | /// If 1 or more units were found, then `true` is also returned. Otherwise, |
1197 | /// `false` indicates that no units were parsed. (Which the caller may want |
1198 | /// to treat as an error.) |
1199 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
1200 | fn parse_date_units<'i>( |
1201 | &self, |
1202 | mut input: &'i [u8], |
1203 | mut span: Span, |
1204 | ) -> Result<Parsed<'i, (Span, bool)>, Error> { |
1205 | let mut parsed_any = false; |
1206 | let mut prev_unit: Option<Unit> = None; |
1207 | loop { |
1208 | let parsed = self.parse_unit_value(input)?; |
1209 | input = parsed.input; |
1210 | let Some(value) = parsed.value else { break }; |
1211 | |
1212 | let parsed = self.parse_unit_date_designator(input)?; |
1213 | input = parsed.input; |
1214 | let unit = parsed.value; |
1215 | |
1216 | if let Some(prev_unit) = prev_unit { |
1217 | if prev_unit <= unit { |
1218 | return Err(err!( |
1219 | "found value {value:?} with unit {unit} \ |
1220 | after unit {prev_unit}, but units must be \ |
1221 | written from largest to smallest \ |
1222 | (and they can't be repeated)" , |
1223 | unit = unit.singular(), |
1224 | prev_unit = prev_unit.singular(), |
1225 | )); |
1226 | } |
1227 | } |
1228 | prev_unit = Some(unit); |
1229 | span = span.try_units_ranged(unit, value).with_context(|| { |
1230 | err!( |
1231 | "failed to set value {value:?} as {unit} unit on span" , |
1232 | unit = Unit::from(unit).singular(), |
1233 | ) |
1234 | })?; |
1235 | parsed_any = true; |
1236 | } |
1237 | Ok(Parsed { value: (span, parsed_any), input }) |
1238 | } |
1239 | |
1240 | /// Parses consecutive time units from an ISO 8601 duration string into the |
1241 | /// span given. |
1242 | /// |
1243 | /// If 1 or more units were found, then `true` is also returned. Otherwise, |
1244 | /// `false` indicates that no units were parsed. (Which the caller may want |
1245 | /// to treat as an error.) |
1246 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
1247 | fn parse_time_units<'i>( |
1248 | &self, |
1249 | mut input: &'i [u8], |
1250 | mut span: Span, |
1251 | ) -> Result<Parsed<'i, (Span, bool)>, Error> { |
1252 | let mut parsed_any = false; |
1253 | let mut prev_unit: Option<Unit> = None; |
1254 | loop { |
1255 | let parsed = self.parse_unit_value(input)?; |
1256 | input = parsed.input; |
1257 | let Some(value) = parsed.value else { break }; |
1258 | |
1259 | let parsed = parse_temporal_fraction(input)?; |
1260 | input = parsed.input; |
1261 | let fraction = parsed.value; |
1262 | |
1263 | let parsed = self.parse_unit_time_designator(input)?; |
1264 | input = parsed.input; |
1265 | let unit = parsed.value; |
1266 | |
1267 | if let Some(prev_unit) = prev_unit { |
1268 | if prev_unit <= unit { |
1269 | return Err(err!( |
1270 | "found value {value:?} with unit {unit} \ |
1271 | after unit {prev_unit}, but units must be \ |
1272 | written from largest to smallest \ |
1273 | (and they can't be repeated)" , |
1274 | unit = unit.singular(), |
1275 | prev_unit = prev_unit.singular(), |
1276 | )); |
1277 | } |
1278 | } |
1279 | prev_unit = Some(unit); |
1280 | parsed_any = true; |
1281 | |
1282 | if let Some(fraction) = fraction { |
1283 | span = fractional_time_to_span(unit, value, fraction, span)?; |
1284 | // Once we see a fraction, we are done. We don't permit parsing |
1285 | // any more units. That is, a fraction can only occur on the |
1286 | // lowest unit of time. |
1287 | break; |
1288 | } else { |
1289 | let result = |
1290 | span.try_units_ranged(unit, value).with_context(|| { |
1291 | err!( |
1292 | "failed to set value {value:?} \ |
1293 | as {unit} unit on span" , |
1294 | unit = Unit::from(unit).singular(), |
1295 | ) |
1296 | }); |
1297 | // This is annoying, but because we can write out a larger |
1298 | // number of hours/minutes/seconds than what we actually |
1299 | // support, we need to be prepared to parse an unbalanced span |
1300 | // if our time units are too big here. This entire dance is |
1301 | // because ISO 8601 requires fractional seconds to represent |
1302 | // milli-, micro- and nano-seconds. This means that spans |
1303 | // cannot retain their full fidelity when roundtripping through |
1304 | // ISO 8601. However, it is guaranteed that their total elapsed |
1305 | // time represented will never change. |
1306 | span = match result { |
1307 | Ok(span) => span, |
1308 | Err(_) => fractional_time_to_span( |
1309 | unit, |
1310 | value, |
1311 | t::SubsecNanosecond::N::<0>(), |
1312 | span, |
1313 | )?, |
1314 | }; |
1315 | } |
1316 | } |
1317 | Ok(Parsed { value: (span, parsed_any), input }) |
1318 | } |
1319 | |
1320 | /// Parses consecutive time units from an ISO 8601 duration string into |
1321 | /// a Jiff signed duration. |
1322 | /// |
1323 | /// If no time units are found, then this returns an error. |
1324 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
1325 | fn parse_time_units_duration<'i>( |
1326 | &self, |
1327 | mut input: &'i [u8], |
1328 | negative: bool, |
1329 | ) -> Result<Parsed<'i, SignedDuration>, Error> { |
1330 | let mut parsed_any = false; |
1331 | let mut prev_unit: Option<Unit> = None; |
1332 | let mut dur = SignedDuration::ZERO; |
1333 | |
1334 | loop { |
1335 | let parsed = self.parse_unit_value(input)?; |
1336 | input = parsed.input; |
1337 | let Some(value) = parsed.value else { break }; |
1338 | |
1339 | let parsed = parse_temporal_fraction(input)?; |
1340 | input = parsed.input; |
1341 | let fraction = parsed.value; |
1342 | |
1343 | let parsed = self.parse_unit_time_designator(input)?; |
1344 | input = parsed.input; |
1345 | let unit = parsed.value; |
1346 | |
1347 | if let Some(prev_unit) = prev_unit { |
1348 | if prev_unit <= unit { |
1349 | return Err(err!( |
1350 | "found value {value:?} with unit {unit} \ |
1351 | after unit {prev_unit}, but units must be \ |
1352 | written from largest to smallest \ |
1353 | (and they can't be repeated)" , |
1354 | unit = unit.singular(), |
1355 | prev_unit = prev_unit.singular(), |
1356 | )); |
1357 | } |
1358 | } |
1359 | prev_unit = Some(unit); |
1360 | parsed_any = true; |
1361 | |
1362 | // Convert our parsed unit into a number of seconds. |
1363 | let unit_secs = match unit { |
1364 | Unit::Second => value.get(), |
1365 | Unit::Minute => { |
1366 | let mins = value.get(); |
1367 | mins.checked_mul(60).ok_or_else(|| { |
1368 | err!( |
1369 | "minute units {mins} overflowed i64 when \ |
1370 | converted to seconds" |
1371 | ) |
1372 | })? |
1373 | } |
1374 | Unit::Hour => { |
1375 | let hours = value.get(); |
1376 | hours.checked_mul(3_600).ok_or_else(|| { |
1377 | err!( |
1378 | "hour units {hours} overflowed i64 when \ |
1379 | converted to seconds" |
1380 | ) |
1381 | })? |
1382 | } |
1383 | // Guaranteed not to be here since `parse_unit_time_designator` |
1384 | // always returns hours, minutes or seconds. |
1385 | _ => unreachable!(), |
1386 | }; |
1387 | // Never panics since nanos==0. |
1388 | let unit_dur = SignedDuration::new(unit_secs, 0); |
1389 | // And now try to add it to our existing duration. |
1390 | let result = if negative { |
1391 | dur.checked_sub(unit_dur) |
1392 | } else { |
1393 | dur.checked_add(unit_dur) |
1394 | }; |
1395 | dur = result.ok_or_else(|| { |
1396 | err!( |
1397 | "adding value {value} from unit {unit} overflowed \ |
1398 | signed duration {dur:?}" , |
1399 | unit = unit.singular(), |
1400 | ) |
1401 | })?; |
1402 | |
1403 | if let Some(fraction) = fraction { |
1404 | let fraction_dur = |
1405 | fractional_time_to_duration(unit, fraction)?; |
1406 | let result = if negative { |
1407 | dur.checked_sub(fraction_dur) |
1408 | } else { |
1409 | dur.checked_add(fraction_dur) |
1410 | }; |
1411 | dur = result.ok_or_else(|| { |
1412 | err!( |
1413 | "adding fractional duration {fraction_dur:?} \ |
1414 | from unit {unit} to {dur:?} overflowed \ |
1415 | signed duration limits" , |
1416 | unit = unit.singular(), |
1417 | ) |
1418 | })?; |
1419 | // Once we see a fraction, we are done. We don't permit parsing |
1420 | // any more units. That is, a fraction can only occur on the |
1421 | // lowest unit of time. |
1422 | break; |
1423 | } |
1424 | } |
1425 | if !parsed_any { |
1426 | return Err(err!( |
1427 | "expected at least one unit of time (hours, minutes or \ |
1428 | seconds) in ISO 8601 duration when parsing into a \ |
1429 | `SignedDuration`" , |
1430 | )); |
1431 | } |
1432 | Ok(Parsed { value: dur, input }) |
1433 | } |
1434 | |
1435 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
1436 | fn parse_unit_value<'i>( |
1437 | &self, |
1438 | mut input: &'i [u8], |
1439 | ) -> Result<Parsed<'i, Option<t::NoUnits>>, Error> { |
1440 | // Discovered via `i64::MAX.to_string().len()`. |
1441 | const MAX_I64_DIGITS: usize = 19; |
1442 | |
1443 | let mkdigits = parse::slicer(input); |
1444 | while mkdigits(input).len() <= MAX_I64_DIGITS |
1445 | && input.first().map_or(false, u8::is_ascii_digit) |
1446 | { |
1447 | input = &input[1..]; |
1448 | } |
1449 | let digits = mkdigits(input); |
1450 | if digits.is_empty() { |
1451 | return Ok(Parsed { value: None, input }); |
1452 | } |
1453 | let value = parse::i64(digits).with_context(|| { |
1454 | err!( |
1455 | "failed to parse {digits:?} as 64-bit signed integer" , |
1456 | digits = escape::Bytes(digits), |
1457 | ) |
1458 | })?; |
1459 | // OK because t::NoUnits permits all possible i64 values. |
1460 | let value = t::NoUnits::new(value).unwrap(); |
1461 | Ok(Parsed { value: Some(value), input }) |
1462 | } |
1463 | |
1464 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
1465 | fn parse_unit_date_designator<'i>( |
1466 | &self, |
1467 | input: &'i [u8], |
1468 | ) -> Result<Parsed<'i, Unit>, Error> { |
1469 | if input.is_empty() { |
1470 | return Err(err!( |
1471 | "expected to find date unit designator suffix \ |
1472 | (Y, M, W or D), but found end of input" , |
1473 | )); |
1474 | } |
1475 | let unit = match input[0] { |
1476 | b'Y' | b'y' => Unit::Year, |
1477 | b'M' | b'm' => Unit::Month, |
1478 | b'W' | b'w' => Unit::Week, |
1479 | b'D' | b'd' => Unit::Day, |
1480 | unknown => { |
1481 | return Err(err!( |
1482 | "expected to find date unit designator suffix \ |
1483 | (Y, M, W or D), but found {found:?} instead" , |
1484 | found = escape::Byte(unknown), |
1485 | )); |
1486 | } |
1487 | }; |
1488 | Ok(Parsed { value: unit, input: &input[1..] }) |
1489 | } |
1490 | |
1491 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
1492 | fn parse_unit_time_designator<'i>( |
1493 | &self, |
1494 | input: &'i [u8], |
1495 | ) -> Result<Parsed<'i, Unit>, Error> { |
1496 | if input.is_empty() { |
1497 | return Err(err!( |
1498 | "expected to find time unit designator suffix \ |
1499 | (H, M or S), but found end of input" , |
1500 | )); |
1501 | } |
1502 | let unit = match input[0] { |
1503 | b'H' | b'h' => Unit::Hour, |
1504 | b'M' | b'm' => Unit::Minute, |
1505 | b'S' | b's' => Unit::Second, |
1506 | unknown => { |
1507 | return Err(err!( |
1508 | "expected to find time unit designator suffix \ |
1509 | (H, M or S), but found {found:?} instead" , |
1510 | found = escape::Byte(unknown), |
1511 | )); |
1512 | } |
1513 | }; |
1514 | Ok(Parsed { value: unit, input: &input[1..] }) |
1515 | } |
1516 | |
1517 | // DurationDesignator ::: one of |
1518 | // P p |
1519 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
1520 | fn parse_duration_designator<'i>( |
1521 | &self, |
1522 | input: &'i [u8], |
1523 | ) -> Result<Parsed<'i, ()>, Error> { |
1524 | if input.is_empty() { |
1525 | return Err(err!( |
1526 | "expected to find duration beginning with 'P' or 'p', \ |
1527 | but found end of input" , |
1528 | )); |
1529 | } |
1530 | if !matches!(input[0], b'P' | b'p' ) { |
1531 | return Err(err!( |
1532 | "expected 'P' or 'p' prefix to begin duration, \ |
1533 | but found {found:?} instead" , |
1534 | found = escape::Byte(input[0]), |
1535 | )); |
1536 | } |
1537 | Ok(Parsed { value: (), input: &input[1..] }) |
1538 | } |
1539 | |
1540 | // TimeDesignator ::: one of |
1541 | // T t |
1542 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
1543 | fn parse_time_designator<'i>(&self, input: &'i [u8]) -> Parsed<'i, bool> { |
1544 | if input.is_empty() || !matches!(input[0], b'T' | b't' ) { |
1545 | return Parsed { value: false, input }; |
1546 | } |
1547 | Parsed { value: true, input: &input[1..] } |
1548 | } |
1549 | |
1550 | // TemporalSign ::: |
1551 | // ASCIISign |
1552 | // <MINUS> |
1553 | // |
1554 | // NOTE: Like with other things with signs, we don't support the Unicode |
1555 | // <MINUS> sign. Just ASCII. |
1556 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
1557 | fn parse_sign<'i>(&self, input: &'i [u8]) -> Parsed<'i, t::Sign> { |
1558 | let Some(sign) = input.get(0).copied() else { |
1559 | return Parsed { value: t::Sign::N::<1>(), input }; |
1560 | }; |
1561 | let sign = if sign == b'+' { |
1562 | t::Sign::N::<1>() |
1563 | } else if sign == b'-' { |
1564 | t::Sign::N::<-1>() |
1565 | } else { |
1566 | return Parsed { value: t::Sign::N::<1>(), input }; |
1567 | }; |
1568 | Parsed { value: sign, input: &input[1..] } |
1569 | } |
1570 | } |
1571 | |
1572 | #[cfg (feature = "alloc" )] |
1573 | #[cfg (test)] |
1574 | mod tests { |
1575 | use super::*; |
1576 | |
1577 | #[test ] |
1578 | fn ok_signed_duration() { |
1579 | let p = |
1580 | |input| SpanParser::new().parse_signed_duration(input).unwrap(); |
1581 | |
1582 | insta::assert_debug_snapshot!(p(b"PT0s" ), @r###" |
1583 | Parsed { |
1584 | value: 0s, |
1585 | input: "", |
1586 | } |
1587 | "### ); |
1588 | insta::assert_debug_snapshot!(p(b"PT0.000000001s" ), @r###" |
1589 | Parsed { |
1590 | value: 1ns, |
1591 | input: "", |
1592 | } |
1593 | "### ); |
1594 | insta::assert_debug_snapshot!(p(b"PT1s" ), @r###" |
1595 | Parsed { |
1596 | value: 1s, |
1597 | input: "", |
1598 | } |
1599 | "### ); |
1600 | insta::assert_debug_snapshot!(p(b"PT59s" ), @r###" |
1601 | Parsed { |
1602 | value: 59s, |
1603 | input: "", |
1604 | } |
1605 | "### ); |
1606 | insta::assert_debug_snapshot!(p(b"PT60s" ), @r#" |
1607 | Parsed { |
1608 | value: 60s, |
1609 | input: "", |
1610 | } |
1611 | "# ); |
1612 | insta::assert_debug_snapshot!(p(b"PT1m" ), @r#" |
1613 | Parsed { |
1614 | value: 60s, |
1615 | input: "", |
1616 | } |
1617 | "# ); |
1618 | insta::assert_debug_snapshot!(p(b"PT1m0.000000001s" ), @r#" |
1619 | Parsed { |
1620 | value: 60s 1ns, |
1621 | input: "", |
1622 | } |
1623 | "# ); |
1624 | insta::assert_debug_snapshot!(p(b"PT1.25m" ), @r#" |
1625 | Parsed { |
1626 | value: 75s, |
1627 | input: "", |
1628 | } |
1629 | "# ); |
1630 | insta::assert_debug_snapshot!(p(b"PT1h" ), @r#" |
1631 | Parsed { |
1632 | value: 3600s, |
1633 | input: "", |
1634 | } |
1635 | "# ); |
1636 | insta::assert_debug_snapshot!(p(b"PT1h0.000000001s" ), @r#" |
1637 | Parsed { |
1638 | value: 3600s 1ns, |
1639 | input: "", |
1640 | } |
1641 | "# ); |
1642 | insta::assert_debug_snapshot!(p(b"PT1.25h" ), @r#" |
1643 | Parsed { |
1644 | value: 4500s, |
1645 | input: "", |
1646 | } |
1647 | "# ); |
1648 | |
1649 | insta::assert_debug_snapshot!(p(b"-PT2562047788015215h30m8.999999999s" ), @r#" |
1650 | Parsed { |
1651 | value: -9223372036854775808s 999999999ns, |
1652 | input: "", |
1653 | } |
1654 | "# ); |
1655 | insta::assert_debug_snapshot!(p(b"PT2562047788015215h30m7.999999999s" ), @r#" |
1656 | Parsed { |
1657 | value: 9223372036854775807s 999999999ns, |
1658 | input: "", |
1659 | } |
1660 | "# ); |
1661 | } |
1662 | |
1663 | #[test ] |
1664 | fn err_signed_duration() { |
1665 | let p = |input| { |
1666 | SpanParser::new().parse_signed_duration(input).unwrap_err() |
1667 | }; |
1668 | |
1669 | insta::assert_snapshot!( |
1670 | p(b"P0d" ), |
1671 | @"failed to parse ISO 8601 duration string into `SignedDuration`: parsing ISO 8601 duration into SignedDuration requires that the duration contain a time component and no components of days or greater" , |
1672 | ); |
1673 | insta::assert_snapshot!( |
1674 | p(b"PT0d" ), |
1675 | @r###"failed to parse ISO 8601 duration string into `SignedDuration`: expected to find time unit designator suffix (H, M or S), but found "d" instead"### , |
1676 | ); |
1677 | insta::assert_snapshot!( |
1678 | p(b"P0dT1s" ), |
1679 | @"failed to parse ISO 8601 duration string into `SignedDuration`: parsing ISO 8601 duration into SignedDuration requires that the duration contain a time component and no components of days or greater" , |
1680 | ); |
1681 | |
1682 | insta::assert_snapshot!( |
1683 | p(b"" ), |
1684 | @"failed to parse ISO 8601 duration string into `SignedDuration`: expected to find duration beginning with 'P' or 'p', but found end of input" , |
1685 | ); |
1686 | insta::assert_snapshot!( |
1687 | p(b"P" ), |
1688 | @"failed to parse ISO 8601 duration string into `SignedDuration`: parsing ISO 8601 duration into SignedDuration requires that the duration contain a time component and no components of days or greater" , |
1689 | ); |
1690 | insta::assert_snapshot!( |
1691 | p(b"PT" ), |
1692 | @"failed to parse ISO 8601 duration string into `SignedDuration`: expected at least one unit of time (hours, minutes or seconds) in ISO 8601 duration when parsing into a `SignedDuration`" , |
1693 | ); |
1694 | insta::assert_snapshot!( |
1695 | p(b"PTs" ), |
1696 | @"failed to parse ISO 8601 duration string into `SignedDuration`: expected at least one unit of time (hours, minutes or seconds) in ISO 8601 duration when parsing into a `SignedDuration`" , |
1697 | ); |
1698 | |
1699 | insta::assert_snapshot!( |
1700 | p(b"PT1s1m" ), |
1701 | @"failed to parse ISO 8601 duration string into `SignedDuration`: found value 1 with unit minute after unit second, but units must be written from largest to smallest (and they can't be repeated)" , |
1702 | ); |
1703 | insta::assert_snapshot!( |
1704 | p(b"PT1s1h" ), |
1705 | @"failed to parse ISO 8601 duration string into `SignedDuration`: found value 1 with unit hour after unit second, but units must be written from largest to smallest (and they can't be repeated)" , |
1706 | ); |
1707 | insta::assert_snapshot!( |
1708 | p(b"PT1m1h" ), |
1709 | @"failed to parse ISO 8601 duration string into `SignedDuration`: found value 1 with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)" , |
1710 | ); |
1711 | |
1712 | insta::assert_snapshot!( |
1713 | p(b"-PT9223372036854775809s" ), |
1714 | @r###"failed to parse ISO 8601 duration string into `SignedDuration`: failed to parse "9223372036854775809" as 64-bit signed integer: number '9223372036854775809' too big to parse into 64-bit integer"### , |
1715 | ); |
1716 | insta::assert_snapshot!( |
1717 | p(b"PT9223372036854775808s" ), |
1718 | @r###"failed to parse ISO 8601 duration string into `SignedDuration`: failed to parse "9223372036854775808" as 64-bit signed integer: number '9223372036854775808' too big to parse into 64-bit integer"### , |
1719 | ); |
1720 | |
1721 | insta::assert_snapshot!( |
1722 | p(b"PT1m9223372036854775807s" ), |
1723 | @"failed to parse ISO 8601 duration string into `SignedDuration`: adding value 9223372036854775807 from unit second overflowed signed duration 1m" , |
1724 | ); |
1725 | insta::assert_snapshot!( |
1726 | p(b"PT2562047788015215.6h" ), |
1727 | @"failed to parse ISO 8601 duration string into `SignedDuration`: adding fractional duration 36m from unit hour to 2562047788015215h overflowed signed duration limits" , |
1728 | ); |
1729 | } |
1730 | |
1731 | #[test ] |
1732 | fn ok_temporal_duration_basic() { |
1733 | let p = |
1734 | |input| SpanParser::new().parse_temporal_duration(input).unwrap(); |
1735 | |
1736 | insta::assert_debug_snapshot!(p(b"P5d" ), @r###" |
1737 | Parsed { |
1738 | value: 5d, |
1739 | input: "", |
1740 | } |
1741 | "### ); |
1742 | insta::assert_debug_snapshot!(p(b"-P5d" ), @r###" |
1743 | Parsed { |
1744 | value: 5d ago, |
1745 | input: "", |
1746 | } |
1747 | "### ); |
1748 | insta::assert_debug_snapshot!(p(b"+P5d" ), @r###" |
1749 | Parsed { |
1750 | value: 5d, |
1751 | input: "", |
1752 | } |
1753 | "### ); |
1754 | insta::assert_debug_snapshot!(p(b"P5DT1s" ), @r###" |
1755 | Parsed { |
1756 | value: 5d 1s, |
1757 | input: "", |
1758 | } |
1759 | "### ); |
1760 | insta::assert_debug_snapshot!(p(b"PT1S" ), @r###" |
1761 | Parsed { |
1762 | value: 1s, |
1763 | input: "", |
1764 | } |
1765 | "### ); |
1766 | insta::assert_debug_snapshot!(p(b"PT0S" ), @r###" |
1767 | Parsed { |
1768 | value: 0s, |
1769 | input: "", |
1770 | } |
1771 | "### ); |
1772 | insta::assert_debug_snapshot!(p(b"P0Y" ), @r###" |
1773 | Parsed { |
1774 | value: 0s, |
1775 | input: "", |
1776 | } |
1777 | "### ); |
1778 | insta::assert_debug_snapshot!(p(b"P1Y1M1W1DT1H1M1S" ), @r###" |
1779 | Parsed { |
1780 | value: 1y 1mo 1w 1d 1h 1m 1s, |
1781 | input: "", |
1782 | } |
1783 | "### ); |
1784 | insta::assert_debug_snapshot!(p(b"P1y1m1w1dT1h1m1s" ), @r###" |
1785 | Parsed { |
1786 | value: 1y 1mo 1w 1d 1h 1m 1s, |
1787 | input: "", |
1788 | } |
1789 | "### ); |
1790 | } |
1791 | |
1792 | #[test ] |
1793 | fn ok_temporal_duration_fractional() { |
1794 | let p = |
1795 | |input| SpanParser::new().parse_temporal_duration(input).unwrap(); |
1796 | |
1797 | insta::assert_debug_snapshot!(p(b"PT0.5h" ), @r###" |
1798 | Parsed { |
1799 | value: 30m, |
1800 | input: "", |
1801 | } |
1802 | "### ); |
1803 | insta::assert_debug_snapshot!(p(b"PT0.123456789h" ), @r###" |
1804 | Parsed { |
1805 | value: 7m 24s 444ms 440µs 400ns, |
1806 | input: "", |
1807 | } |
1808 | "### ); |
1809 | insta::assert_debug_snapshot!(p(b"PT1.123456789h" ), @r###" |
1810 | Parsed { |
1811 | value: 1h 7m 24s 444ms 440µs 400ns, |
1812 | input: "", |
1813 | } |
1814 | "### ); |
1815 | |
1816 | insta::assert_debug_snapshot!(p(b"PT0.5m" ), @r###" |
1817 | Parsed { |
1818 | value: 30s, |
1819 | input: "", |
1820 | } |
1821 | "### ); |
1822 | insta::assert_debug_snapshot!(p(b"PT0.123456789m" ), @r###" |
1823 | Parsed { |
1824 | value: 7s 407ms 407µs 340ns, |
1825 | input: "", |
1826 | } |
1827 | "### ); |
1828 | insta::assert_debug_snapshot!(p(b"PT1.123456789m" ), @r###" |
1829 | Parsed { |
1830 | value: 1m 7s 407ms 407µs 340ns, |
1831 | input: "", |
1832 | } |
1833 | "### ); |
1834 | |
1835 | insta::assert_debug_snapshot!(p(b"PT0.5s" ), @r###" |
1836 | Parsed { |
1837 | value: 500ms, |
1838 | input: "", |
1839 | } |
1840 | "### ); |
1841 | insta::assert_debug_snapshot!(p(b"PT0.123456789s" ), @r###" |
1842 | Parsed { |
1843 | value: 123ms 456µs 789ns, |
1844 | input: "", |
1845 | } |
1846 | "### ); |
1847 | insta::assert_debug_snapshot!(p(b"PT1.123456789s" ), @r###" |
1848 | Parsed { |
1849 | value: 1s 123ms 456µs 789ns, |
1850 | input: "", |
1851 | } |
1852 | "### ); |
1853 | |
1854 | // The tests below all have a whole second value that exceeds the |
1855 | // maximum allowed seconds in a span. But they should still parse |
1856 | // correctly by spilling over into milliseconds, microseconds and |
1857 | // nanoseconds. |
1858 | insta::assert_debug_snapshot!(p(b"PT1902545624836.854775807s" ), @r###" |
1859 | Parsed { |
1860 | value: 631107417600s 631107417600000ms 631107417600000000µs 9223372036854775807ns, |
1861 | input: "", |
1862 | } |
1863 | "### ); |
1864 | insta::assert_debug_snapshot!(p(b"PT175307616h10518456960m640330789636.854775807s" ), @r###" |
1865 | Parsed { |
1866 | value: 175307616h 10518456960m 631107417600s 9223372036854ms 775µs 807ns, |
1867 | input: "", |
1868 | } |
1869 | "### ); |
1870 | insta::assert_debug_snapshot!(p(b"-PT1902545624836.854775807s" ), @r###" |
1871 | Parsed { |
1872 | value: 631107417600s 631107417600000ms 631107417600000000µs 9223372036854775807ns ago, |
1873 | input: "", |
1874 | } |
1875 | "### ); |
1876 | insta::assert_debug_snapshot!(p(b"-PT175307616h10518456960m640330789636.854775807s" ), @r###" |
1877 | Parsed { |
1878 | value: 175307616h 10518456960m 631107417600s 9223372036854ms 775µs 807ns ago, |
1879 | input: "", |
1880 | } |
1881 | "### ); |
1882 | } |
1883 | |
1884 | #[test ] |
1885 | fn ok_temporal_duration_unbalanced() { |
1886 | let p = |
1887 | |input| SpanParser::new().parse_temporal_duration(input).unwrap(); |
1888 | |
1889 | insta::assert_debug_snapshot!( |
1890 | p(b"PT175307616h10518456960m1774446656760s" ), @r###" |
1891 | Parsed { |
1892 | value: 175307616h 10518456960m 631107417600s 631107417600000ms 512231821560000000µs, |
1893 | input: "", |
1894 | } |
1895 | "### ); |
1896 | insta::assert_debug_snapshot!( |
1897 | p(b"Pt843517082H" ), @r###" |
1898 | Parsed { |
1899 | value: 175307616h 10518456960m 631107417600s 631107417600000ms 512231824800000000µs, |
1900 | input: "", |
1901 | } |
1902 | "### ); |
1903 | insta::assert_debug_snapshot!( |
1904 | p(b"Pt843517081H" ), @r###" |
1905 | Parsed { |
1906 | value: 175307616h 10518456960m 631107417600s 631107417600000ms 512231821200000000µs, |
1907 | input: "", |
1908 | } |
1909 | "### ); |
1910 | } |
1911 | |
1912 | #[test ] |
1913 | fn ok_temporal_datetime_basic() { |
1914 | let p = |input| { |
1915 | DateTimeParser::new().parse_temporal_datetime(input).unwrap() |
1916 | }; |
1917 | |
1918 | insta::assert_debug_snapshot!(p(b"2024-06-01" ), @r###" |
1919 | Parsed { |
1920 | value: ParsedDateTime { |
1921 | input: "2024-06-01", |
1922 | date: ParsedDate { |
1923 | input: "2024-06-01", |
1924 | date: 2024-06-01, |
1925 | }, |
1926 | time: None, |
1927 | offset: None, |
1928 | annotations: ParsedAnnotations { |
1929 | input: "", |
1930 | time_zone: None, |
1931 | }, |
1932 | }, |
1933 | input: "", |
1934 | } |
1935 | "### ); |
1936 | insta::assert_debug_snapshot!(p(b"2024-06-01[America/New_York]" ), @r###" |
1937 | Parsed { |
1938 | value: ParsedDateTime { |
1939 | input: "2024-06-01[America/New_York]", |
1940 | date: ParsedDate { |
1941 | input: "2024-06-01", |
1942 | date: 2024-06-01, |
1943 | }, |
1944 | time: None, |
1945 | offset: None, |
1946 | annotations: ParsedAnnotations { |
1947 | input: "[America/New_York]", |
1948 | time_zone: Some( |
1949 | Named { |
1950 | critical: false, |
1951 | name: "America/New_York", |
1952 | }, |
1953 | ), |
1954 | }, |
1955 | }, |
1956 | input: "", |
1957 | } |
1958 | "### ); |
1959 | insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03" ), @r###" |
1960 | Parsed { |
1961 | value: ParsedDateTime { |
1962 | input: "2024-06-01T01:02:03", |
1963 | date: ParsedDate { |
1964 | input: "2024-06-01", |
1965 | date: 2024-06-01, |
1966 | }, |
1967 | time: Some( |
1968 | ParsedTime { |
1969 | input: "01:02:03", |
1970 | time: 01:02:03, |
1971 | extended: true, |
1972 | }, |
1973 | ), |
1974 | offset: None, |
1975 | annotations: ParsedAnnotations { |
1976 | input: "", |
1977 | time_zone: None, |
1978 | }, |
1979 | }, |
1980 | input: "", |
1981 | } |
1982 | "### ); |
1983 | insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03-05" ), @r###" |
1984 | Parsed { |
1985 | value: ParsedDateTime { |
1986 | input: "2024-06-01T01:02:03-05", |
1987 | date: ParsedDate { |
1988 | input: "2024-06-01", |
1989 | date: 2024-06-01, |
1990 | }, |
1991 | time: Some( |
1992 | ParsedTime { |
1993 | input: "01:02:03", |
1994 | time: 01:02:03, |
1995 | extended: true, |
1996 | }, |
1997 | ), |
1998 | offset: Some( |
1999 | ParsedOffset { |
2000 | kind: Numeric( |
2001 | -05, |
2002 | ), |
2003 | }, |
2004 | ), |
2005 | annotations: ParsedAnnotations { |
2006 | input: "", |
2007 | time_zone: None, |
2008 | }, |
2009 | }, |
2010 | input: "", |
2011 | } |
2012 | "### ); |
2013 | insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03-05[America/New_York]" ), @r###" |
2014 | Parsed { |
2015 | value: ParsedDateTime { |
2016 | input: "2024-06-01T01:02:03-05[America/New_York]", |
2017 | date: ParsedDate { |
2018 | input: "2024-06-01", |
2019 | date: 2024-06-01, |
2020 | }, |
2021 | time: Some( |
2022 | ParsedTime { |
2023 | input: "01:02:03", |
2024 | time: 01:02:03, |
2025 | extended: true, |
2026 | }, |
2027 | ), |
2028 | offset: Some( |
2029 | ParsedOffset { |
2030 | kind: Numeric( |
2031 | -05, |
2032 | ), |
2033 | }, |
2034 | ), |
2035 | annotations: ParsedAnnotations { |
2036 | input: "[America/New_York]", |
2037 | time_zone: Some( |
2038 | Named { |
2039 | critical: false, |
2040 | name: "America/New_York", |
2041 | }, |
2042 | ), |
2043 | }, |
2044 | }, |
2045 | input: "", |
2046 | } |
2047 | "### ); |
2048 | insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03Z[America/New_York]" ), @r###" |
2049 | Parsed { |
2050 | value: ParsedDateTime { |
2051 | input: "2024-06-01T01:02:03Z[America/New_York]", |
2052 | date: ParsedDate { |
2053 | input: "2024-06-01", |
2054 | date: 2024-06-01, |
2055 | }, |
2056 | time: Some( |
2057 | ParsedTime { |
2058 | input: "01:02:03", |
2059 | time: 01:02:03, |
2060 | extended: true, |
2061 | }, |
2062 | ), |
2063 | offset: Some( |
2064 | ParsedOffset { |
2065 | kind: Zulu, |
2066 | }, |
2067 | ), |
2068 | annotations: ParsedAnnotations { |
2069 | input: "[America/New_York]", |
2070 | time_zone: Some( |
2071 | Named { |
2072 | critical: false, |
2073 | name: "America/New_York", |
2074 | }, |
2075 | ), |
2076 | }, |
2077 | }, |
2078 | input: "", |
2079 | } |
2080 | "### ); |
2081 | insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03-01[America/New_York]" ), @r###" |
2082 | Parsed { |
2083 | value: ParsedDateTime { |
2084 | input: "2024-06-01T01:02:03-01[America/New_York]", |
2085 | date: ParsedDate { |
2086 | input: "2024-06-01", |
2087 | date: 2024-06-01, |
2088 | }, |
2089 | time: Some( |
2090 | ParsedTime { |
2091 | input: "01:02:03", |
2092 | time: 01:02:03, |
2093 | extended: true, |
2094 | }, |
2095 | ), |
2096 | offset: Some( |
2097 | ParsedOffset { |
2098 | kind: Numeric( |
2099 | -01, |
2100 | ), |
2101 | }, |
2102 | ), |
2103 | annotations: ParsedAnnotations { |
2104 | input: "[America/New_York]", |
2105 | time_zone: Some( |
2106 | Named { |
2107 | critical: false, |
2108 | name: "America/New_York", |
2109 | }, |
2110 | ), |
2111 | }, |
2112 | }, |
2113 | input: "", |
2114 | } |
2115 | "### ); |
2116 | } |
2117 | |
2118 | #[test ] |
2119 | fn ok_temporal_datetime_incomplete() { |
2120 | let p = |input| { |
2121 | DateTimeParser::new().parse_temporal_datetime(input).unwrap() |
2122 | }; |
2123 | |
2124 | insta::assert_debug_snapshot!(p(b"2024-06-01T01" ), @r###" |
2125 | Parsed { |
2126 | value: ParsedDateTime { |
2127 | input: "2024-06-01T01", |
2128 | date: ParsedDate { |
2129 | input: "2024-06-01", |
2130 | date: 2024-06-01, |
2131 | }, |
2132 | time: Some( |
2133 | ParsedTime { |
2134 | input: "01", |
2135 | time: 01:00:00, |
2136 | extended: false, |
2137 | }, |
2138 | ), |
2139 | offset: None, |
2140 | annotations: ParsedAnnotations { |
2141 | input: "", |
2142 | time_zone: None, |
2143 | }, |
2144 | }, |
2145 | input: "", |
2146 | } |
2147 | "### ); |
2148 | insta::assert_debug_snapshot!(p(b"2024-06-01T0102" ), @r###" |
2149 | Parsed { |
2150 | value: ParsedDateTime { |
2151 | input: "2024-06-01T0102", |
2152 | date: ParsedDate { |
2153 | input: "2024-06-01", |
2154 | date: 2024-06-01, |
2155 | }, |
2156 | time: Some( |
2157 | ParsedTime { |
2158 | input: "0102", |
2159 | time: 01:02:00, |
2160 | extended: false, |
2161 | }, |
2162 | ), |
2163 | offset: None, |
2164 | annotations: ParsedAnnotations { |
2165 | input: "", |
2166 | time_zone: None, |
2167 | }, |
2168 | }, |
2169 | input: "", |
2170 | } |
2171 | "### ); |
2172 | insta::assert_debug_snapshot!(p(b"2024-06-01T01:02" ), @r###" |
2173 | Parsed { |
2174 | value: ParsedDateTime { |
2175 | input: "2024-06-01T01:02", |
2176 | date: ParsedDate { |
2177 | input: "2024-06-01", |
2178 | date: 2024-06-01, |
2179 | }, |
2180 | time: Some( |
2181 | ParsedTime { |
2182 | input: "01:02", |
2183 | time: 01:02:00, |
2184 | extended: true, |
2185 | }, |
2186 | ), |
2187 | offset: None, |
2188 | annotations: ParsedAnnotations { |
2189 | input: "", |
2190 | time_zone: None, |
2191 | }, |
2192 | }, |
2193 | input: "", |
2194 | } |
2195 | "### ); |
2196 | } |
2197 | |
2198 | #[test ] |
2199 | fn ok_temporal_datetime_separator() { |
2200 | let p = |input| { |
2201 | DateTimeParser::new().parse_temporal_datetime(input).unwrap() |
2202 | }; |
2203 | |
2204 | insta::assert_debug_snapshot!(p(b"2024-06-01t01:02:03" ), @r###" |
2205 | Parsed { |
2206 | value: ParsedDateTime { |
2207 | input: "2024-06-01t01:02:03", |
2208 | date: ParsedDate { |
2209 | input: "2024-06-01", |
2210 | date: 2024-06-01, |
2211 | }, |
2212 | time: Some( |
2213 | ParsedTime { |
2214 | input: "01:02:03", |
2215 | time: 01:02:03, |
2216 | extended: true, |
2217 | }, |
2218 | ), |
2219 | offset: None, |
2220 | annotations: ParsedAnnotations { |
2221 | input: "", |
2222 | time_zone: None, |
2223 | }, |
2224 | }, |
2225 | input: "", |
2226 | } |
2227 | "### ); |
2228 | insta::assert_debug_snapshot!(p(b"2024-06-01 01:02:03" ), @r###" |
2229 | Parsed { |
2230 | value: ParsedDateTime { |
2231 | input: "2024-06-01 01:02:03", |
2232 | date: ParsedDate { |
2233 | input: "2024-06-01", |
2234 | date: 2024-06-01, |
2235 | }, |
2236 | time: Some( |
2237 | ParsedTime { |
2238 | input: "01:02:03", |
2239 | time: 01:02:03, |
2240 | extended: true, |
2241 | }, |
2242 | ), |
2243 | offset: None, |
2244 | annotations: ParsedAnnotations { |
2245 | input: "", |
2246 | time_zone: None, |
2247 | }, |
2248 | }, |
2249 | input: "", |
2250 | } |
2251 | "### ); |
2252 | } |
2253 | |
2254 | #[test ] |
2255 | fn ok_temporal_time_basic() { |
2256 | let p = |
2257 | |input| DateTimeParser::new().parse_temporal_time(input).unwrap(); |
2258 | |
2259 | insta::assert_debug_snapshot!(p(b"01:02:03" ), @r###" |
2260 | Parsed { |
2261 | value: ParsedTime { |
2262 | input: "01:02:03", |
2263 | time: 01:02:03, |
2264 | extended: true, |
2265 | }, |
2266 | input: "", |
2267 | } |
2268 | "### ); |
2269 | insta::assert_debug_snapshot!(p(b"130113" ), @r###" |
2270 | Parsed { |
2271 | value: ParsedTime { |
2272 | input: "130113", |
2273 | time: 13:01:13, |
2274 | extended: false, |
2275 | }, |
2276 | input: "", |
2277 | } |
2278 | "### ); |
2279 | insta::assert_debug_snapshot!(p(b"T01:02:03" ), @r###" |
2280 | Parsed { |
2281 | value: ParsedTime { |
2282 | input: "01:02:03", |
2283 | time: 01:02:03, |
2284 | extended: true, |
2285 | }, |
2286 | input: "", |
2287 | } |
2288 | "### ); |
2289 | insta::assert_debug_snapshot!(p(b"T010203" ), @r###" |
2290 | Parsed { |
2291 | value: ParsedTime { |
2292 | input: "010203", |
2293 | time: 01:02:03, |
2294 | extended: false, |
2295 | }, |
2296 | input: "", |
2297 | } |
2298 | "### ); |
2299 | } |
2300 | |
2301 | #[test ] |
2302 | fn ok_temporal_time_from_full_datetime() { |
2303 | let p = |
2304 | |input| DateTimeParser::new().parse_temporal_time(input).unwrap(); |
2305 | |
2306 | insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03" ), @r###" |
2307 | Parsed { |
2308 | value: ParsedTime { |
2309 | input: "01:02:03", |
2310 | time: 01:02:03, |
2311 | extended: true, |
2312 | }, |
2313 | input: "", |
2314 | } |
2315 | "### ); |
2316 | insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03.123" ), @r###" |
2317 | Parsed { |
2318 | value: ParsedTime { |
2319 | input: "01:02:03.123", |
2320 | time: 01:02:03.123, |
2321 | extended: true, |
2322 | }, |
2323 | input: "", |
2324 | } |
2325 | "### ); |
2326 | insta::assert_debug_snapshot!(p(b"2024-06-01T01" ), @r###" |
2327 | Parsed { |
2328 | value: ParsedTime { |
2329 | input: "01", |
2330 | time: 01:00:00, |
2331 | extended: false, |
2332 | }, |
2333 | input: "", |
2334 | } |
2335 | "### ); |
2336 | insta::assert_debug_snapshot!(p(b"2024-06-01T0102" ), @r###" |
2337 | Parsed { |
2338 | value: ParsedTime { |
2339 | input: "0102", |
2340 | time: 01:02:00, |
2341 | extended: false, |
2342 | }, |
2343 | input: "", |
2344 | } |
2345 | "### ); |
2346 | insta::assert_debug_snapshot!(p(b"2024-06-01T010203" ), @r###" |
2347 | Parsed { |
2348 | value: ParsedTime { |
2349 | input: "010203", |
2350 | time: 01:02:03, |
2351 | extended: false, |
2352 | }, |
2353 | input: "", |
2354 | } |
2355 | "### ); |
2356 | insta::assert_debug_snapshot!(p(b"2024-06-01T010203-05" ), @r###" |
2357 | Parsed { |
2358 | value: ParsedTime { |
2359 | input: "010203", |
2360 | time: 01:02:03, |
2361 | extended: false, |
2362 | }, |
2363 | input: "", |
2364 | } |
2365 | "### ); |
2366 | insta::assert_debug_snapshot!( |
2367 | p(b"2024-06-01T010203-05[America/New_York]" ), @r###" |
2368 | Parsed { |
2369 | value: ParsedTime { |
2370 | input: "010203", |
2371 | time: 01:02:03, |
2372 | extended: false, |
2373 | }, |
2374 | input: "", |
2375 | } |
2376 | "### ); |
2377 | insta::assert_debug_snapshot!( |
2378 | p(b"2024-06-01T010203[America/New_York]" ), @r###" |
2379 | Parsed { |
2380 | value: ParsedTime { |
2381 | input: "010203", |
2382 | time: 01:02:03, |
2383 | extended: false, |
2384 | }, |
2385 | input: "", |
2386 | } |
2387 | "### ); |
2388 | } |
2389 | |
2390 | #[test ] |
2391 | fn err_temporal_time_ambiguous() { |
2392 | let p = |input| { |
2393 | DateTimeParser::new().parse_temporal_time(input).unwrap_err() |
2394 | }; |
2395 | |
2396 | insta::assert_snapshot!( |
2397 | p(b"010203" ), |
2398 | @r###"parsed time from "010203" is ambiguous with a month-day date"### , |
2399 | ); |
2400 | insta::assert_snapshot!( |
2401 | p(b"130112" ), |
2402 | @r###"parsed time from "130112" is ambiguous with a year-month date"### , |
2403 | ); |
2404 | } |
2405 | |
2406 | #[test ] |
2407 | fn err_temporal_time_missing_time() { |
2408 | let p = |input| { |
2409 | DateTimeParser::new().parse_temporal_time(input).unwrap_err() |
2410 | }; |
2411 | |
2412 | insta::assert_snapshot!( |
2413 | p(b"2024-06-01[America/New_York]" ), |
2414 | @r###"successfully parsed date from "2024-06-01[America/New_York]", but no time component was found"### , |
2415 | ); |
2416 | // 2099 is not a valid time, but 2099-12-01 is a valid date, so this |
2417 | // carves a path where a full datetime parse is OK, but a basic |
2418 | // time-only parse is not. |
2419 | insta::assert_snapshot!( |
2420 | p(b"2099-12-01[America/New_York]" ), |
2421 | @r###"successfully parsed date from "2099-12-01[America/New_York]", but no time component was found"### , |
2422 | ); |
2423 | // Like above, but this time we use an invalid date. As a result, we |
2424 | // get an error reported not on the invalid date, but on how it is an |
2425 | // invalid time. (Because we're asking for a time here.) |
2426 | insta::assert_snapshot!( |
2427 | p(b"2099-13-01[America/New_York]" ), |
2428 | @r###"failed to parse minute in time "2099-13-01[America/New_York]": minute is not valid: parameter 'minute' with value 99 is not in the required range of 0..=59"### , |
2429 | ); |
2430 | } |
2431 | |
2432 | #[test ] |
2433 | fn err_temporal_time_zulu() { |
2434 | let p = |input| { |
2435 | DateTimeParser::new().parse_temporal_time(input).unwrap_err() |
2436 | }; |
2437 | |
2438 | insta::assert_snapshot!( |
2439 | p(b"T00:00:00Z" ), |
2440 | @"cannot parse civil time from string with a Zulu offset, parse as a `Timestamp` and convert to a civil time instead" , |
2441 | ); |
2442 | insta::assert_snapshot!( |
2443 | p(b"00:00:00Z" ), |
2444 | @"cannot parse plain time from string with a Zulu offset, parse as a `Timestamp` and convert to a plain time instead" , |
2445 | ); |
2446 | insta::assert_snapshot!( |
2447 | p(b"000000Z" ), |
2448 | @"cannot parse plain time from string with a Zulu offset, parse as a `Timestamp` and convert to a plain time instead" , |
2449 | ); |
2450 | insta::assert_snapshot!( |
2451 | p(b"2099-12-01T00:00:00Z" ), |
2452 | @"cannot parse plain time from full datetime string with a Zulu offset, parse as a `Timestamp` and convert to a plain time instead" , |
2453 | ); |
2454 | } |
2455 | |
2456 | #[test ] |
2457 | fn ok_date_basic() { |
2458 | let p = |input| DateTimeParser::new().parse_date_spec(input).unwrap(); |
2459 | |
2460 | insta::assert_debug_snapshot!(p(b"2010-03-14" ), @r###" |
2461 | Parsed { |
2462 | value: ParsedDate { |
2463 | input: "2010-03-14", |
2464 | date: 2010-03-14, |
2465 | }, |
2466 | input: "", |
2467 | } |
2468 | "### ); |
2469 | insta::assert_debug_snapshot!(p(b"20100314" ), @r###" |
2470 | Parsed { |
2471 | value: ParsedDate { |
2472 | input: "20100314", |
2473 | date: 2010-03-14, |
2474 | }, |
2475 | input: "", |
2476 | } |
2477 | "### ); |
2478 | insta::assert_debug_snapshot!(p(b"2010-03-14T01:02:03" ), @r###" |
2479 | Parsed { |
2480 | value: ParsedDate { |
2481 | input: "2010-03-14", |
2482 | date: 2010-03-14, |
2483 | }, |
2484 | input: "T01:02:03", |
2485 | } |
2486 | "### ); |
2487 | insta::assert_debug_snapshot!(p(b"-009999-03-14" ), @r###" |
2488 | Parsed { |
2489 | value: ParsedDate { |
2490 | input: "-009999-03-14", |
2491 | date: -009999-03-14, |
2492 | }, |
2493 | input: "", |
2494 | } |
2495 | "### ); |
2496 | insta::assert_debug_snapshot!(p(b"+009999-03-14" ), @r###" |
2497 | Parsed { |
2498 | value: ParsedDate { |
2499 | input: "+009999-03-14", |
2500 | date: 9999-03-14, |
2501 | }, |
2502 | input: "", |
2503 | } |
2504 | "### ); |
2505 | } |
2506 | |
2507 | #[test ] |
2508 | fn err_date_empty() { |
2509 | insta::assert_snapshot!( |
2510 | DateTimeParser::new().parse_date_spec(b"" ).unwrap_err(), |
2511 | @r###"failed to parse year in date "": expected four digit year (or leading sign for six digit year), but found end of input"### , |
2512 | ); |
2513 | } |
2514 | |
2515 | #[test ] |
2516 | fn err_date_year() { |
2517 | insta::assert_snapshot!( |
2518 | DateTimeParser::new().parse_date_spec(b"123" ).unwrap_err(), |
2519 | @r###"failed to parse year in date "123": expected four digit year (or leading sign for six digit year), but found end of input"### , |
2520 | ); |
2521 | insta::assert_snapshot!( |
2522 | DateTimeParser::new().parse_date_spec(b"123a" ).unwrap_err(), |
2523 | @r###"failed to parse year in date "123a": failed to parse "123a" as year (a four digit integer): invalid digit, expected 0-9 but got a"### , |
2524 | ); |
2525 | |
2526 | insta::assert_snapshot!( |
2527 | DateTimeParser::new().parse_date_spec(b"-9999" ).unwrap_err(), |
2528 | @r###"failed to parse year in date "-9999": expected six digit year (because of a leading sign), but found end of input"### , |
2529 | ); |
2530 | insta::assert_snapshot!( |
2531 | DateTimeParser::new().parse_date_spec(b"+9999" ).unwrap_err(), |
2532 | @r###"failed to parse year in date "+9999": expected six digit year (because of a leading sign), but found end of input"### , |
2533 | ); |
2534 | insta::assert_snapshot!( |
2535 | DateTimeParser::new().parse_date_spec(b"-99999" ).unwrap_err(), |
2536 | @r###"failed to parse year in date "-99999": expected six digit year (because of a leading sign), but found end of input"### , |
2537 | ); |
2538 | insta::assert_snapshot!( |
2539 | DateTimeParser::new().parse_date_spec(b"+99999" ).unwrap_err(), |
2540 | @r###"failed to parse year in date "+99999": expected six digit year (because of a leading sign), but found end of input"### , |
2541 | ); |
2542 | insta::assert_snapshot!( |
2543 | DateTimeParser::new().parse_date_spec(b"-99999a" ).unwrap_err(), |
2544 | @r###"failed to parse year in date "-99999a": failed to parse "99999a" as year (a six digit integer): invalid digit, expected 0-9 but got a"### , |
2545 | ); |
2546 | insta::assert_snapshot!( |
2547 | DateTimeParser::new().parse_date_spec(b"+999999" ).unwrap_err(), |
2548 | @r###"failed to parse year in date "+999999": year is not valid: parameter 'year' with value 999999 is not in the required range of -9999..=9999"### , |
2549 | ); |
2550 | insta::assert_snapshot!( |
2551 | DateTimeParser::new().parse_date_spec(b"-010000" ).unwrap_err(), |
2552 | @r###"failed to parse year in date "-010000": year is not valid: parameter 'year' with value 10000 is not in the required range of -9999..=9999"### , |
2553 | ); |
2554 | } |
2555 | |
2556 | #[test ] |
2557 | fn err_date_month() { |
2558 | insta::assert_snapshot!( |
2559 | DateTimeParser::new().parse_date_spec(b"2024-" ).unwrap_err(), |
2560 | @r###"failed to parse month in date "2024-": expected two digit month, but found end of input"### , |
2561 | ); |
2562 | insta::assert_snapshot!( |
2563 | DateTimeParser::new().parse_date_spec(b"2024" ).unwrap_err(), |
2564 | @r###"failed to parse month in date "2024": expected two digit month, but found end of input"### , |
2565 | ); |
2566 | insta::assert_snapshot!( |
2567 | DateTimeParser::new().parse_date_spec(b"2024-13-01" ).unwrap_err(), |
2568 | @r###"failed to parse month in date "2024-13-01": month is not valid: parameter 'month' with value 13 is not in the required range of 1..=12"### , |
2569 | ); |
2570 | insta::assert_snapshot!( |
2571 | DateTimeParser::new().parse_date_spec(b"20241301" ).unwrap_err(), |
2572 | @r###"failed to parse month in date "20241301": month is not valid: parameter 'month' with value 13 is not in the required range of 1..=12"### , |
2573 | ); |
2574 | } |
2575 | |
2576 | #[test ] |
2577 | fn err_date_day() { |
2578 | insta::assert_snapshot!( |
2579 | DateTimeParser::new().parse_date_spec(b"2024-12-" ).unwrap_err(), |
2580 | @r###"failed to parse day in date "2024-12-": expected two digit day, but found end of input"### , |
2581 | ); |
2582 | insta::assert_snapshot!( |
2583 | DateTimeParser::new().parse_date_spec(b"202412" ).unwrap_err(), |
2584 | @r###"failed to parse day in date "202412": expected two digit day, but found end of input"### , |
2585 | ); |
2586 | insta::assert_snapshot!( |
2587 | DateTimeParser::new().parse_date_spec(b"2024-12-40" ).unwrap_err(), |
2588 | @r###"failed to parse day in date "2024-12-40": day is not valid: parameter 'day' with value 40 is not in the required range of 1..=31"### , |
2589 | ); |
2590 | insta::assert_snapshot!( |
2591 | DateTimeParser::new().parse_date_spec(b"2024-11-31" ).unwrap_err(), |
2592 | @r###"date parsed from "2024-11-31" is not valid: parameter 'day' with value 31 is not in the required range of 1..=30"### , |
2593 | ); |
2594 | insta::assert_snapshot!( |
2595 | DateTimeParser::new().parse_date_spec(b"2024-02-30" ).unwrap_err(), |
2596 | @r###"date parsed from "2024-02-30" is not valid: parameter 'day' with value 30 is not in the required range of 1..=29"### , |
2597 | ); |
2598 | insta::assert_snapshot!( |
2599 | DateTimeParser::new().parse_date_spec(b"2023-02-29" ).unwrap_err(), |
2600 | @r###"date parsed from "2023-02-29" is not valid: parameter 'day' with value 29 is not in the required range of 1..=28"### , |
2601 | ); |
2602 | } |
2603 | |
2604 | #[test ] |
2605 | fn err_date_separator() { |
2606 | insta::assert_snapshot!( |
2607 | DateTimeParser::new().parse_date_spec(b"2024-1231" ).unwrap_err(), |
2608 | @r###"failed to parse separator after month: expected '-' separator, but found "3" instead"### , |
2609 | ); |
2610 | insta::assert_snapshot!( |
2611 | DateTimeParser::new().parse_date_spec(b"202412-31" ).unwrap_err(), |
2612 | @"failed to parse separator after month: expected no separator after month since none was found after the year, but found a '-' separator" , |
2613 | ); |
2614 | } |
2615 | |
2616 | #[test ] |
2617 | fn ok_time_basic() { |
2618 | let p = |input| DateTimeParser::new().parse_time_spec(input).unwrap(); |
2619 | |
2620 | insta::assert_debug_snapshot!(p(b"01:02:03" ), @r###" |
2621 | Parsed { |
2622 | value: ParsedTime { |
2623 | input: "01:02:03", |
2624 | time: 01:02:03, |
2625 | extended: true, |
2626 | }, |
2627 | input: "", |
2628 | } |
2629 | "### ); |
2630 | insta::assert_debug_snapshot!(p(b"010203" ), @r###" |
2631 | Parsed { |
2632 | value: ParsedTime { |
2633 | input: "010203", |
2634 | time: 01:02:03, |
2635 | extended: false, |
2636 | }, |
2637 | input: "", |
2638 | } |
2639 | "### ); |
2640 | } |
2641 | |
2642 | #[test ] |
2643 | fn ok_time_fractional() { |
2644 | let p = |input| DateTimeParser::new().parse_time_spec(input).unwrap(); |
2645 | |
2646 | insta::assert_debug_snapshot!(p(b"01:02:03.123456789" ), @r###" |
2647 | Parsed { |
2648 | value: ParsedTime { |
2649 | input: "01:02:03.123456789", |
2650 | time: 01:02:03.123456789, |
2651 | extended: true, |
2652 | }, |
2653 | input: "", |
2654 | } |
2655 | "### ); |
2656 | insta::assert_debug_snapshot!(p(b"010203.123456789" ), @r###" |
2657 | Parsed { |
2658 | value: ParsedTime { |
2659 | input: "010203.123456789", |
2660 | time: 01:02:03.123456789, |
2661 | extended: false, |
2662 | }, |
2663 | input: "", |
2664 | } |
2665 | "### ); |
2666 | |
2667 | insta::assert_debug_snapshot!(p(b"01:02:03.9" ), @r###" |
2668 | Parsed { |
2669 | value: ParsedTime { |
2670 | input: "01:02:03.9", |
2671 | time: 01:02:03.9, |
2672 | extended: true, |
2673 | }, |
2674 | input: "", |
2675 | } |
2676 | "### ); |
2677 | } |
2678 | |
2679 | #[test ] |
2680 | fn ok_time_no_fractional() { |
2681 | let p = |input| DateTimeParser::new().parse_time_spec(input).unwrap(); |
2682 | |
2683 | insta::assert_debug_snapshot!(p(b"01:02.123456789" ), @r###" |
2684 | Parsed { |
2685 | value: ParsedTime { |
2686 | input: "01:02", |
2687 | time: 01:02:00, |
2688 | extended: true, |
2689 | }, |
2690 | input: ".123456789", |
2691 | } |
2692 | "### ); |
2693 | } |
2694 | |
2695 | #[test ] |
2696 | fn ok_time_leap() { |
2697 | let p = |input| DateTimeParser::new().parse_time_spec(input).unwrap(); |
2698 | |
2699 | insta::assert_debug_snapshot!(p(b"01:02:60" ), @r###" |
2700 | Parsed { |
2701 | value: ParsedTime { |
2702 | input: "01:02:60", |
2703 | time: 01:02:59, |
2704 | extended: true, |
2705 | }, |
2706 | input: "", |
2707 | } |
2708 | "### ); |
2709 | } |
2710 | |
2711 | #[test ] |
2712 | fn ok_time_mixed_format() { |
2713 | let p = |input| DateTimeParser::new().parse_time_spec(input).unwrap(); |
2714 | |
2715 | insta::assert_debug_snapshot!(p(b"01:0203" ), @r###" |
2716 | Parsed { |
2717 | value: ParsedTime { |
2718 | input: "01:02", |
2719 | time: 01:02:00, |
2720 | extended: true, |
2721 | }, |
2722 | input: "03", |
2723 | } |
2724 | "### ); |
2725 | insta::assert_debug_snapshot!(p(b"0102:03" ), @r###" |
2726 | Parsed { |
2727 | value: ParsedTime { |
2728 | input: "0102", |
2729 | time: 01:02:00, |
2730 | extended: false, |
2731 | }, |
2732 | input: ":03", |
2733 | } |
2734 | "### ); |
2735 | } |
2736 | |
2737 | #[test ] |
2738 | fn err_time_empty() { |
2739 | insta::assert_snapshot!( |
2740 | DateTimeParser::new().parse_time_spec(b"" ).unwrap_err(), |
2741 | @r###"failed to parse hour in time "": expected two digit hour, but found end of input"### , |
2742 | ); |
2743 | } |
2744 | |
2745 | #[test ] |
2746 | fn err_time_hour() { |
2747 | insta::assert_snapshot!( |
2748 | DateTimeParser::new().parse_time_spec(b"a" ).unwrap_err(), |
2749 | @r###"failed to parse hour in time "a": expected two digit hour, but found end of input"### , |
2750 | ); |
2751 | insta::assert_snapshot!( |
2752 | DateTimeParser::new().parse_time_spec(b"1a" ).unwrap_err(), |
2753 | @r###"failed to parse hour in time "1a": failed to parse "1a" as hour (a two digit integer): invalid digit, expected 0-9 but got a"### , |
2754 | ); |
2755 | insta::assert_snapshot!( |
2756 | DateTimeParser::new().parse_time_spec(b"24" ).unwrap_err(), |
2757 | @r###"failed to parse hour in time "24": hour is not valid: parameter 'hour' with value 24 is not in the required range of 0..=23"### , |
2758 | ); |
2759 | } |
2760 | |
2761 | #[test ] |
2762 | fn err_time_minute() { |
2763 | insta::assert_snapshot!( |
2764 | DateTimeParser::new().parse_time_spec(b"01:" ).unwrap_err(), |
2765 | @r###"failed to parse minute in time "01:": expected two digit minute, but found end of input"### , |
2766 | ); |
2767 | insta::assert_snapshot!( |
2768 | DateTimeParser::new().parse_time_spec(b"01:a" ).unwrap_err(), |
2769 | @r###"failed to parse minute in time "01:a": expected two digit minute, but found end of input"### , |
2770 | ); |
2771 | insta::assert_snapshot!( |
2772 | DateTimeParser::new().parse_time_spec(b"01:1a" ).unwrap_err(), |
2773 | @r###"failed to parse minute in time "01:1a": failed to parse "1a" as minute (a two digit integer): invalid digit, expected 0-9 but got a"### , |
2774 | ); |
2775 | insta::assert_snapshot!( |
2776 | DateTimeParser::new().parse_time_spec(b"01:60" ).unwrap_err(), |
2777 | @r###"failed to parse minute in time "01:60": minute is not valid: parameter 'minute' with value 60 is not in the required range of 0..=59"### , |
2778 | ); |
2779 | } |
2780 | |
2781 | #[test ] |
2782 | fn err_time_second() { |
2783 | insta::assert_snapshot!( |
2784 | DateTimeParser::new().parse_time_spec(b"01:02:" ).unwrap_err(), |
2785 | @r###"failed to parse second in time "01:02:": expected two digit second, but found end of input"### , |
2786 | ); |
2787 | insta::assert_snapshot!( |
2788 | DateTimeParser::new().parse_time_spec(b"01:02:a" ).unwrap_err(), |
2789 | @r###"failed to parse second in time "01:02:a": expected two digit second, but found end of input"### , |
2790 | ); |
2791 | insta::assert_snapshot!( |
2792 | DateTimeParser::new().parse_time_spec(b"01:02:1a" ).unwrap_err(), |
2793 | @r###"failed to parse second in time "01:02:1a": failed to parse "1a" as second (a two digit integer): invalid digit, expected 0-9 but got a"### , |
2794 | ); |
2795 | insta::assert_snapshot!( |
2796 | DateTimeParser::new().parse_time_spec(b"01:02:61" ).unwrap_err(), |
2797 | @r###"failed to parse second in time "01:02:61": second is not valid: parameter 'second' with value 61 is not in the required range of 0..=59"### , |
2798 | ); |
2799 | } |
2800 | |
2801 | #[test ] |
2802 | fn err_time_fractional() { |
2803 | insta::assert_snapshot!( |
2804 | DateTimeParser::new().parse_time_spec(b"01:02:03." ).unwrap_err(), |
2805 | @r###"failed to parse fractional nanoseconds in time "01:02:03.": found decimal after seconds component, but did not find any decimal digits after decimal"### , |
2806 | ); |
2807 | insta::assert_snapshot!( |
2808 | DateTimeParser::new().parse_time_spec(b"01:02:03.a" ).unwrap_err(), |
2809 | @r###"failed to parse fractional nanoseconds in time "01:02:03.a": found decimal after seconds component, but did not find any decimal digits after decimal"### , |
2810 | ); |
2811 | } |
2812 | } |
2813 | |