1 | use core::fmt::Write; |
2 | |
3 | use crate::{ |
4 | civil::Weekday, |
5 | error::{err, ErrorContext}, |
6 | fmt::strtime::{BrokenDownTime, Extension, Flag, Meridiem}, |
7 | tz::Offset, |
8 | util::{ |
9 | escape, parse, |
10 | rangeint::{ri8, RFrom}, |
11 | t::{self, C}, |
12 | }, |
13 | Error, Timestamp, |
14 | }; |
15 | |
16 | // Custom offset value ranges. They're the same as what we use for `Offset`, |
17 | // but always positive since parsing proceeds by getting the absolute value |
18 | // and then applying the sign. |
19 | type ParsedOffsetHours = ri8<0, { t::SpanZoneOffsetHours::MAX }>; |
20 | type ParsedOffsetMinutes = ri8<0, { t::SpanZoneOffsetMinutes::MAX }>; |
21 | type ParsedOffsetSeconds = ri8<0, { t::SpanZoneOffsetSeconds::MAX }>; |
22 | |
23 | pub(super) struct Parser<'f, 'i, 't> { |
24 | pub(super) fmt: &'f [u8], |
25 | pub(super) inp: &'i [u8], |
26 | pub(super) tm: &'t mut BrokenDownTime, |
27 | } |
28 | |
29 | impl<'f, 'i, 't> Parser<'f, 'i, 't> { |
30 | pub(super) fn parse(&mut self) -> Result<(), Error> { |
31 | while !self.fmt.is_empty() { |
32 | if self.f() != b'%' { |
33 | self.parse_literal()?; |
34 | continue; |
35 | } |
36 | if !self.bump_fmt() { |
37 | return Err(err!( |
38 | "invalid format string, expected byte after '%', \ |
39 | but found end of format string" , |
40 | )); |
41 | } |
42 | // We don't check this for `%.` since that currently always |
43 | // must lead to `%.f` which can actually parse the empty string! |
44 | if self.inp.is_empty() && self.f() != b'.' { |
45 | return Err(err!( |
46 | "expected non-empty input for directive % {directive}, \ |
47 | but found end of input" , |
48 | directive = escape::Byte(self.f()), |
49 | )); |
50 | } |
51 | // Parse extensions like padding/case options and padding width. |
52 | let ext = self.parse_extension()?; |
53 | match self.f() { |
54 | b'%' => self.parse_percent().context("%% failed" )?, |
55 | b'A' => self.parse_weekday_full().context("%A failed" )?, |
56 | b'a' => self.parse_weekday_abbrev().context("%a failed" )?, |
57 | b'B' => self.parse_month_name_full().context("%B failed" )?, |
58 | b'b' => self.parse_month_name_abbrev().context("%b failed" )?, |
59 | b'C' => self.parse_century(ext).context("%C failed" )?, |
60 | b'D' => self.parse_american_date().context("%D failed" )?, |
61 | b'd' => self.parse_day(ext).context("%d failed" )?, |
62 | b'e' => self.parse_day(ext).context("%e failed" )?, |
63 | b'F' => self.parse_iso_date().context("%F failed" )?, |
64 | b'f' => self.parse_fractional(ext).context("%f failed" )?, |
65 | b'G' => self.parse_iso_week_year(ext).context("%G failed" )?, |
66 | b'g' => self.parse_iso_week_year2(ext).context("%g failed" )?, |
67 | b'H' => self.parse_hour24(ext).context("%H failed" )?, |
68 | b'h' => self.parse_month_name_abbrev().context("%h failed" )?, |
69 | b'I' => self.parse_hour12(ext).context("%I failed" )?, |
70 | b'j' => self.parse_day_of_year(ext).context("%j failed" )?, |
71 | b'k' => self.parse_hour24(ext).context("%k failed" )?, |
72 | b'l' => self.parse_hour12(ext).context("%l failed" )?, |
73 | b'M' => self.parse_minute(ext).context("%M failed" )?, |
74 | b'm' => self.parse_month(ext).context("%m failed" )?, |
75 | b'n' => self.parse_whitespace().context("%n failed" )?, |
76 | b'P' => self.parse_ampm().context("%P failed" )?, |
77 | b'p' => self.parse_ampm().context("%p failed" )?, |
78 | b'Q' => self.parse_iana_nocolon().context("%Q failed" )?, |
79 | b'R' => self.parse_clock_nosecs().context("%R failed" )?, |
80 | b'S' => self.parse_second(ext).context("%S failed" )?, |
81 | b's' => self.parse_timestamp(ext).context("%s failed" )?, |
82 | b'T' => self.parse_clock_secs().context("%T failed" )?, |
83 | b't' => self.parse_whitespace().context("%t failed" )?, |
84 | b'U' => self.parse_week_sun(ext).context("%U failed" )?, |
85 | b'u' => self.parse_weekday_mon(ext).context("%u failed" )?, |
86 | b'V' => self.parse_week_iso(ext).context("%V failed" )?, |
87 | b'W' => self.parse_week_mon(ext).context("%W failed" )?, |
88 | b'w' => self.parse_weekday_sun(ext).context("%w failed" )?, |
89 | b'Y' => self.parse_year(ext).context("%Y failed" )?, |
90 | b'y' => self.parse_year2(ext).context("%y failed" )?, |
91 | b'z' => self.parse_offset_nocolon().context("%z failed" )?, |
92 | b':' => { |
93 | if !self.bump_fmt() { |
94 | return Err(err!( |
95 | "invalid format string, expected directive \ |
96 | after '%:'" , |
97 | )); |
98 | } |
99 | match self.f() { |
100 | b'Q' => { |
101 | self.parse_iana_colon().context("%:Q failed" )? |
102 | } |
103 | b'z' => { |
104 | self.parse_offset_colon().context("%:z failed" )? |
105 | } |
106 | unk => { |
107 | return Err(err!( |
108 | "found unrecognized directive % {unk} \ |
109 | following %:" , |
110 | unk = escape::Byte(unk), |
111 | )); |
112 | } |
113 | } |
114 | } |
115 | b'Z' => { |
116 | return Err(err!("cannot parse time zone abbreviations" )); |
117 | } |
118 | b'.' => { |
119 | if !self.bump_fmt() { |
120 | return Err(err!( |
121 | "invalid format string, expected directive \ |
122 | after '%.'" , |
123 | )); |
124 | } |
125 | // Skip over any precision settings that might be here. |
126 | // This is a specific special format supported by `%.f`. |
127 | let (width, fmt) = Extension::parse_width(self.fmt)?; |
128 | let ext = Extension { width, ..ext }; |
129 | self.fmt = fmt; |
130 | match self.f() { |
131 | b'f' => self |
132 | .parse_dot_fractional(ext) |
133 | .context("%.f failed" )?, |
134 | unk => { |
135 | return Err(err!( |
136 | "found unrecognized directive % {unk} \ |
137 | following %." , |
138 | unk = escape::Byte(unk), |
139 | )); |
140 | } |
141 | } |
142 | } |
143 | unk => { |
144 | return Err(err!( |
145 | "found unrecognized directive % {unk}" , |
146 | unk = escape::Byte(unk), |
147 | )); |
148 | } |
149 | } |
150 | } |
151 | Ok(()) |
152 | } |
153 | |
154 | /// Returns the byte at the current position of the format string. |
155 | /// |
156 | /// # Panics |
157 | /// |
158 | /// This panics when the entire format string has been consumed. |
159 | fn f(&self) -> u8 { |
160 | self.fmt[0] |
161 | } |
162 | |
163 | /// Returns the byte at the current position of the input string. |
164 | /// |
165 | /// # Panics |
166 | /// |
167 | /// This panics when the entire input string has been consumed. |
168 | fn i(&self) -> u8 { |
169 | self.inp[0] |
170 | } |
171 | |
172 | /// Bumps the position of the format string. |
173 | /// |
174 | /// This returns true in precisely the cases where `self.f()` will not |
175 | /// panic. i.e., When the end of the format string hasn't been reached yet. |
176 | fn bump_fmt(&mut self) -> bool { |
177 | self.fmt = &self.fmt[1..]; |
178 | !self.fmt.is_empty() |
179 | } |
180 | |
181 | /// Bumps the position of the input string. |
182 | /// |
183 | /// This returns true in precisely the cases where `self.i()` will not |
184 | /// panic. i.e., When the end of the input string hasn't been reached yet. |
185 | fn bump_input(&mut self) -> bool { |
186 | self.inp = &self.inp[1..]; |
187 | !self.inp.is_empty() |
188 | } |
189 | |
190 | /// Parses optional extensions before a specifier directive. That is, right |
191 | /// after the `%`. If any extensions are parsed, the parser is bumped |
192 | /// to the next byte. (If no next byte exists, then an error is returned.) |
193 | fn parse_extension(&mut self) -> Result<Extension, Error> { |
194 | let (flag, fmt) = Extension::parse_flag(self.fmt)?; |
195 | let (width, fmt) = Extension::parse_width(fmt)?; |
196 | self.fmt = fmt; |
197 | Ok(Extension { flag, width }) |
198 | } |
199 | |
200 | // We write out a parsing routine for each directive below. Each parsing |
201 | // routine assumes that the parser is positioned immediately after the |
202 | // `%` for the current directive, and that there is at least one unconsumed |
203 | // byte in the input. |
204 | |
205 | /// Parses a literal from the input that matches the current byte in the |
206 | /// format string. |
207 | /// |
208 | /// This may consume multiple bytes from the input, for example, a single |
209 | /// whitespace byte in the format string can match zero or more whitespace |
210 | /// in the input. |
211 | fn parse_literal(&mut self) -> Result<(), Error> { |
212 | if self.f().is_ascii_whitespace() { |
213 | if !self.inp.is_empty() { |
214 | while self.i().is_ascii_whitespace() && self.bump_input() {} |
215 | } |
216 | } else if self.inp.is_empty() { |
217 | return Err(err!( |
218 | "expected to match literal byte {byte:?} from \ |
219 | format string, but found end of input" , |
220 | byte = escape::Byte(self.fmt[0]), |
221 | )); |
222 | } else if self.f() != self.i() { |
223 | return Err(err!( |
224 | "expected to match literal byte {expect:?} from \ |
225 | format string, but found byte {found:?} in input" , |
226 | expect = escape::Byte(self.f()), |
227 | found = escape::Byte(self.i()), |
228 | )); |
229 | } else { |
230 | self.bump_input(); |
231 | } |
232 | self.bump_fmt(); |
233 | Ok(()) |
234 | } |
235 | |
236 | /// Parses an arbitrary (zero or more) amount ASCII whitespace. |
237 | /// |
238 | /// This is for `%n` and `%t`. |
239 | fn parse_whitespace(&mut self) -> Result<(), Error> { |
240 | if !self.inp.is_empty() { |
241 | while self.i().is_ascii_whitespace() && self.bump_input() {} |
242 | } |
243 | self.bump_fmt(); |
244 | Ok(()) |
245 | } |
246 | |
247 | /// Parses a literal '%' from the input. |
248 | fn parse_percent(&mut self) -> Result<(), Error> { |
249 | if self.i() != b'%' { |
250 | return Err(err!( |
251 | "expected '%' due to '%%' in format string, \ |
252 | but found {byte:?} in input" , |
253 | byte = escape::Byte(self.inp[0]), |
254 | )); |
255 | } |
256 | self.bump_fmt(); |
257 | self.bump_input(); |
258 | Ok(()) |
259 | } |
260 | |
261 | /// Parses `%D`, which is equivalent to `%m/%d/%y`. |
262 | fn parse_american_date(&mut self) -> Result<(), Error> { |
263 | let mut p = Parser { fmt: b"%m/%d/%y" , inp: self.inp, tm: self.tm }; |
264 | p.parse()?; |
265 | self.inp = p.inp; |
266 | self.bump_fmt(); |
267 | Ok(()) |
268 | } |
269 | |
270 | /// Parse `%p`, which indicates whether the time is AM or PM. |
271 | /// |
272 | /// This is generally only useful with `%I`. If, say, `%H` is used, then |
273 | /// the AM/PM moniker will be validated, but it doesn't actually influence |
274 | /// the clock time. |
275 | fn parse_ampm(&mut self) -> Result<(), Error> { |
276 | let (index, inp) = parse_ampm(self.inp)?; |
277 | self.inp = inp; |
278 | |
279 | self.tm.meridiem = Some(match index { |
280 | 0 => Meridiem::AM, |
281 | 1 => Meridiem::PM, |
282 | // OK because 0 <= index <= 1. |
283 | index => unreachable!("unknown AM/PM index {index}" ), |
284 | }); |
285 | self.bump_fmt(); |
286 | Ok(()) |
287 | } |
288 | |
289 | /// Parses `%T`, which is equivalent to `%H:%M:%S`. |
290 | fn parse_clock_secs(&mut self) -> Result<(), Error> { |
291 | let mut p = Parser { fmt: b"%H:%M:%S" , inp: self.inp, tm: self.tm }; |
292 | p.parse()?; |
293 | self.inp = p.inp; |
294 | self.bump_fmt(); |
295 | Ok(()) |
296 | } |
297 | |
298 | /// Parses `%R`, which is equivalent to `%H:%M`. |
299 | fn parse_clock_nosecs(&mut self) -> Result<(), Error> { |
300 | let mut p = Parser { fmt: b"%H:%M" , inp: self.inp, tm: self.tm }; |
301 | p.parse()?; |
302 | self.inp = p.inp; |
303 | self.bump_fmt(); |
304 | Ok(()) |
305 | } |
306 | |
307 | /// Parses `%d` and `%e`, which is equivalent to the day of the month. |
308 | /// |
309 | /// We merely require that it is in the range 1-31 here. |
310 | fn parse_day(&mut self, ext: Extension) -> Result<(), Error> { |
311 | let (day, inp) = ext |
312 | .parse_number(2, Flag::PadZero, self.inp) |
313 | .context("failed to parse day" )?; |
314 | self.inp = inp; |
315 | |
316 | let day = |
317 | t::Day::try_new("day" , day).context("day number is invalid" )?; |
318 | self.tm.day = Some(day); |
319 | self.bump_fmt(); |
320 | Ok(()) |
321 | } |
322 | |
323 | /// Parses `%j`, which is equivalent to the day of the year. |
324 | /// |
325 | /// We merely require that it is in the range 1-366 here. |
326 | fn parse_day_of_year(&mut self, ext: Extension) -> Result<(), Error> { |
327 | let (day, inp) = ext |
328 | .parse_number(3, Flag::PadZero, self.inp) |
329 | .context("failed to parse day of year" )?; |
330 | self.inp = inp; |
331 | |
332 | let day = t::DayOfYear::try_new("day-of-year" , day) |
333 | .context("day of year number is invalid" )?; |
334 | self.tm.day_of_year = Some(day); |
335 | self.bump_fmt(); |
336 | Ok(()) |
337 | } |
338 | |
339 | /// Parses `%H`, which is equivalent to the hour. |
340 | fn parse_hour24(&mut self, ext: Extension) -> Result<(), Error> { |
341 | let (hour, inp) = ext |
342 | .parse_number(2, Flag::PadZero, self.inp) |
343 | .context("failed to parse hour" )?; |
344 | self.inp = inp; |
345 | |
346 | let hour = t::Hour::try_new("hour" , hour) |
347 | .context("hour number is invalid" )?; |
348 | self.tm.hour = Some(hour); |
349 | self.bump_fmt(); |
350 | Ok(()) |
351 | } |
352 | |
353 | /// Parses `%I`, which is equivalent to the hour on a 12-hour clock. |
354 | fn parse_hour12(&mut self, ext: Extension) -> Result<(), Error> { |
355 | type Hour12 = ri8<1, 12>; |
356 | |
357 | let (hour, inp) = ext |
358 | .parse_number(2, Flag::PadZero, self.inp) |
359 | .context("failed to parse hour" )?; |
360 | self.inp = inp; |
361 | |
362 | let hour = |
363 | Hour12::try_new("hour" , hour).context("hour number is invalid" )?; |
364 | self.tm.hour = Some(t::Hour::rfrom(hour)); |
365 | self.bump_fmt(); |
366 | Ok(()) |
367 | } |
368 | |
369 | /// Parses `%F`, which is equivalent to `%Y-%m-%d`. |
370 | fn parse_iso_date(&mut self) -> Result<(), Error> { |
371 | let mut p = Parser { fmt: b"%Y-%m-%d" , inp: self.inp, tm: self.tm }; |
372 | p.parse()?; |
373 | self.inp = p.inp; |
374 | self.bump_fmt(); |
375 | Ok(()) |
376 | } |
377 | |
378 | /// Parses `%M`, which is equivalent to the minute. |
379 | fn parse_minute(&mut self, ext: Extension) -> Result<(), Error> { |
380 | let (minute, inp) = ext |
381 | .parse_number(2, Flag::PadZero, self.inp) |
382 | .context("failed to parse minute" )?; |
383 | self.inp = inp; |
384 | |
385 | let minute = t::Minute::try_new("minute" , minute) |
386 | .context("minute number is invalid" )?; |
387 | self.tm.minute = Some(minute); |
388 | self.bump_fmt(); |
389 | Ok(()) |
390 | } |
391 | |
392 | /// Parse `%Q`, which is the IANA time zone identifier or an offset without |
393 | /// colons. |
394 | fn parse_iana_nocolon(&mut self) -> Result<(), Error> { |
395 | #[cfg (not(feature = "alloc" ))] |
396 | { |
397 | Err(err!( |
398 | "cannot parse `%Q` without Jiff's `alloc` feature enabled" |
399 | )) |
400 | } |
401 | #[cfg (feature = "alloc" )] |
402 | { |
403 | use alloc::string::ToString; |
404 | |
405 | if !self.inp.is_empty() && matches!(self.inp[0], b'+' | b'-' ) { |
406 | return self.parse_offset_nocolon(); |
407 | } |
408 | let (iana, inp) = parse_iana(self.inp)?; |
409 | self.inp = inp; |
410 | self.tm.iana = Some(iana.to_string()); |
411 | self.bump_fmt(); |
412 | Ok(()) |
413 | } |
414 | } |
415 | |
416 | /// Parse `%:Q`, which is the IANA time zone identifier or an offset with |
417 | /// colons. |
418 | fn parse_iana_colon(&mut self) -> Result<(), Error> { |
419 | #[cfg (not(feature = "alloc" ))] |
420 | { |
421 | Err(err!( |
422 | "cannot parse `%:Q` without Jiff's `alloc` feature enabled" |
423 | )) |
424 | } |
425 | #[cfg (feature = "alloc" )] |
426 | { |
427 | use alloc::string::ToString; |
428 | |
429 | if !self.inp.is_empty() && matches!(self.inp[0], b'+' | b'-' ) { |
430 | return self.parse_offset_colon(); |
431 | } |
432 | let (iana, inp) = parse_iana(self.inp)?; |
433 | self.inp = inp; |
434 | self.tm.iana = Some(iana.to_string()); |
435 | self.bump_fmt(); |
436 | Ok(()) |
437 | } |
438 | } |
439 | |
440 | /// Parse `%z`, which is a time zone offset without colons. |
441 | fn parse_offset_nocolon(&mut self) -> Result<(), Error> { |
442 | let (sign, inp) = parse_required_sign(self.inp) |
443 | .context("sign is required for time zone offset" )?; |
444 | let (hhmm, inp) = parse::split(inp, 4).ok_or_else(|| { |
445 | err!( |
446 | "expected at least 4 digits for time zone offset \ |
447 | after sign, but found only {len} bytes remaining" , |
448 | len = inp.len(), |
449 | ) |
450 | })?; |
451 | |
452 | let hh = parse::i64(&hhmm[0..2]).with_context(|| { |
453 | err!( |
454 | "failed to parse hours from time zone offset {hhmm}" , |
455 | hhmm = escape::Bytes(hhmm) |
456 | ) |
457 | })?; |
458 | let hh = ParsedOffsetHours::try_new("zone-offset-hours" , hh) |
459 | .context("time zone offset hours are not valid" )?; |
460 | let hh = t::SpanZoneOffset::rfrom(hh); |
461 | |
462 | let mm = parse::i64(&hhmm[2..4]).with_context(|| { |
463 | err!( |
464 | "failed to parse minutes from time zone offset {hhmm}" , |
465 | hhmm = escape::Bytes(hhmm) |
466 | ) |
467 | })?; |
468 | let mm = ParsedOffsetMinutes::try_new("zone-offset-minutes" , mm) |
469 | .context("time zone offset minutes are not valid" )?; |
470 | let mm = t::SpanZoneOffset::rfrom(mm); |
471 | |
472 | let (ss, inp) = if inp.len() < 2 |
473 | || !inp[..2].iter().all(u8::is_ascii_digit) |
474 | { |
475 | (t::SpanZoneOffset::N::<0>(), inp) |
476 | } else { |
477 | let (ss, inp) = parse::split(inp, 2).unwrap(); |
478 | let ss = parse::i64(ss).with_context(|| { |
479 | err!( |
480 | "failed to parse seconds from time zone offset {ss}" , |
481 | ss = escape::Bytes(ss) |
482 | ) |
483 | })?; |
484 | let ss = ParsedOffsetSeconds::try_new("zone-offset-seconds" , ss) |
485 | .context("time zone offset seconds are not valid" )?; |
486 | if inp.starts_with(b"." ) { |
487 | // I suppose we could parse them and then round, but meh... |
488 | // (At time of writing, the precision of tz::Offset is |
489 | // seconds. If that improves to nanoseconds, then yes, let's |
490 | // parse fractional seconds here.) |
491 | return Err(err!( |
492 | "parsing fractional seconds in time zone offset \ |
493 | is not supported" , |
494 | )); |
495 | } |
496 | (t::SpanZoneOffset::rfrom(ss), inp) |
497 | }; |
498 | |
499 | let seconds = hh * C(3_600) + mm * C(60) + ss; |
500 | let offset = Offset::from_seconds_ranged(seconds * sign); |
501 | self.tm.offset = Some(offset); |
502 | self.inp = inp; |
503 | self.bump_fmt(); |
504 | |
505 | Ok(()) |
506 | } |
507 | |
508 | /// Parse `%:z`, which is a time zone offset with colons. |
509 | fn parse_offset_colon(&mut self) -> Result<(), Error> { |
510 | let (sign, inp) = parse_required_sign(self.inp) |
511 | .context("sign is required for time zone offset" )?; |
512 | let (hhmm, inp) = parse::split(inp, 5).ok_or_else(|| { |
513 | err!( |
514 | "expected at least HH:MM digits for time zone offset \ |
515 | after sign, but found only {len} bytes remaining" , |
516 | len = inp.len(), |
517 | ) |
518 | })?; |
519 | if hhmm[2] != b':' { |
520 | return Err(err!( |
521 | "expected colon after between HH and MM in time zone \ |
522 | offset, but found {found:?} instead" , |
523 | found = escape::Byte(hhmm[2]), |
524 | )); |
525 | } |
526 | |
527 | let hh = parse::i64(&hhmm[0..2]).with_context(|| { |
528 | err!( |
529 | "failed to parse hours from time zone offset {hhmm}" , |
530 | hhmm = escape::Bytes(hhmm) |
531 | ) |
532 | })?; |
533 | let hh = ParsedOffsetHours::try_new("zone-offset-hours" , hh) |
534 | .context("time zone offset hours are not valid" )?; |
535 | let hh = t::SpanZoneOffset::rfrom(hh); |
536 | |
537 | let mm = parse::i64(&hhmm[3..5]).with_context(|| { |
538 | err!( |
539 | "failed to parse minutes from time zone offset {hhmm}" , |
540 | hhmm = escape::Bytes(hhmm) |
541 | ) |
542 | })?; |
543 | let mm = ParsedOffsetMinutes::try_new("zone-offset-minutes" , mm) |
544 | .context("time zone offset minutes are not valid" )?; |
545 | let mm = t::SpanZoneOffset::rfrom(mm); |
546 | |
547 | let (ss, inp) = if inp.len() < 3 |
548 | || inp[0] != b':' |
549 | || !inp[1..3].iter().all(u8::is_ascii_digit) |
550 | { |
551 | (t::SpanZoneOffset::N::<0>(), inp) |
552 | } else { |
553 | let (ss, inp) = parse::split(&inp[1..], 2).unwrap(); |
554 | let ss = parse::i64(ss).with_context(|| { |
555 | err!( |
556 | "failed to parse seconds from time zone offset {ss}" , |
557 | ss = escape::Bytes(ss) |
558 | ) |
559 | })?; |
560 | let ss = ParsedOffsetSeconds::try_new("zone-offset-seconds" , ss) |
561 | .context("time zone offset seconds are not valid" )?; |
562 | if inp.starts_with(b"." ) { |
563 | // I suppose we could parse them and then round, but meh... |
564 | // (At time of writing, the precision of tz::Offset is |
565 | // seconds. If that improves to nanoseconds, then yes, let's |
566 | // parse fractional seconds here.) |
567 | return Err(err!( |
568 | "parsing fractional seconds in time zone offset \ |
569 | is not supported" , |
570 | )); |
571 | } |
572 | (t::SpanZoneOffset::rfrom(ss), inp) |
573 | }; |
574 | |
575 | let seconds = hh * C(3_600) + mm * C(60) + ss; |
576 | let offset = Offset::from_seconds_ranged(seconds * sign); |
577 | self.tm.offset = Some(offset); |
578 | self.inp = inp; |
579 | self.bump_fmt(); |
580 | |
581 | Ok(()) |
582 | } |
583 | |
584 | /// Parses `%S`, which is equivalent to the second. |
585 | fn parse_second(&mut self, ext: Extension) -> Result<(), Error> { |
586 | let (mut second, inp) = ext |
587 | .parse_number(2, Flag::PadZero, self.inp) |
588 | .context("failed to parse second" )?; |
589 | self.inp = inp; |
590 | |
591 | // As with other parses in Jiff, and like Temporal, |
592 | // we constrain `60` seconds to `59` because we don't |
593 | // support leap seconds. |
594 | if second == 60 { |
595 | second = 59; |
596 | } |
597 | let second = t::Second::try_new("second" , second) |
598 | .context("second number is invalid" )?; |
599 | self.tm.second = Some(second); |
600 | self.bump_fmt(); |
601 | Ok(()) |
602 | } |
603 | |
604 | /// Parses `%s`, which is equivalent to a Unix timestamp. |
605 | fn parse_timestamp(&mut self, ext: Extension) -> Result<(), Error> { |
606 | let (sign, inp) = parse_optional_sign(self.inp); |
607 | let (timestamp, inp) = ext |
608 | // 19 comes from `i64::MAX.to_string().len()`. |
609 | .parse_number(19, Flag::PadSpace, inp) |
610 | .context("failed to parse Unix timestamp (in seconds)" )?; |
611 | // I believe this error case is actually impossible. Since `timestamp` |
612 | // is guaranteed to be positive, and negating any positive `i64` will |
613 | // always result in a valid `i64`. |
614 | let timestamp = timestamp.checked_mul(sign).ok_or_else(|| { |
615 | err!( |
616 | "parsed Unix timestamp ` {timestamp}` with a \ |
617 | leading `-` sign, which causes overflow" , |
618 | ) |
619 | })?; |
620 | let timestamp = |
621 | Timestamp::from_second(timestamp).with_context(|| { |
622 | err!( |
623 | "parsed Unix timestamp ` {timestamp}`, \ |
624 | but out of range of valid Jiff `Timestamp`" , |
625 | ) |
626 | })?; |
627 | self.inp = inp; |
628 | |
629 | // This is basically just repeating the |
630 | // `From<Timestamp> for BrokenDownTime` |
631 | // trait implementation. |
632 | let dt = Offset::UTC.to_datetime(timestamp); |
633 | let (d, t) = (dt.date(), dt.time()); |
634 | self.tm.offset = Some(Offset::UTC); |
635 | self.tm.year = Some(d.year_ranged()); |
636 | self.tm.month = Some(d.month_ranged()); |
637 | self.tm.day = Some(d.day_ranged()); |
638 | self.tm.hour = Some(t.hour_ranged()); |
639 | self.tm.minute = Some(t.minute_ranged()); |
640 | self.tm.second = Some(t.second_ranged()); |
641 | self.tm.subsec = Some(t.subsec_nanosecond_ranged()); |
642 | self.tm.meridiem = Some(Meridiem::from(t)); |
643 | |
644 | self.bump_fmt(); |
645 | Ok(()) |
646 | } |
647 | |
648 | /// Parses `%f`, which is equivalent to a fractional second up to |
649 | /// nanosecond precision. This must always parse at least one decimal digit |
650 | /// and does not parse any leading dot. |
651 | /// |
652 | /// At present, we don't use any flags/width/precision settings to |
653 | /// influence parsing. That is, `%3f` will parse the fractional component |
654 | /// in `0.123456789`. |
655 | fn parse_fractional(&mut self, _ext: Extension) -> Result<(), Error> { |
656 | let mkdigits = parse::slicer(self.inp); |
657 | while mkdigits(self.inp).len() < 9 |
658 | && self.inp.first().map_or(false, u8::is_ascii_digit) |
659 | { |
660 | self.inp = &self.inp[1..]; |
661 | } |
662 | let digits = mkdigits(self.inp); |
663 | if digits.is_empty() { |
664 | return Err(err!( |
665 | "expected at least one fractional decimal digit, \ |
666 | but did not find any" , |
667 | )); |
668 | } |
669 | // I believe this error can never happen, since we know we have no more |
670 | // than 9 ASCII digits. Any sequence of 9 ASCII digits can be parsed |
671 | // into an `i64`. |
672 | let nanoseconds = parse::fraction(digits, 9).map_err(|err| { |
673 | err!( |
674 | "failed to parse {digits:?} as fractional second component \ |
675 | (up to 9 digits, nanosecond precision): {err}" , |
676 | digits = escape::Bytes(digits), |
677 | ) |
678 | })?; |
679 | // I believe this is also impossible to fail, since the maximal |
680 | // fractional nanosecond is 999_999_999, and which also corresponds |
681 | // to the maximal expressible number with 9 ASCII digits. So every |
682 | // possible expressible value here is in range. |
683 | let nanoseconds = |
684 | t::SubsecNanosecond::try_new("nanoseconds" , nanoseconds).map_err( |
685 | |err| err!("fractional nanoseconds are not valid: {err}" ), |
686 | )?; |
687 | self.tm.subsec = Some(nanoseconds); |
688 | self.bump_fmt(); |
689 | Ok(()) |
690 | } |
691 | |
692 | /// Parses `%f`, which is equivalent to a dot followed by a fractional |
693 | /// second up to nanosecond precision. Note that if there is no leading |
694 | /// dot, then this successfully parses the empty string. |
695 | fn parse_dot_fractional(&mut self, ext: Extension) -> Result<(), Error> { |
696 | if !self.inp.starts_with(b"." ) { |
697 | self.bump_fmt(); |
698 | return Ok(()); |
699 | } |
700 | self.inp = &self.inp[1..]; |
701 | self.parse_fractional(ext) |
702 | } |
703 | |
704 | /// Parses `%m`, which is equivalent to the month. |
705 | fn parse_month(&mut self, ext: Extension) -> Result<(), Error> { |
706 | let (month, inp) = ext |
707 | .parse_number(2, Flag::PadZero, self.inp) |
708 | .context("failed to parse month" )?; |
709 | self.inp = inp; |
710 | |
711 | let month = t::Month::try_new("month" , month) |
712 | .context("month number is invalid" )?; |
713 | self.tm.month = Some(month); |
714 | self.bump_fmt(); |
715 | Ok(()) |
716 | } |
717 | |
718 | /// Parse `%b` or `%h`, which is an abbreviated month name. |
719 | fn parse_month_name_abbrev(&mut self) -> Result<(), Error> { |
720 | let (index, inp) = parse_month_name_abbrev(self.inp)?; |
721 | self.inp = inp; |
722 | |
723 | // Both are OK because 0 <= index <= 11. |
724 | let index = i8::try_from(index).unwrap(); |
725 | self.tm.month = Some(t::Month::new(index + 1).unwrap()); |
726 | self.bump_fmt(); |
727 | Ok(()) |
728 | } |
729 | |
730 | /// Parse `%B`, which is a full month name. |
731 | fn parse_month_name_full(&mut self) -> Result<(), Error> { |
732 | static CHOICES: &'static [&'static [u8]] = &[ |
733 | b"January" , |
734 | b"February" , |
735 | b"March" , |
736 | b"April" , |
737 | b"May" , |
738 | b"June" , |
739 | b"July" , |
740 | b"August" , |
741 | b"September" , |
742 | b"October" , |
743 | b"November" , |
744 | b"December" , |
745 | ]; |
746 | |
747 | let (index, inp) = parse_choice(self.inp, CHOICES) |
748 | .context("unrecognized month name" )?; |
749 | self.inp = inp; |
750 | |
751 | // Both are OK because 0 <= index <= 11. |
752 | let index = i8::try_from(index).unwrap(); |
753 | self.tm.month = Some(t::Month::new(index + 1).unwrap()); |
754 | self.bump_fmt(); |
755 | Ok(()) |
756 | } |
757 | |
758 | /// Parse `%a`, which is an abbreviated weekday. |
759 | fn parse_weekday_abbrev(&mut self) -> Result<(), Error> { |
760 | let (index, inp) = parse_weekday_abbrev(self.inp)?; |
761 | self.inp = inp; |
762 | |
763 | // Both are OK because 0 <= index <= 6. |
764 | let index = i8::try_from(index).unwrap(); |
765 | self.tm.weekday = |
766 | Some(Weekday::from_sunday_zero_offset(index).unwrap()); |
767 | self.bump_fmt(); |
768 | Ok(()) |
769 | } |
770 | |
771 | /// Parse `%A`, which is a full weekday name. |
772 | fn parse_weekday_full(&mut self) -> Result<(), Error> { |
773 | static CHOICES: &'static [&'static [u8]] = &[ |
774 | b"Sunday" , |
775 | b"Monday" , |
776 | b"Tuesday" , |
777 | b"Wednesday" , |
778 | b"Thursday" , |
779 | b"Friday" , |
780 | b"Saturday" , |
781 | ]; |
782 | |
783 | let (index, inp) = parse_choice(self.inp, CHOICES) |
784 | .context("unrecognized weekday abbreviation" )?; |
785 | self.inp = inp; |
786 | |
787 | // Both are OK because 0 <= index <= 6. |
788 | let index = i8::try_from(index).unwrap(); |
789 | self.tm.weekday = |
790 | Some(Weekday::from_sunday_zero_offset(index).unwrap()); |
791 | self.bump_fmt(); |
792 | Ok(()) |
793 | } |
794 | |
795 | /// Parse `%u`, which is a weekday number with Monday being `1` and |
796 | /// Sunday being `7`. |
797 | fn parse_weekday_mon(&mut self, ext: Extension) -> Result<(), Error> { |
798 | let (weekday, inp) = ext |
799 | .parse_number(1, Flag::NoPad, self.inp) |
800 | .context("failed to parse weekday number" )?; |
801 | self.inp = inp; |
802 | |
803 | let weekday = i8::try_from(weekday).map_err(|_| { |
804 | err!("parsed weekday number ` {weekday}` is invalid" ) |
805 | })?; |
806 | let weekday = Weekday::from_monday_one_offset(weekday) |
807 | .context("weekday number is invalid" )?; |
808 | self.tm.weekday = Some(weekday); |
809 | self.bump_fmt(); |
810 | Ok(()) |
811 | } |
812 | |
813 | /// Parse `%w`, which is a weekday number with Sunday being `0`. |
814 | fn parse_weekday_sun(&mut self, ext: Extension) -> Result<(), Error> { |
815 | let (weekday, inp) = ext |
816 | .parse_number(1, Flag::NoPad, self.inp) |
817 | .context("failed to parse weekday number" )?; |
818 | self.inp = inp; |
819 | |
820 | let weekday = i8::try_from(weekday).map_err(|_| { |
821 | err!("parsed weekday number ` {weekday}` is invalid" ) |
822 | })?; |
823 | let weekday = Weekday::from_sunday_zero_offset(weekday) |
824 | .context("weekday number is invalid" )?; |
825 | self.tm.weekday = Some(weekday); |
826 | self.bump_fmt(); |
827 | Ok(()) |
828 | } |
829 | |
830 | /// Parse `%U`, which is a week number with Sunday being the first day |
831 | /// in the first week numbered `01`. |
832 | fn parse_week_sun(&mut self, ext: Extension) -> Result<(), Error> { |
833 | let (week, inp) = ext |
834 | .parse_number(2, Flag::PadZero, self.inp) |
835 | .context("failed to parse Sunday-based week number" )?; |
836 | self.inp = inp; |
837 | |
838 | let week = t::WeekNum::try_new("week" , week) |
839 | .context("Sunday-based week number is invalid" )?; |
840 | self.tm.week_sun = Some(week); |
841 | self.bump_fmt(); |
842 | Ok(()) |
843 | } |
844 | |
845 | /// Parse `%V`, which is an ISO 8601 week number. |
846 | fn parse_week_iso(&mut self, ext: Extension) -> Result<(), Error> { |
847 | let (week, inp) = ext |
848 | .parse_number(2, Flag::PadZero, self.inp) |
849 | .context("failed to parse ISO 8601 week number" )?; |
850 | self.inp = inp; |
851 | |
852 | let week = t::ISOWeek::try_new("week" , week) |
853 | .context("ISO 8601 week number is invalid" )?; |
854 | self.tm.iso_week = Some(week); |
855 | self.bump_fmt(); |
856 | Ok(()) |
857 | } |
858 | |
859 | /// Parse `%W`, which is a week number with Monday being the first day |
860 | /// in the first week numbered `01`. |
861 | fn parse_week_mon(&mut self, ext: Extension) -> Result<(), Error> { |
862 | let (week, inp) = ext |
863 | .parse_number(2, Flag::PadZero, self.inp) |
864 | .context("failed to parse Monday-based week number" )?; |
865 | self.inp = inp; |
866 | |
867 | let week = t::WeekNum::try_new("week" , week) |
868 | .context("Monday-based week number is invalid" )?; |
869 | self.tm.week_mon = Some(week); |
870 | self.bump_fmt(); |
871 | Ok(()) |
872 | } |
873 | |
874 | /// Parses `%Y`, which we permit to be any year, including a negative year. |
875 | fn parse_year(&mut self, ext: Extension) -> Result<(), Error> { |
876 | let (sign, inp) = parse_optional_sign(self.inp); |
877 | let (year, inp) = ext |
878 | .parse_number(4, Flag::PadZero, inp) |
879 | .context("failed to parse year" )?; |
880 | self.inp = inp; |
881 | |
882 | // OK because sign=={1,-1} and year can't be bigger than 4 digits |
883 | // so overflow isn't possible. |
884 | let year = sign.checked_mul(year).unwrap(); |
885 | let year = t::Year::try_new("year" , year) |
886 | .context("year number is invalid" )?; |
887 | self.tm.year = Some(year); |
888 | self.bump_fmt(); |
889 | Ok(()) |
890 | } |
891 | |
892 | /// Parses `%y`, which is equivalent to a 2-digit year. |
893 | /// |
894 | /// The numbers 69-99 refer to 1969-1999, while 00-68 refer to 2000-2068. |
895 | fn parse_year2(&mut self, ext: Extension) -> Result<(), Error> { |
896 | type Year2Digit = ri8<0, 99>; |
897 | |
898 | let (year, inp) = ext |
899 | .parse_number(2, Flag::PadZero, self.inp) |
900 | .context("failed to parse 2-digit year" )?; |
901 | self.inp = inp; |
902 | |
903 | let year = Year2Digit::try_new("year (2 digits)" , year) |
904 | .context("year number is invalid" )?; |
905 | let mut year = t::Year::rfrom(year); |
906 | if year <= C(68) { |
907 | year += C(2000); |
908 | } else { |
909 | year += C(1900); |
910 | } |
911 | self.tm.year = Some(year); |
912 | self.bump_fmt(); |
913 | Ok(()) |
914 | } |
915 | |
916 | /// Parses `%C`, which we permit to just be a century, including a negative |
917 | /// century. |
918 | fn parse_century(&mut self, ext: Extension) -> Result<(), Error> { |
919 | let (sign, inp) = parse_optional_sign(self.inp); |
920 | let (century, inp) = ext |
921 | .parse_number(2, Flag::NoPad, inp) |
922 | .context("failed to parse century" )?; |
923 | self.inp = inp; |
924 | |
925 | // OK because sign=={1,-1} and century can't be bigger than 2 digits |
926 | // so overflow isn't possible. |
927 | let century = sign.checked_mul(century).unwrap(); |
928 | // Similarly, we have 64-bit integers here. Two digits multiplied by |
929 | // 100 will never overflow. |
930 | let year = century.checked_mul(100).unwrap(); |
931 | // I believe the error condition here is impossible. |
932 | let year = t::Year::try_new("year" , year) |
933 | .context("year number (from century) is invalid" )?; |
934 | self.tm.year = Some(year); |
935 | self.bump_fmt(); |
936 | Ok(()) |
937 | } |
938 | |
939 | /// Parses `%G`, which we permit to be any year, including a negative year. |
940 | fn parse_iso_week_year(&mut self, ext: Extension) -> Result<(), Error> { |
941 | let (sign, inp) = parse_optional_sign(self.inp); |
942 | let (year, inp) = ext |
943 | .parse_number(4, Flag::PadZero, inp) |
944 | .context("failed to parse ISO 8601 week-based year" )?; |
945 | self.inp = inp; |
946 | |
947 | // OK because sign=={1,-1} and year can't be bigger than 4 digits |
948 | // so overflow isn't possible. |
949 | let year = sign.checked_mul(year).unwrap(); |
950 | let year = t::ISOYear::try_new("year" , year) |
951 | .context("ISO 8601 week-based year number is invalid" )?; |
952 | self.tm.iso_week_year = Some(year); |
953 | self.bump_fmt(); |
954 | Ok(()) |
955 | } |
956 | |
957 | /// Parses `%g`, which is equivalent to a 2-digit ISO 8601 week-based year. |
958 | /// |
959 | /// The numbers 69-99 refer to 1969-1999, while 00-68 refer to 2000-2068. |
960 | fn parse_iso_week_year2(&mut self, ext: Extension) -> Result<(), Error> { |
961 | type Year2Digit = ri8<0, 99>; |
962 | |
963 | let (year, inp) = ext |
964 | .parse_number(2, Flag::PadZero, self.inp) |
965 | .context("failed to parse 2-digit ISO 8601 week-based year" )?; |
966 | self.inp = inp; |
967 | |
968 | let year = Year2Digit::try_new("year (2 digits)" , year) |
969 | .context("ISO 8601 week-based year number is invalid" )?; |
970 | let mut year = t::ISOYear::rfrom(year); |
971 | if year <= C(68) { |
972 | year += C(2000); |
973 | } else { |
974 | year += C(1900); |
975 | } |
976 | self.tm.iso_week_year = Some(year); |
977 | self.bump_fmt(); |
978 | Ok(()) |
979 | } |
980 | } |
981 | |
982 | impl Extension { |
983 | /// Parse an integer with the given default padding and flag settings. |
984 | /// |
985 | /// The default padding is usually 2 (4 for %Y) and the default flag is |
986 | /// usually Flag::PadZero (there are no cases where the default flag is |
987 | /// different at time of writing). But both the padding and the flag can be |
988 | /// overridden by the settings on this extension. |
989 | /// |
990 | /// Generally speaking, parsing ignores everything in an extension except |
991 | /// for padding. When padding is set, then parsing will limit itself to a |
992 | /// number of digits equal to the greater of the default padding size or |
993 | /// the configured padding size. This permits `%Y%m%d` to parse `20240730` |
994 | /// successfully, for example. |
995 | /// |
996 | /// The remaining input is returned. This returns an error if the given |
997 | /// input is empty. |
998 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
999 | fn parse_number<'i>( |
1000 | self, |
1001 | default_pad_width: usize, |
1002 | default_flag: Flag, |
1003 | mut inp: &'i [u8], |
1004 | ) -> Result<(i64, &'i [u8]), Error> { |
1005 | let flag = self.flag.unwrap_or(default_flag); |
1006 | let zero_pad_width = match flag { |
1007 | Flag::PadSpace | Flag::NoPad => 0, |
1008 | _ => self.width.map(usize::from).unwrap_or(default_pad_width), |
1009 | }; |
1010 | let max_digits = default_pad_width.max(zero_pad_width); |
1011 | |
1012 | // Strip and ignore any whitespace we might see here. |
1013 | while inp.get(0).map_or(false, |b| b.is_ascii_whitespace()) { |
1014 | inp = &inp[1..]; |
1015 | } |
1016 | let mut digits = 0; |
1017 | while digits < inp.len() |
1018 | && digits < zero_pad_width |
1019 | && inp[digits] == b'0' |
1020 | { |
1021 | digits += 1; |
1022 | } |
1023 | let mut n: i64 = 0; |
1024 | while digits < inp.len() |
1025 | && digits < max_digits |
1026 | && inp[digits].is_ascii_digit() |
1027 | { |
1028 | let byte = inp[digits]; |
1029 | digits += 1; |
1030 | // This is manually inlined from `crate::util::parse::i64` to avoid |
1031 | // repeating this loop, and with some error cases removed since we |
1032 | // know that `byte` is an ASCII digit. |
1033 | let digit = i64::from(byte - b'0' ); |
1034 | n = n |
1035 | .checked_mul(10) |
1036 | .and_then(|n| n.checked_add(digit)) |
1037 | .ok_or_else(|| { |
1038 | err!( |
1039 | "number ' {}' too big to parse into 64-bit integer" , |
1040 | escape::Bytes(&inp[..digits]), |
1041 | ) |
1042 | })?; |
1043 | } |
1044 | if digits == 0 { |
1045 | return Err(err!("invalid number, no digits found" )); |
1046 | } |
1047 | Ok((n, &inp[digits..])) |
1048 | } |
1049 | } |
1050 | |
1051 | /// Parses an optional sign from the beginning of the input. If one isn't |
1052 | /// found, then the sign returned is positive. |
1053 | /// |
1054 | /// This also returns the remaining unparsed input. |
1055 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
1056 | fn parse_optional_sign<'i>(input: &'i [u8]) -> (i64, &'i [u8]) { |
1057 | if input.is_empty() { |
1058 | (1, input) |
1059 | } else if input[0] == b'-' { |
1060 | (-1, &input[1..]) |
1061 | } else if input[0] == b'+' { |
1062 | (1, &input[1..]) |
1063 | } else { |
1064 | (1, input) |
1065 | } |
1066 | } |
1067 | |
1068 | /// Parses an optional sign from the beginning of the input. If one isn't |
1069 | /// found, then the sign returned is positive. |
1070 | /// |
1071 | /// This also returns the remaining unparsed input. |
1072 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
1073 | fn parse_required_sign<'i>( |
1074 | input: &'i [u8], |
1075 | ) -> Result<(t::Sign, &'i [u8]), Error> { |
1076 | if input.is_empty() { |
1077 | Err(err!("expected +/- sign, but found end of input" )) |
1078 | } else if input[0] == b'-' { |
1079 | Ok((t::Sign::N::<-1>(), &input[1..])) |
1080 | } else if input[0] == b'+' { |
1081 | Ok((t::Sign::N::<1>(), &input[1..])) |
1082 | } else { |
1083 | Err(err!( |
1084 | "expected +/- sign, but found {found:?} instead" , |
1085 | found = escape::Byte(input[0]) |
1086 | )) |
1087 | } |
1088 | } |
1089 | |
1090 | /// Parses the input such that, on success, the index of the first matching |
1091 | /// choice (via ASCII case insensitive comparisons) is returned, along with |
1092 | /// any remaining unparsed input. |
1093 | /// |
1094 | /// If no choice given is a prefix of the input, then an error is returned. |
1095 | /// The error includes the possible allowed choices. |
1096 | fn parse_choice<'i>( |
1097 | input: &'i [u8], |
1098 | choices: &[&'static [u8]], |
1099 | ) -> Result<(usize, &'i [u8]), Error> { |
1100 | for (i, choice) in choices.into_iter().enumerate() { |
1101 | if input.len() < choice.len() { |
1102 | continue; |
1103 | } |
1104 | let (candidate, input) = input.split_at(choice.len()); |
1105 | if candidate.eq_ignore_ascii_case(choice) { |
1106 | return Ok((i, input)); |
1107 | } |
1108 | } |
1109 | #[cfg (feature = "alloc" )] |
1110 | { |
1111 | let mut err = alloc::format!( |
1112 | "failed to find expected choice at beginning of {input:?}, \ |
1113 | available choices are: " , |
1114 | input = escape::Bytes(input), |
1115 | ); |
1116 | for (i, choice) in choices.iter().enumerate() { |
1117 | if i > 0 { |
1118 | write!(err, ", " ).unwrap(); |
1119 | } |
1120 | write!(err, " {}" , escape::Bytes(choice)).unwrap(); |
1121 | } |
1122 | Err(Error::adhoc(err)) |
1123 | } |
1124 | #[cfg (not(feature = "alloc" ))] |
1125 | { |
1126 | Err(err!( |
1127 | "failed to find expected value from a set of allowed choices" |
1128 | )) |
1129 | } |
1130 | } |
1131 | |
1132 | /// Like `parse_choice`, but specialized for AM/PM. |
1133 | /// |
1134 | /// This exists because AM/PM is common and we can take advantage of the fact |
1135 | /// that they are both exactly two bytes. |
1136 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
1137 | fn parse_ampm<'i>(input: &'i [u8]) -> Result<(usize, &'i [u8]), Error> { |
1138 | if input.len() < 2 { |
1139 | return Err(err!( |
1140 | "expected to find AM or PM, \ |
1141 | but the remaining input, {input:?}, is too short \ |
1142 | to contain one" , |
1143 | input = escape::Bytes(input), |
1144 | )); |
1145 | } |
1146 | let (x: &[u8], input: &[u8]) = input.split_at(mid:2); |
1147 | let candidate: &[u8; 2] = &[x[0].to_ascii_lowercase(), x[1].to_ascii_lowercase()]; |
1148 | let index: usize = match candidate { |
1149 | b"am" => 0, |
1150 | b"pm" => 1, |
1151 | _ => { |
1152 | return Err(err!( |
1153 | "expected to find AM or PM, but found \ |
1154 | {candidate:?} instead" , |
1155 | candidate = escape::Bytes(x), |
1156 | )) |
1157 | } |
1158 | }; |
1159 | Ok((index, input)) |
1160 | } |
1161 | |
1162 | /// Like `parse_choice`, but specialized for weekday abbreviation. |
1163 | /// |
1164 | /// This exists because weekday abbreviations are common and we can take |
1165 | /// advantage of the fact that they are all exactly three bytes. |
1166 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
1167 | fn parse_weekday_abbrev<'i>( |
1168 | input: &'i [u8], |
1169 | ) -> Result<(usize, &'i [u8]), Error> { |
1170 | if input.len() < 3 { |
1171 | return Err(err!( |
1172 | "expected to find a weekday abbreviation, \ |
1173 | but the remaining input, {input:?}, is too short \ |
1174 | to contain one" , |
1175 | input = escape::Bytes(input), |
1176 | )); |
1177 | } |
1178 | let (x, input) = input.split_at(3); |
1179 | let candidate = &[ |
1180 | x[0].to_ascii_lowercase(), |
1181 | x[1].to_ascii_lowercase(), |
1182 | x[2].to_ascii_lowercase(), |
1183 | ]; |
1184 | let index = match candidate { |
1185 | b"sun" => 0, |
1186 | b"mon" => 1, |
1187 | b"tue" => 2, |
1188 | b"wed" => 3, |
1189 | b"thu" => 4, |
1190 | b"fri" => 5, |
1191 | b"sat" => 6, |
1192 | _ => { |
1193 | return Err(err!( |
1194 | "expected to find weekday abbreviation, but found \ |
1195 | {candidate:?} instead" , |
1196 | candidate = escape::Bytes(x), |
1197 | )) |
1198 | } |
1199 | }; |
1200 | Ok((index, input)) |
1201 | } |
1202 | |
1203 | /// Like `parse_choice`, but specialized for month name abbreviation. |
1204 | /// |
1205 | /// This exists because month name abbreviations are common and we can take |
1206 | /// advantage of the fact that they are all exactly three bytes. |
1207 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
1208 | fn parse_month_name_abbrev<'i>( |
1209 | input: &'i [u8], |
1210 | ) -> Result<(usize, &'i [u8]), Error> { |
1211 | if input.len() < 3 { |
1212 | return Err(err!( |
1213 | "expected to find a month name abbreviation, \ |
1214 | but the remaining input, {input:?}, is too short \ |
1215 | to contain one" , |
1216 | input = escape::Bytes(input), |
1217 | )); |
1218 | } |
1219 | let (x, input) = input.split_at(3); |
1220 | let candidate = &[ |
1221 | x[0].to_ascii_lowercase(), |
1222 | x[1].to_ascii_lowercase(), |
1223 | x[2].to_ascii_lowercase(), |
1224 | ]; |
1225 | let index = match candidate { |
1226 | b"jan" => 0, |
1227 | b"feb" => 1, |
1228 | b"mar" => 2, |
1229 | b"apr" => 3, |
1230 | b"may" => 4, |
1231 | b"jun" => 5, |
1232 | b"jul" => 6, |
1233 | b"aug" => 7, |
1234 | b"sep" => 8, |
1235 | b"oct" => 9, |
1236 | b"nov" => 10, |
1237 | b"dec" => 11, |
1238 | _ => { |
1239 | return Err(err!( |
1240 | "expected to find month name abbreviation, but found \ |
1241 | {candidate:?} instead" , |
1242 | candidate = escape::Bytes(x), |
1243 | )) |
1244 | } |
1245 | }; |
1246 | Ok((index, input)) |
1247 | } |
1248 | |
1249 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
1250 | fn parse_iana<'i>(input: &'i [u8]) -> Result<(&'i str, &'i [u8]), Error> { |
1251 | let mkiana: impl Fn(&[u8]) -> &[u8] = parse::slicer(start:input); |
1252 | let (_, mut input: &[u8]) = parse_iana_component(input)?; |
1253 | while input.starts_with(needle:b"/" ) { |
1254 | input = &input[1..]; |
1255 | let (_, unconsumed: &[u8]) = parse_iana_component(input)?; |
1256 | input = unconsumed; |
1257 | } |
1258 | // This is OK because all bytes in a IANA TZ annotation are guaranteed |
1259 | // to be ASCII, or else we wouldn't be here. If this turns out to be |
1260 | // a perf issue, we can do an unchecked conversion here. But I figured |
1261 | // it would be better to start conservative. |
1262 | let iana: &str = core::str::from_utf8(mkiana(input)).expect(msg:"ASCII" ); |
1263 | Ok((iana, input)) |
1264 | } |
1265 | |
1266 | /// Parses a single IANA name component. That is, the thing that leads all IANA |
1267 | /// time zone identifiers and the thing that must always come after a `/`. This |
1268 | /// returns an error if no component could be found. |
1269 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
1270 | fn parse_iana_component<'i>( |
1271 | mut input: &'i [u8], |
1272 | ) -> Result<(&'i [u8], &'i [u8]), Error> { |
1273 | let mkname = parse::slicer(input); |
1274 | if input.is_empty() { |
1275 | return Err(err!( |
1276 | "expected the start of an IANA time zone identifier \ |
1277 | name or component, but found end of input instead" , |
1278 | )); |
1279 | } |
1280 | if !matches!(input[0], b'_' | b'.' | b'A' ..=b'Z' | b'a' ..=b'z' ) { |
1281 | return Err(err!( |
1282 | "expected the start of an IANA time zone identifier \ |
1283 | name or component, but found {:?} instead" , |
1284 | escape::Byte(input[0]), |
1285 | )); |
1286 | } |
1287 | input = &input[1..]; |
1288 | |
1289 | let is_iana_char = |byte| { |
1290 | matches!( |
1291 | byte, |
1292 | b'_' | b'.' | b'+' | b'-' | b'0' ..=b'9' | b'A' ..=b'Z' | b'a' ..=b'z' , |
1293 | ) |
1294 | }; |
1295 | while !input.is_empty() && is_iana_char(input[0]) { |
1296 | input = &input[1..]; |
1297 | } |
1298 | Ok((mkname(input), input)) |
1299 | } |
1300 | |
1301 | #[cfg (feature = "alloc" )] |
1302 | #[cfg (test)] |
1303 | mod tests { |
1304 | use alloc::string::ToString; |
1305 | |
1306 | use super::*; |
1307 | |
1308 | #[test ] |
1309 | fn ok_parse_zoned() { |
1310 | if crate::tz::db().is_definitively_empty() { |
1311 | return; |
1312 | } |
1313 | |
1314 | let p = |fmt: &str, input: &str| { |
1315 | BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes()) |
1316 | .unwrap() |
1317 | .to_zoned() |
1318 | .unwrap() |
1319 | }; |
1320 | |
1321 | insta::assert_debug_snapshot!( |
1322 | p("%h %d, %Y %H:%M:%S %z" , "Apr 1, 2022 20:46:15 -0400" ), |
1323 | @"2022-04-01T20:46:15-04:00[-04:00]" , |
1324 | ); |
1325 | insta::assert_debug_snapshot!( |
1326 | p("%h %d, %Y %H:%M:%S %Q" , "Apr 1, 2022 20:46:15 -0400" ), |
1327 | @"2022-04-01T20:46:15-04:00[-04:00]" , |
1328 | ); |
1329 | insta::assert_debug_snapshot!( |
1330 | p("%h %d, %Y %H:%M:%S [%Q]" , "Apr 1, 2022 20:46:15 [America/New_York]" ), |
1331 | @"2022-04-01T20:46:15-04:00[America/New_York]" , |
1332 | ); |
1333 | insta::assert_debug_snapshot!( |
1334 | p("%h %d, %Y %H:%M:%S %Q" , "Apr 1, 2022 20:46:15 America/New_York" ), |
1335 | @"2022-04-01T20:46:15-04:00[America/New_York]" , |
1336 | ); |
1337 | insta::assert_debug_snapshot!( |
1338 | p("%h %d, %Y %H:%M:%S %:z %:Q" , "Apr 1, 2022 20:46:15 -08:00 -04:00" ), |
1339 | @"2022-04-01T20:46:15-04:00[-04:00]" , |
1340 | ); |
1341 | } |
1342 | |
1343 | #[test ] |
1344 | fn ok_parse_timestamp() { |
1345 | let p = |fmt: &str, input: &str| { |
1346 | BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes()) |
1347 | .unwrap() |
1348 | .to_timestamp() |
1349 | .unwrap() |
1350 | }; |
1351 | |
1352 | insta::assert_debug_snapshot!( |
1353 | p("%h %d, %Y %H:%M:%S %z" , "Apr 1, 2022 20:46:15 -0400" ), |
1354 | @"2022-04-02T00:46:15Z" , |
1355 | ); |
1356 | insta::assert_debug_snapshot!( |
1357 | p("%h %d, %Y %H:%M:%S %z" , "Apr 1, 2022 20:46:15 +0400" ), |
1358 | @"2022-04-01T16:46:15Z" , |
1359 | ); |
1360 | insta::assert_debug_snapshot!( |
1361 | p("%h %d, %Y %H:%M:%S %z" , "Apr 1, 2022 20:46:15 -040059" ), |
1362 | @"2022-04-02T00:47:14Z" , |
1363 | ); |
1364 | |
1365 | insta::assert_debug_snapshot!( |
1366 | p("%h %d, %Y %H:%M:%S %:z" , "Apr 1, 2022 20:46:15 -04:00" ), |
1367 | @"2022-04-02T00:46:15Z" , |
1368 | ); |
1369 | insta::assert_debug_snapshot!( |
1370 | p("%h %d, %Y %H:%M:%S %:z" , "Apr 1, 2022 20:46:15 +04:00" ), |
1371 | @"2022-04-01T16:46:15Z" , |
1372 | ); |
1373 | insta::assert_debug_snapshot!( |
1374 | p("%h %d, %Y %H:%M:%S %:z" , "Apr 1, 2022 20:46:15 -04:00:59" ), |
1375 | @"2022-04-02T00:47:14Z" , |
1376 | ); |
1377 | |
1378 | insta::assert_debug_snapshot!( |
1379 | p("%s" , "0" ), |
1380 | @"1970-01-01T00:00:00Z" , |
1381 | ); |
1382 | insta::assert_debug_snapshot!( |
1383 | p("%s" , "-0" ), |
1384 | @"1970-01-01T00:00:00Z" , |
1385 | ); |
1386 | insta::assert_debug_snapshot!( |
1387 | p("%s" , "-1" ), |
1388 | @"1969-12-31T23:59:59Z" , |
1389 | ); |
1390 | insta::assert_debug_snapshot!( |
1391 | p("%s" , "1" ), |
1392 | @"1970-01-01T00:00:01Z" , |
1393 | ); |
1394 | insta::assert_debug_snapshot!( |
1395 | p("%s" , "+1" ), |
1396 | @"1970-01-01T00:00:01Z" , |
1397 | ); |
1398 | insta::assert_debug_snapshot!( |
1399 | p("%s" , "1737396540" ), |
1400 | @"2025-01-20T18:09:00Z" , |
1401 | ); |
1402 | insta::assert_debug_snapshot!( |
1403 | p("%s" , "-377705023201" ), |
1404 | @"-009999-01-02T01:59:59Z" , |
1405 | ); |
1406 | insta::assert_debug_snapshot!( |
1407 | p("%s" , "253402207200" ), |
1408 | @"9999-12-30T22:00:00Z" , |
1409 | ); |
1410 | } |
1411 | |
1412 | #[test ] |
1413 | fn ok_parse_datetime() { |
1414 | let p = |fmt: &str, input: &str| { |
1415 | BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes()) |
1416 | .unwrap() |
1417 | .to_datetime() |
1418 | .unwrap() |
1419 | }; |
1420 | |
1421 | insta::assert_debug_snapshot!( |
1422 | p("%h %d, %Y %H:%M:%S" , "Apr 1, 2022 20:46:15" ), |
1423 | @"2022-04-01T20:46:15" , |
1424 | ); |
1425 | insta::assert_debug_snapshot!( |
1426 | p("%h %05d, %Y %H:%M:%S" , "Apr 1, 2022 20:46:15" ), |
1427 | @"2022-04-01T20:46:15" , |
1428 | ); |
1429 | } |
1430 | |
1431 | #[test ] |
1432 | fn ok_parse_date() { |
1433 | let p = |fmt: &str, input: &str| { |
1434 | BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes()) |
1435 | .unwrap() |
1436 | .to_date() |
1437 | .unwrap() |
1438 | }; |
1439 | |
1440 | insta::assert_debug_snapshot!( |
1441 | p("%m/%d/%y" , "1/1/99" ), |
1442 | @"1999-01-01" , |
1443 | ); |
1444 | insta::assert_debug_snapshot!( |
1445 | p("%m/%d/%04y" , "1/1/0099" ), |
1446 | @"1999-01-01" , |
1447 | ); |
1448 | insta::assert_debug_snapshot!( |
1449 | p("%D" , "1/1/99" ), |
1450 | @"1999-01-01" , |
1451 | ); |
1452 | insta::assert_debug_snapshot!( |
1453 | p("%m/%d/%Y" , "1/1/0099" ), |
1454 | @"0099-01-01" , |
1455 | ); |
1456 | insta::assert_debug_snapshot!( |
1457 | p("%m/%d/%Y" , "1/1/1999" ), |
1458 | @"1999-01-01" , |
1459 | ); |
1460 | insta::assert_debug_snapshot!( |
1461 | p("%m/%d/%Y" , "12/31/9999" ), |
1462 | @"9999-12-31" , |
1463 | ); |
1464 | insta::assert_debug_snapshot!( |
1465 | p("%m/%d/%Y" , "01/01/-9999" ), |
1466 | @"-009999-01-01" , |
1467 | ); |
1468 | insta::assert_snapshot!( |
1469 | p("%a %m/%d/%Y" , "sun 7/14/2024" ), |
1470 | @"2024-07-14" , |
1471 | ); |
1472 | insta::assert_snapshot!( |
1473 | p("%A %m/%d/%Y" , "sUnDaY 7/14/2024" ), |
1474 | @"2024-07-14" , |
1475 | ); |
1476 | insta::assert_snapshot!( |
1477 | p("%b %d %Y" , "Jul 14 2024" ), |
1478 | @"2024-07-14" , |
1479 | ); |
1480 | insta::assert_snapshot!( |
1481 | p("%B %d, %Y" , "July 14, 2024" ), |
1482 | @"2024-07-14" , |
1483 | ); |
1484 | insta::assert_snapshot!( |
1485 | p("%A, %B %d, %Y" , "Wednesday, dEcEmBeR 25, 2024" ), |
1486 | @"2024-12-25" , |
1487 | ); |
1488 | |
1489 | insta::assert_debug_snapshot!( |
1490 | p("%Y%m%d" , "20240730" ), |
1491 | @"2024-07-30" , |
1492 | ); |
1493 | insta::assert_debug_snapshot!( |
1494 | p("%Y%m%d" , "09990730" ), |
1495 | @"0999-07-30" , |
1496 | ); |
1497 | insta::assert_debug_snapshot!( |
1498 | p("%Y%m%d" , "9990111" ), |
1499 | @"9990-11-01" , |
1500 | ); |
1501 | insta::assert_debug_snapshot!( |
1502 | p("%3Y%m%d" , "09990111" ), |
1503 | @"0999-01-11" , |
1504 | ); |
1505 | insta::assert_debug_snapshot!( |
1506 | p("%5Y%m%d" , "09990111" ), |
1507 | @"9990-11-01" , |
1508 | ); |
1509 | insta::assert_debug_snapshot!( |
1510 | p("%5Y%m%d" , "009990111" ), |
1511 | @"0999-01-11" , |
1512 | ); |
1513 | |
1514 | insta::assert_debug_snapshot!( |
1515 | p("%C-%m-%d" , "20-07-01" ), |
1516 | @"2000-07-01" , |
1517 | ); |
1518 | insta::assert_debug_snapshot!( |
1519 | p("%C-%m-%d" , "-20-07-01" ), |
1520 | @"-002000-07-01" , |
1521 | ); |
1522 | insta::assert_debug_snapshot!( |
1523 | p("%C-%m-%d" , "9-07-01" ), |
1524 | @"0900-07-01" , |
1525 | ); |
1526 | insta::assert_debug_snapshot!( |
1527 | p("%C-%m-%d" , "-9-07-01" ), |
1528 | @"-000900-07-01" , |
1529 | ); |
1530 | insta::assert_debug_snapshot!( |
1531 | p("%C-%m-%d" , "09-07-01" ), |
1532 | @"0900-07-01" , |
1533 | ); |
1534 | insta::assert_debug_snapshot!( |
1535 | p("%C-%m-%d" , "-09-07-01" ), |
1536 | @"-000900-07-01" , |
1537 | ); |
1538 | insta::assert_debug_snapshot!( |
1539 | p("%C-%m-%d" , "0-07-01" ), |
1540 | @"0000-07-01" , |
1541 | ); |
1542 | insta::assert_debug_snapshot!( |
1543 | p("%C-%m-%d" , "-0-07-01" ), |
1544 | @"0000-07-01" , |
1545 | ); |
1546 | |
1547 | insta::assert_snapshot!( |
1548 | p("%u %m/%d/%Y" , "7 7/14/2024" ), |
1549 | @"2024-07-14" , |
1550 | ); |
1551 | insta::assert_snapshot!( |
1552 | p("%w %m/%d/%Y" , "0 7/14/2024" ), |
1553 | @"2024-07-14" , |
1554 | ); |
1555 | |
1556 | insta::assert_snapshot!( |
1557 | p("%Y-%U-%u" , "2025-00-6" ), |
1558 | @"2025-01-04" , |
1559 | ); |
1560 | insta::assert_snapshot!( |
1561 | p("%Y-%U-%u" , "2025-01-7" ), |
1562 | @"2025-01-05" , |
1563 | ); |
1564 | insta::assert_snapshot!( |
1565 | p("%Y-%U-%u" , "2025-01-1" ), |
1566 | @"2025-01-06" , |
1567 | ); |
1568 | |
1569 | insta::assert_snapshot!( |
1570 | p("%Y-%W-%u" , "2025-00-6" ), |
1571 | @"2025-01-04" , |
1572 | ); |
1573 | insta::assert_snapshot!( |
1574 | p("%Y-%W-%u" , "2025-00-7" ), |
1575 | @"2025-01-05" , |
1576 | ); |
1577 | insta::assert_snapshot!( |
1578 | p("%Y-%W-%u" , "2025-01-1" ), |
1579 | @"2025-01-06" , |
1580 | ); |
1581 | insta::assert_snapshot!( |
1582 | p("%Y-%W-%u" , "2025-01-2" ), |
1583 | @"2025-01-07" , |
1584 | ); |
1585 | } |
1586 | |
1587 | #[test ] |
1588 | fn ok_parse_time() { |
1589 | let p = |fmt: &str, input: &str| { |
1590 | BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes()) |
1591 | .unwrap() |
1592 | .to_time() |
1593 | .unwrap() |
1594 | }; |
1595 | |
1596 | insta::assert_debug_snapshot!( |
1597 | p("%H:%M" , "15:48" ), |
1598 | @"15:48:00" , |
1599 | ); |
1600 | insta::assert_debug_snapshot!( |
1601 | p("%H:%M:%S" , "15:48:59" ), |
1602 | @"15:48:59" , |
1603 | ); |
1604 | insta::assert_debug_snapshot!( |
1605 | p("%H:%M:%S" , "15:48:60" ), |
1606 | @"15:48:59" , |
1607 | ); |
1608 | insta::assert_debug_snapshot!( |
1609 | p("%T" , "15:48:59" ), |
1610 | @"15:48:59" , |
1611 | ); |
1612 | insta::assert_debug_snapshot!( |
1613 | p("%R" , "15:48" ), |
1614 | @"15:48:00" , |
1615 | ); |
1616 | |
1617 | insta::assert_debug_snapshot!( |
1618 | p("%H %p" , "5 am" ), |
1619 | @"05:00:00" , |
1620 | ); |
1621 | insta::assert_debug_snapshot!( |
1622 | p("%H%p" , "5am" ), |
1623 | @"05:00:00" , |
1624 | ); |
1625 | insta::assert_debug_snapshot!( |
1626 | p("%H%p" , "11pm" ), |
1627 | @"23:00:00" , |
1628 | ); |
1629 | insta::assert_debug_snapshot!( |
1630 | p("%I%p" , "11pm" ), |
1631 | @"23:00:00" , |
1632 | ); |
1633 | insta::assert_debug_snapshot!( |
1634 | p("%I%p" , "12am" ), |
1635 | @"00:00:00" , |
1636 | ); |
1637 | insta::assert_debug_snapshot!( |
1638 | p("%H%p" , "23pm" ), |
1639 | @"23:00:00" , |
1640 | ); |
1641 | insta::assert_debug_snapshot!( |
1642 | p("%H%p" , "23am" ), |
1643 | @"11:00:00" , |
1644 | ); |
1645 | |
1646 | insta::assert_debug_snapshot!( |
1647 | p("%H:%M:%S%.f" , "15:48:01.1" ), |
1648 | @"15:48:01.1" , |
1649 | ); |
1650 | insta::assert_debug_snapshot!( |
1651 | p("%H:%M:%S%.255f" , "15:48:01.1" ), |
1652 | @"15:48:01.1" , |
1653 | ); |
1654 | insta::assert_debug_snapshot!( |
1655 | p("%H:%M:%S%255.255f" , "15:48:01.1" ), |
1656 | @"15:48:01.1" , |
1657 | ); |
1658 | insta::assert_debug_snapshot!( |
1659 | p("%H:%M:%S%.f" , "15:48:01" ), |
1660 | @"15:48:01" , |
1661 | ); |
1662 | insta::assert_debug_snapshot!( |
1663 | p("%H:%M:%S%.fa" , "15:48:01a" ), |
1664 | @"15:48:01" , |
1665 | ); |
1666 | insta::assert_debug_snapshot!( |
1667 | p("%H:%M:%S%.f" , "15:48:01.123456789" ), |
1668 | @"15:48:01.123456789" , |
1669 | ); |
1670 | insta::assert_debug_snapshot!( |
1671 | p("%H:%M:%S%.f" , "15:48:01.000000001" ), |
1672 | @"15:48:01.000000001" , |
1673 | ); |
1674 | |
1675 | insta::assert_debug_snapshot!( |
1676 | p("%H:%M:%S.%f" , "15:48:01.1" ), |
1677 | @"15:48:01.1" , |
1678 | ); |
1679 | insta::assert_debug_snapshot!( |
1680 | p("%H:%M:%S.%3f" , "15:48:01.123" ), |
1681 | @"15:48:01.123" , |
1682 | ); |
1683 | insta::assert_debug_snapshot!( |
1684 | p("%H:%M:%S.%3f" , "15:48:01.123456" ), |
1685 | @"15:48:01.123456" , |
1686 | ); |
1687 | |
1688 | insta::assert_debug_snapshot!( |
1689 | p("%H" , "09" ), |
1690 | @"09:00:00" , |
1691 | ); |
1692 | insta::assert_debug_snapshot!( |
1693 | p("%H" , " 9" ), |
1694 | @"09:00:00" , |
1695 | ); |
1696 | insta::assert_debug_snapshot!( |
1697 | p("%H" , "15" ), |
1698 | @"15:00:00" , |
1699 | ); |
1700 | insta::assert_debug_snapshot!( |
1701 | p("%k" , "09" ), |
1702 | @"09:00:00" , |
1703 | ); |
1704 | insta::assert_debug_snapshot!( |
1705 | p("%k" , " 9" ), |
1706 | @"09:00:00" , |
1707 | ); |
1708 | insta::assert_debug_snapshot!( |
1709 | p("%k" , "15" ), |
1710 | @"15:00:00" , |
1711 | ); |
1712 | |
1713 | insta::assert_debug_snapshot!( |
1714 | p("%I" , "09" ), |
1715 | @"09:00:00" , |
1716 | ); |
1717 | insta::assert_debug_snapshot!( |
1718 | p("%I" , " 9" ), |
1719 | @"09:00:00" , |
1720 | ); |
1721 | insta::assert_debug_snapshot!( |
1722 | p("%l" , "09" ), |
1723 | @"09:00:00" , |
1724 | ); |
1725 | insta::assert_debug_snapshot!( |
1726 | p("%l" , " 9" ), |
1727 | @"09:00:00" , |
1728 | ); |
1729 | } |
1730 | |
1731 | #[test ] |
1732 | fn ok_parse_whitespace() { |
1733 | let p = |fmt: &str, input: &str| { |
1734 | BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes()) |
1735 | .unwrap() |
1736 | .to_time() |
1737 | .unwrap() |
1738 | }; |
1739 | |
1740 | insta::assert_debug_snapshot!( |
1741 | p("%H%M" , "1548" ), |
1742 | @"15:48:00" , |
1743 | ); |
1744 | insta::assert_debug_snapshot!( |
1745 | p("%H%M" , "15 \n48" ), |
1746 | @"15:48:00" , |
1747 | ); |
1748 | insta::assert_debug_snapshot!( |
1749 | p("%H%M" , "15 \t48" ), |
1750 | @"15:48:00" , |
1751 | ); |
1752 | insta::assert_debug_snapshot!( |
1753 | p("%H%n%M" , "1548" ), |
1754 | @"15:48:00" , |
1755 | ); |
1756 | insta::assert_debug_snapshot!( |
1757 | p("%H%n%M" , "15 \n48" ), |
1758 | @"15:48:00" , |
1759 | ); |
1760 | insta::assert_debug_snapshot!( |
1761 | p("%H%n%M" , "15 \t48" ), |
1762 | @"15:48:00" , |
1763 | ); |
1764 | insta::assert_debug_snapshot!( |
1765 | p("%H%t%M" , "1548" ), |
1766 | @"15:48:00" , |
1767 | ); |
1768 | insta::assert_debug_snapshot!( |
1769 | p("%H%t%M" , "15 \n48" ), |
1770 | @"15:48:00" , |
1771 | ); |
1772 | insta::assert_debug_snapshot!( |
1773 | p("%H%t%M" , "15 \t48" ), |
1774 | @"15:48:00" , |
1775 | ); |
1776 | } |
1777 | |
1778 | #[test ] |
1779 | fn err_parse() { |
1780 | let p = |fmt: &str, input: &str| { |
1781 | BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes()) |
1782 | .unwrap_err() |
1783 | .to_string() |
1784 | }; |
1785 | |
1786 | insta::assert_snapshot!( |
1787 | p("%M" , "" ), |
1788 | @"strptime parsing failed: expected non-empty input for directive %M, but found end of input" , |
1789 | ); |
1790 | insta::assert_snapshot!( |
1791 | p("%M" , "a" ), |
1792 | @"strptime parsing failed: %M failed: failed to parse minute: invalid number, no digits found" , |
1793 | ); |
1794 | insta::assert_snapshot!( |
1795 | p("%M%S" , "15" ), |
1796 | @"strptime parsing failed: expected non-empty input for directive %S, but found end of input" , |
1797 | ); |
1798 | insta::assert_snapshot!( |
1799 | p("%M%a" , "Sun" ), |
1800 | @"strptime parsing failed: %M failed: failed to parse minute: invalid number, no digits found" , |
1801 | ); |
1802 | |
1803 | insta::assert_snapshot!( |
1804 | p("%y" , "999" ), |
1805 | @r###"strptime expects to consume the entire input, but "9" remains unparsed"### , |
1806 | ); |
1807 | insta::assert_snapshot!( |
1808 | p("%Y" , "-10000" ), |
1809 | @r###"strptime expects to consume the entire input, but "0" remains unparsed"### , |
1810 | ); |
1811 | insta::assert_snapshot!( |
1812 | p("%Y" , "10000" ), |
1813 | @r###"strptime expects to consume the entire input, but "0" remains unparsed"### , |
1814 | ); |
1815 | insta::assert_snapshot!( |
1816 | p("%A %m/%d/%y" , "Mon 7/14/24" ), |
1817 | @r#"strptime parsing failed: %A failed: unrecognized weekday abbreviation: failed to find expected choice at beginning of "Mon 7/14/24", available choices are: Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday"# , |
1818 | ); |
1819 | insta::assert_snapshot!( |
1820 | p("%b" , "Bad" ), |
1821 | @r###"strptime parsing failed: %b failed: expected to find month name abbreviation, but found "Bad" instead"### , |
1822 | ); |
1823 | insta::assert_snapshot!( |
1824 | p("%h" , "July" ), |
1825 | @r###"strptime expects to consume the entire input, but "y" remains unparsed"### , |
1826 | ); |
1827 | insta::assert_snapshot!( |
1828 | p("%B" , "Jul" ), |
1829 | @r###"strptime parsing failed: %B failed: unrecognized month name: failed to find expected choice at beginning of "Jul", available choices are: January, February, March, April, May, June, July, August, September, October, November, December"### , |
1830 | ); |
1831 | insta::assert_snapshot!( |
1832 | p("%H" , "24" ), |
1833 | @"strptime parsing failed: %H failed: hour number is invalid: parameter 'hour' with value 24 is not in the required range of 0..=23" , |
1834 | ); |
1835 | insta::assert_snapshot!( |
1836 | p("%M" , "60" ), |
1837 | @"strptime parsing failed: %M failed: minute number is invalid: parameter 'minute' with value 60 is not in the required range of 0..=59" , |
1838 | ); |
1839 | insta::assert_snapshot!( |
1840 | p("%S" , "61" ), |
1841 | @"strptime parsing failed: %S failed: second number is invalid: parameter 'second' with value 61 is not in the required range of 0..=59" , |
1842 | ); |
1843 | insta::assert_snapshot!( |
1844 | p("%I" , "0" ), |
1845 | @"strptime parsing failed: %I failed: hour number is invalid: parameter 'hour' with value 0 is not in the required range of 1..=12" , |
1846 | ); |
1847 | insta::assert_snapshot!( |
1848 | p("%I" , "13" ), |
1849 | @"strptime parsing failed: %I failed: hour number is invalid: parameter 'hour' with value 13 is not in the required range of 1..=12" , |
1850 | ); |
1851 | insta::assert_snapshot!( |
1852 | p("%p" , "aa" ), |
1853 | @r###"strptime parsing failed: %p failed: expected to find AM or PM, but found "aa" instead"### , |
1854 | ); |
1855 | |
1856 | insta::assert_snapshot!( |
1857 | p("%_" , " " ), |
1858 | @r###"strptime parsing failed: expected to find specifier directive after flag "_", but found end of format string"### , |
1859 | ); |
1860 | insta::assert_snapshot!( |
1861 | p("%-" , " " ), |
1862 | @r###"strptime parsing failed: expected to find specifier directive after flag "-", but found end of format string"### , |
1863 | ); |
1864 | insta::assert_snapshot!( |
1865 | p("%0" , " " ), |
1866 | @r###"strptime parsing failed: expected to find specifier directive after flag "0", but found end of format string"### , |
1867 | ); |
1868 | insta::assert_snapshot!( |
1869 | p("%^" , " " ), |
1870 | @r###"strptime parsing failed: expected to find specifier directive after flag "^", but found end of format string"### , |
1871 | ); |
1872 | insta::assert_snapshot!( |
1873 | p("%#" , " " ), |
1874 | @r###"strptime parsing failed: expected to find specifier directive after flag "#", but found end of format string"### , |
1875 | ); |
1876 | insta::assert_snapshot!( |
1877 | p("%_1" , " " ), |
1878 | @"strptime parsing failed: expected to find specifier directive after width 1, but found end of format string" , |
1879 | ); |
1880 | insta::assert_snapshot!( |
1881 | p("%_23" , " " ), |
1882 | @"strptime parsing failed: expected to find specifier directive after width 23, but found end of format string" , |
1883 | ); |
1884 | |
1885 | insta::assert_snapshot!( |
1886 | p("%H:%M:%S%.f" , "15:59:01." ), |
1887 | @"strptime parsing failed: %.f failed: expected at least one fractional decimal digit, but did not find any" , |
1888 | ); |
1889 | insta::assert_snapshot!( |
1890 | p("%H:%M:%S%.f" , "15:59:01.a" ), |
1891 | @"strptime parsing failed: %.f failed: expected at least one fractional decimal digit, but did not find any" , |
1892 | ); |
1893 | insta::assert_snapshot!( |
1894 | p("%H:%M:%S%.f" , "15:59:01.1234567891" ), |
1895 | @r###"strptime expects to consume the entire input, but "1" remains unparsed"### , |
1896 | ); |
1897 | insta::assert_snapshot!( |
1898 | p("%H:%M:%S.%f" , "15:59:01." ), |
1899 | @"strptime parsing failed: expected non-empty input for directive %f, but found end of input" , |
1900 | ); |
1901 | insta::assert_snapshot!( |
1902 | p("%H:%M:%S.%f" , "15:59:01" ), |
1903 | @r###"strptime parsing failed: expected to match literal byte "." from format string, but found end of input"### , |
1904 | ); |
1905 | insta::assert_snapshot!( |
1906 | p("%H:%M:%S.%f" , "15:59:01.a" ), |
1907 | @"strptime parsing failed: %f failed: expected at least one fractional decimal digit, but did not find any" , |
1908 | ); |
1909 | |
1910 | insta::assert_snapshot!( |
1911 | p("%Q" , "+America/New_York" ), |
1912 | @"strptime parsing failed: %Q failed: failed to parse hours from time zone offset Amer: invalid digit, expected 0-9 but got A" , |
1913 | ); |
1914 | insta::assert_snapshot!( |
1915 | p("%Q" , "-America/New_York" ), |
1916 | @"strptime parsing failed: %Q failed: failed to parse hours from time zone offset Amer: invalid digit, expected 0-9 but got A" , |
1917 | ); |
1918 | insta::assert_snapshot!( |
1919 | p("%:Q" , "+0400" ), |
1920 | @"strptime parsing failed: %:Q failed: expected at least HH:MM digits for time zone offset after sign, but found only 4 bytes remaining" , |
1921 | ); |
1922 | insta::assert_snapshot!( |
1923 | p("%Q" , "+04:00" ), |
1924 | @"strptime parsing failed: %Q failed: failed to parse minutes from time zone offset 04:0: invalid digit, expected 0-9 but got :" , |
1925 | ); |
1926 | insta::assert_snapshot!( |
1927 | p("%Q" , "America/" ), |
1928 | @"strptime parsing failed: %Q failed: expected the start of an IANA time zone identifier name or component, but found end of input instead" , |
1929 | ); |
1930 | insta::assert_snapshot!( |
1931 | p("%Q" , "America/+" ), |
1932 | @r###"strptime parsing failed: %Q failed: expected the start of an IANA time zone identifier name or component, but found "+" instead"### , |
1933 | ); |
1934 | |
1935 | insta::assert_snapshot!( |
1936 | p("%s" , "-377705023202" ), |
1937 | @"strptime parsing failed: %s failed: parsed Unix timestamp `-377705023202`, but out of range of valid Jiff `Timestamp`: parameter 'second' with value -377705023202 is not in the required range of -377705023201..=253402207200" , |
1938 | ); |
1939 | insta::assert_snapshot!( |
1940 | p("%s" , "253402207201" ), |
1941 | @"strptime parsing failed: %s failed: parsed Unix timestamp `253402207201`, but out of range of valid Jiff `Timestamp`: parameter 'second' with value 253402207201 is not in the required range of -377705023201..=253402207200" , |
1942 | ); |
1943 | insta::assert_snapshot!( |
1944 | p("%s" , "-9999999999999999999" ), |
1945 | @"strptime parsing failed: %s failed: failed to parse Unix timestamp (in seconds): number '9999999999999999999' too big to parse into 64-bit integer" , |
1946 | ); |
1947 | insta::assert_snapshot!( |
1948 | p("%s" , "9999999999999999999" ), |
1949 | @"strptime parsing failed: %s failed: failed to parse Unix timestamp (in seconds): number '9999999999999999999' too big to parse into 64-bit integer" , |
1950 | ); |
1951 | |
1952 | insta::assert_snapshot!( |
1953 | p("%u" , "0" ), |
1954 | @"strptime parsing failed: %u failed: weekday number is invalid: parameter 'weekday' with value 0 is not in the required range of 1..=7" , |
1955 | ); |
1956 | insta::assert_snapshot!( |
1957 | p("%w" , "7" ), |
1958 | @"strptime parsing failed: %w failed: weekday number is invalid: parameter 'weekday' with value 7 is not in the required range of 0..=6" , |
1959 | ); |
1960 | insta::assert_snapshot!( |
1961 | p("%u" , "128" ), |
1962 | @r###"strptime expects to consume the entire input, but "28" remains unparsed"### , |
1963 | ); |
1964 | insta::assert_snapshot!( |
1965 | p("%w" , "128" ), |
1966 | @r###"strptime expects to consume the entire input, but "28" remains unparsed"### , |
1967 | ); |
1968 | } |
1969 | |
1970 | #[test ] |
1971 | fn err_parse_date() { |
1972 | let p = |fmt: &str, input: &str| { |
1973 | BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes()) |
1974 | .unwrap() |
1975 | .to_date() |
1976 | .unwrap_err() |
1977 | .to_string() |
1978 | }; |
1979 | |
1980 | insta::assert_snapshot!( |
1981 | p("%Y" , "2024" ), |
1982 | @"a month/day, day-of-year or week date must be present to create a date, but none were found" , |
1983 | ); |
1984 | insta::assert_snapshot!( |
1985 | p("%m" , "7" ), |
1986 | @"missing year, date cannot be created" , |
1987 | ); |
1988 | insta::assert_snapshot!( |
1989 | p("%d" , "25" ), |
1990 | @"missing year, date cannot be created" , |
1991 | ); |
1992 | insta::assert_snapshot!( |
1993 | p("%Y-%m" , "2024-7" ), |
1994 | @"a month/day, day-of-year or week date must be present to create a date, but none were found" , |
1995 | ); |
1996 | insta::assert_snapshot!( |
1997 | p("%Y-%d" , "2024-25" ), |
1998 | @"a month/day, day-of-year or week date must be present to create a date, but none were found" , |
1999 | ); |
2000 | insta::assert_snapshot!( |
2001 | p("%m-%d" , "7-25" ), |
2002 | @"missing year, date cannot be created" , |
2003 | ); |
2004 | |
2005 | insta::assert_snapshot!( |
2006 | p("%m/%d/%y" , "6/31/24" ), |
2007 | @"invalid date: parameter 'day' with value 31 is not in the required range of 1..=30" , |
2008 | ); |
2009 | insta::assert_snapshot!( |
2010 | p("%m/%d/%y" , "2/29/23" ), |
2011 | @"invalid date: parameter 'day' with value 29 is not in the required range of 1..=28" , |
2012 | ); |
2013 | insta::assert_snapshot!( |
2014 | p("%a %m/%d/%y" , "Mon 7/14/24" ), |
2015 | @"parsed weekday Monday does not match weekday Sunday from parsed date 2024-07-14" , |
2016 | ); |
2017 | insta::assert_snapshot!( |
2018 | p("%A %m/%d/%y" , "Monday 7/14/24" ), |
2019 | @"parsed weekday Monday does not match weekday Sunday from parsed date 2024-07-14" , |
2020 | ); |
2021 | |
2022 | insta::assert_snapshot!( |
2023 | p("%Y-%U-%u" , "2025-00-2" ), |
2024 | @"weekday `Tuesday` is not valid for Sunday based week number `0` in year `2025`" , |
2025 | ); |
2026 | insta::assert_snapshot!( |
2027 | p("%Y-%W-%u" , "2025-00-2" ), |
2028 | @"weekday `Tuesday` is not valid for Monday based week number `0` in year `2025`" , |
2029 | ); |
2030 | } |
2031 | |
2032 | #[test ] |
2033 | fn err_parse_time() { |
2034 | let p = |fmt: &str, input: &str| { |
2035 | BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes()) |
2036 | .unwrap() |
2037 | .to_time() |
2038 | .unwrap_err() |
2039 | .to_string() |
2040 | }; |
2041 | |
2042 | insta::assert_snapshot!( |
2043 | p("%M" , "59" ), |
2044 | @"parsing format did not include hour directive, but did include minute directive (cannot have smaller time units with bigger time units missing)" , |
2045 | ); |
2046 | insta::assert_snapshot!( |
2047 | p("%S" , "59" ), |
2048 | @"parsing format did not include hour directive, but did include second directive (cannot have smaller time units with bigger time units missing)" , |
2049 | ); |
2050 | insta::assert_snapshot!( |
2051 | p("%M:%S" , "59:59" ), |
2052 | @"parsing format did not include hour directive, but did include minute directive (cannot have smaller time units with bigger time units missing)" , |
2053 | ); |
2054 | insta::assert_snapshot!( |
2055 | p("%H:%S" , "15:59" ), |
2056 | @"parsing format did not include minute directive, but did include second directive (cannot have smaller time units with bigger time units missing)" , |
2057 | ); |
2058 | } |
2059 | } |
2060 | |