jiff/fmt/rfc2822.rs
1/*!
2Support for printing and parsing instants using the [RFC 2822] datetime format.
3
4RFC 2822 is most commonly found when dealing with email messages.
5
6Since RFC 2822 only supports specifying a complete instant in time, the parser
7and printer in this module only use [`Zoned`] and [`Timestamp`]. If you need
8inexact time, you can get it from [`Zoned`] via [`Zoned::datetime`].
9
10[RFC 2822]: https://datatracker.ietf.org/doc/html/rfc2822
11
12# Incomplete support
13
14The RFC 2822 support in this crate is technically incomplete. Specifically,
15it does not support parsing comments within folding whitespace. It will parse
16comments after the datetime itself (including nested comments). See [Issue
17#39][issue39] for an example. If you find a real world use case for parsing
18comments within whitespace at any point in the datetime string, please file
19an issue. That is, the main reason it isn't currently supported is because
20it didn't seem worth the implementation complexity to account for it. But if
21there are real world use cases that need it, then that would be sufficient
22justification for adding it.
23
24RFC 2822 support should otherwise be complete, including support for parsing
25obselete offsets.
26
27[issue39]: https://github.com/BurntSushi/jiff/issues/39
28
29# Warning
30
31The RFC 2822 format only supports writing a precise instant in time
32expressed via a time zone offset. It does *not* support serializing
33the time zone itself. This means that if you format a zoned datetime
34in a time zone like `America/New_York` and then deserialize it, the
35zoned datetime you get back will be a "fixed offset" zoned datetime.
36This in turn means it will not perform daylight saving time safe
37arithmetic.
38
39Basically, you should use the RFC 2822 format if it's required (for
40example, when dealing with email). But you should not choose it as a
41general interchange format for new applications.
42*/
43
44use crate::{
45 civil::{Date, DateTime, Time, Weekday},
46 error::{err, ErrorContext},
47 fmt::{util::DecimalFormatter, Parsed, Write, WriteExt},
48 tz::{Offset, TimeZone},
49 util::{
50 escape, parse,
51 rangeint::{ri8, RFrom},
52 t::{self, C},
53 },
54 Error, Timestamp, Zoned,
55};
56
57/// The default date time parser that we use throughout Jiff.
58pub(crate) static DEFAULT_DATETIME_PARSER: DateTimeParser =
59 DateTimeParser::new();
60
61/// The default date time printer that we use throughout Jiff.
62pub(crate) static DEFAULT_DATETIME_PRINTER: DateTimePrinter =
63 DateTimePrinter::new();
64
65/// Convert a [`Zoned`] to an [RFC 2822] datetime string.
66///
67/// This is a convenience function for using [`DateTimePrinter`]. In
68/// particular, this always creates and allocates a new `String`. For writing
69/// to an existing string, or converting a [`Timestamp`] to an RFC 2822
70/// datetime string, you'll need to use `DateTimePrinter`.
71///
72/// [RFC 2822]: https://datatracker.ietf.org/doc/html/rfc2822
73///
74/// # Warning
75///
76/// The RFC 2822 format only supports writing a precise instant in time
77/// expressed via a time zone offset. It does *not* support serializing
78/// the time zone itself. This means that if you format a zoned datetime
79/// in a time zone like `America/New_York` and then deserialize it, the
80/// zoned datetime you get back will be a "fixed offset" zoned datetime.
81/// This in turn means it will not perform daylight saving time safe
82/// arithmetic.
83///
84/// Basically, you should use the RFC 2822 format if it's required (for
85/// example, when dealing with email). But you should not choose it as a
86/// general interchange format for new applications.
87///
88/// # Errors
89///
90/// This returns an error if the year corresponding to this timestamp cannot be
91/// represented in the RFC 2822 format. For example, a negative year.
92///
93/// # Example
94///
95/// This example shows how to convert a zoned datetime to the RFC 2822 format:
96///
97/// ```
98/// use jiff::{civil::date, fmt::rfc2822};
99///
100/// let zdt = date(2024, 6, 15).at(7, 0, 0, 0).in_tz("Australia/Tasmania")?;
101/// assert_eq!(rfc2822::to_string(&zdt)?, "Sat, 15 Jun 2024 07:00:00 +1000");
102///
103/// # Ok::<(), Box<dyn std::error::Error>>(())
104/// ```
105#[cfg(feature = "alloc")]
106#[inline]
107pub fn to_string(zdt: &Zoned) -> Result<alloc::string::String, Error> {
108 let mut buf = alloc::string::String::new();
109 DEFAULT_DATETIME_PRINTER.print_zoned(zdt, &mut buf)?;
110 Ok(buf)
111}
112
113/// Parse an [RFC 2822] datetime string into a [`Zoned`].
114///
115/// This is a convenience function for using [`DateTimeParser`]. In particular,
116/// this takes a `&str` while the `DateTimeParser` accepts a `&[u8]`.
117/// Moreover, if any configuration options are added to RFC 2822 parsing (none
118/// currently exist at time of writing), then it will be necessary to use a
119/// `DateTimeParser` to toggle them. Additionally, a `DateTimeParser` is needed
120/// for parsing into a [`Timestamp`].
121///
122/// [RFC 2822]: https://datatracker.ietf.org/doc/html/rfc2822
123///
124/// # Warning
125///
126/// The RFC 2822 format only supports writing a precise instant in time
127/// expressed via a time zone offset. It does *not* support serializing
128/// the time zone itself. This means that if you format a zoned datetime
129/// in a time zone like `America/New_York` and then deserialize it, the
130/// zoned datetime you get back will be a "fixed offset" zoned datetime.
131/// This in turn means it will not perform daylight saving time safe
132/// arithmetic.
133///
134/// Basically, you should use the RFC 2822 format if it's required (for
135/// example, when dealing with email). But you should not choose it as a
136/// general interchange format for new applications.
137///
138/// # Errors
139///
140/// This returns an error if the datetime string given is invalid or if it
141/// is valid but doesn't fit in the datetime range supported by Jiff. For
142/// example, RFC 2822 supports offsets up to 99 hours and 59 minutes,
143/// but Jiff's maximum offset is 25 hours, 59 minutes and 59 seconds.
144///
145/// # Example
146///
147/// This example shows how serializing a zoned datetime to RFC 2822 format
148/// and then deserializing will drop information:
149///
150/// ```
151/// use jiff::{civil::date, fmt::rfc2822};
152///
153/// let zdt = date(2024, 7, 13)
154/// .at(15, 9, 59, 789_000_000)
155/// .in_tz("America/New_York")?;
156/// // The default format (i.e., Temporal) guarantees lossless
157/// // serialization.
158/// assert_eq!(zdt.to_string(), "2024-07-13T15:09:59.789-04:00[America/New_York]");
159///
160/// let rfc2822 = rfc2822::to_string(&zdt)?;
161/// // Notice that the time zone name and fractional seconds have been dropped!
162/// assert_eq!(rfc2822, "Sat, 13 Jul 2024 15:09:59 -0400");
163/// // And of course, if we parse it back, all that info is still lost.
164/// // Which means this `zdt` cannot do DST safe arithmetic!
165/// let zdt = rfc2822::parse(&rfc2822)?;
166/// assert_eq!(zdt.to_string(), "2024-07-13T15:09:59-04:00[-04:00]");
167///
168/// # Ok::<(), Box<dyn std::error::Error>>(())
169/// ```
170#[inline]
171pub fn parse(string: &str) -> Result<Zoned, Error> {
172 DEFAULT_DATETIME_PARSER.parse_zoned(string)
173}
174
175/// A parser for [RFC 2822] datetimes.
176///
177/// [RFC 2822]: https://datatracker.ietf.org/doc/html/rfc2822
178///
179/// # Warning
180///
181/// The RFC 2822 format only supports writing a precise instant in time
182/// expressed via a time zone offset. It does *not* support serializing
183/// the time zone itself. This means that if you format a zoned datetime
184/// in a time zone like `America/New_York` and then deserialize it, the
185/// zoned datetime you get back will be a "fixed offset" zoned datetime.
186/// This in turn means it will not perform daylight saving time safe
187/// arithmetic.
188///
189/// Basically, you should use the RFC 2822 format if it's required (for
190/// example, when dealing with email). But you should not choose it as a
191/// general interchange format for new applications.
192///
193/// # Example
194///
195/// This example shows how serializing a zoned datetime to RFC 2822 format
196/// and then deserializing will drop information:
197///
198/// ```
199/// use jiff::{civil::date, fmt::rfc2822};
200///
201/// let zdt = date(2024, 7, 13)
202/// .at(15, 9, 59, 789_000_000)
203/// .in_tz("America/New_York")?;
204/// // The default format (i.e., Temporal) guarantees lossless
205/// // serialization.
206/// assert_eq!(zdt.to_string(), "2024-07-13T15:09:59.789-04:00[America/New_York]");
207///
208/// let rfc2822 = rfc2822::to_string(&zdt)?;
209/// // Notice that the time zone name and fractional seconds have been dropped!
210/// assert_eq!(rfc2822, "Sat, 13 Jul 2024 15:09:59 -0400");
211/// // And of course, if we parse it back, all that info is still lost.
212/// // Which means this `zdt` cannot do DST safe arithmetic!
213/// let zdt = rfc2822::parse(&rfc2822)?;
214/// assert_eq!(zdt.to_string(), "2024-07-13T15:09:59-04:00[-04:00]");
215///
216/// # Ok::<(), Box<dyn std::error::Error>>(())
217/// ```
218#[derive(Debug)]
219pub struct DateTimeParser {
220 relaxed_weekday: bool,
221}
222
223impl DateTimeParser {
224 /// Create a new RFC 2822 datetime parser with the default configuration.
225 #[inline]
226 pub const fn new() -> DateTimeParser {
227 DateTimeParser { relaxed_weekday: false }
228 }
229
230 /// When enabled, parsing will permit the weekday to be inconsistent with
231 /// the date. When enabled, the weekday is still parsed and can result in
232 /// an error if it isn't _a_ valid weekday. Only the error checking for
233 /// whether it is _the_ correct weekday for the parsed date is disabled.
234 ///
235 /// This is sometimes useful for interaction with systems that don't do
236 /// strict error checking.
237 ///
238 /// This is disabled by default. And note that RFC 2822 compliance requires
239 /// that the weekday is consistent with the date.
240 ///
241 /// # Example
242 ///
243 /// ```
244 /// use jiff::{civil::date, fmt::rfc2822};
245 ///
246 /// let string = "Sun, 13 Jul 2024 15:09:59 -0400";
247 /// // The above normally results in an error, since 2024-07-13 is a
248 /// // Saturday:
249 /// assert!(rfc2822::parse(string).is_err());
250 /// // But we can relax the error checking:
251 /// static P: rfc2822::DateTimeParser = rfc2822::DateTimeParser::new()
252 /// .relaxed_weekday(true);
253 /// assert_eq!(
254 /// P.parse_zoned(string)?,
255 /// date(2024, 7, 13).at(15, 9, 59, 0).in_tz("America/New_York")?,
256 /// );
257 /// // But note that something that isn't recognized as a valid weekday
258 /// // will still result in an error:
259 /// assert!(P.parse_zoned("Wat, 13 Jul 2024 15:09:59 -0400").is_err());
260 ///
261 /// # Ok::<(), Box<dyn std::error::Error>>(())
262 /// ```
263 #[inline]
264 pub const fn relaxed_weekday(self, yes: bool) -> DateTimeParser {
265 DateTimeParser { relaxed_weekday: yes, ..self }
266 }
267
268 /// Parse a datetime string into a [`Zoned`] value.
269 ///
270 /// Note that RFC 2822 does not support time zone annotations. The zoned
271 /// datetime returned will therefore always have a fixed offset time zone.
272 ///
273 /// # Warning
274 ///
275 /// The RFC 2822 format only supports writing a precise instant in time
276 /// expressed via a time zone offset. It does *not* support serializing
277 /// the time zone itself. This means that if you format a zoned datetime
278 /// in a time zone like `America/New_York` and then deserialize it, the
279 /// zoned datetime you get back will be a "fixed offset" zoned datetime.
280 /// This in turn means it will not perform daylight saving time safe
281 /// arithmetic.
282 ///
283 /// Basically, you should use the RFC 2822 format if it's required (for
284 /// example, when dealing with email). But you should not choose it as a
285 /// general interchange format for new applications.
286 ///
287 /// # Errors
288 ///
289 /// This returns an error if the datetime string given is invalid or if it
290 /// is valid but doesn't fit in the datetime range supported by Jiff. For
291 /// example, RFC 2822 supports offsets up to 99 hours and 59 minutes,
292 /// but Jiff's maximum offset is 25 hours, 59 minutes and 59 seconds.
293 ///
294 /// # Example
295 ///
296 /// This shows a basic example of parsing a `Timestamp` from an RFC 2822
297 /// datetime string.
298 ///
299 /// ```
300 /// use jiff::fmt::rfc2822::DateTimeParser;
301 ///
302 /// static PARSER: DateTimeParser = DateTimeParser::new();
303 ///
304 /// let zdt = PARSER.parse_zoned("Thu, 29 Feb 2024 05:34 -0500")?;
305 /// assert_eq!(zdt.to_string(), "2024-02-29T05:34:00-05:00[-05:00]");
306 ///
307 /// # Ok::<(), Box<dyn std::error::Error>>(())
308 /// ```
309 pub fn parse_zoned<I: AsRef<[u8]>>(
310 &self,
311 input: I,
312 ) -> Result<Zoned, Error> {
313 let input = input.as_ref();
314 let zdt = self
315 .parse_zoned_internal(input)
316 .context(
317 "failed to parse RFC 2822 datetime into Jiff zoned datetime",
318 )?
319 .into_full()?;
320 Ok(zdt)
321 }
322
323 /// Parse an RFC 2822 datetime string into a [`Timestamp`].
324 ///
325 /// # Errors
326 ///
327 /// This returns an error if the datetime string given is invalid or if it
328 /// is valid but doesn't fit in the datetime range supported by Jiff. For
329 /// example, RFC 2822 supports offsets up to 99 hours and 59 minutes,
330 /// but Jiff's maximum offset is 25 hours, 59 minutes and 59 seconds.
331 ///
332 /// # Example
333 ///
334 /// This shows a basic example of parsing a `Timestamp` from an RFC 2822
335 /// datetime string.
336 ///
337 /// ```
338 /// use jiff::fmt::rfc2822::DateTimeParser;
339 ///
340 /// static PARSER: DateTimeParser = DateTimeParser::new();
341 ///
342 /// let timestamp = PARSER.parse_timestamp("Thu, 29 Feb 2024 05:34 -0500")?;
343 /// assert_eq!(timestamp.to_string(), "2024-02-29T10:34:00Z");
344 ///
345 /// # Ok::<(), Box<dyn std::error::Error>>(())
346 /// ```
347 pub fn parse_timestamp<I: AsRef<[u8]>>(
348 &self,
349 input: I,
350 ) -> Result<Timestamp, Error> {
351 let input = input.as_ref();
352 let ts = self
353 .parse_timestamp_internal(input)
354 .context("failed to parse RFC 2822 datetime into Jiff timestamp")?
355 .into_full()?;
356 Ok(ts)
357 }
358
359 /// Parses an RFC 2822 datetime as a zoned datetime.
360 ///
361 /// Note that this doesn't check that the input has been completely
362 /// consumed.
363 #[cfg_attr(feature = "perf-inline", inline(always))]
364 fn parse_zoned_internal<'i>(
365 &self,
366 input: &'i [u8],
367 ) -> Result<Parsed<'i, Zoned>, Error> {
368 let Parsed { value: (dt, offset), input } =
369 self.parse_datetime_offset(input)?;
370 let ts = offset
371 .to_timestamp(dt)
372 .context("RFC 2822 datetime out of Jiff's range")?;
373 let zdt = ts.to_zoned(TimeZone::fixed(offset));
374 Ok(Parsed { value: zdt, input })
375 }
376
377 /// Parses an RFC 2822 datetime as a timestamp.
378 ///
379 /// Note that this doesn't check that the input has been completely
380 /// consumed.
381 #[cfg_attr(feature = "perf-inline", inline(always))]
382 fn parse_timestamp_internal<'i>(
383 &self,
384 input: &'i [u8],
385 ) -> Result<Parsed<'i, Timestamp>, Error> {
386 let Parsed { value: (dt, offset), input } =
387 self.parse_datetime_offset(input)?;
388 let ts = offset
389 .to_timestamp(dt)
390 .context("RFC 2822 datetime out of Jiff's range")?;
391 Ok(Parsed { value: ts, input })
392 }
393
394 /// Parse the entirety of the given input into RFC 2822 components: a civil
395 /// datetime and its offset.
396 ///
397 /// This also consumes any trailing (superfluous) whitespace.
398 #[cfg_attr(feature = "perf-inline", inline(always))]
399 fn parse_datetime_offset<'i>(
400 &self,
401 input: &'i [u8],
402 ) -> Result<Parsed<'i, (DateTime, Offset)>, Error> {
403 let input = input.as_ref();
404 let Parsed { value: dt, input } = self.parse_datetime(input)?;
405 let Parsed { value: offset, input } = self.parse_offset(input)?;
406 let Parsed { input, .. } = self.skip_whitespace(input);
407 let input = if input.is_empty() {
408 input
409 } else {
410 self.skip_comment(input)?.input
411 };
412 Ok(Parsed { value: (dt, offset), input })
413 }
414
415 /// Parses a civil datetime from an RFC 2822 string. The input may have
416 /// leading whitespace.
417 ///
418 /// This also parses and trailing whitespace, including requiring at least
419 /// one whitespace character.
420 ///
421 /// This basically parses everything except for the zone.
422 #[cfg_attr(feature = "perf-inline", inline(always))]
423 fn parse_datetime<'i>(
424 &self,
425 input: &'i [u8],
426 ) -> Result<Parsed<'i, DateTime>, Error> {
427 if input.is_empty() {
428 return Err(err!(
429 "expected RFC 2822 datetime, but got empty string"
430 ));
431 }
432 let Parsed { input, .. } = self.skip_whitespace(input);
433 if input.is_empty() {
434 return Err(err!(
435 "expected RFC 2822 datetime, but got empty string after \
436 trimming whitespace",
437 ));
438 }
439 let Parsed { value: wd, input } = self.parse_weekday(input)?;
440 let Parsed { value: day, input } = self.parse_day(input)?;
441 let Parsed { value: month, input } = self.parse_month(input)?;
442 let Parsed { value: year, input } = self.parse_year(input)?;
443
444 let Parsed { value: hour, input } = self.parse_hour(input)?;
445 let Parsed { input, .. } = self.skip_whitespace(input);
446 let Parsed { input, .. } = self.parse_time_separator(input)?;
447 let Parsed { input, .. } = self.skip_whitespace(input);
448 let Parsed { value: minute, input } = self.parse_minute(input)?;
449
450 let Parsed { value: whitespace_after_minute, input } =
451 self.skip_whitespace(input);
452 let (second, input) = if !input.starts_with(b":") {
453 if !whitespace_after_minute {
454 return Err(err!(
455 "expected whitespace after parsing time: \
456 expected at least one whitespace character \
457 (space or tab), but found none",
458 ));
459 }
460 (t::Second::N::<0>(), input)
461 } else {
462 let Parsed { input, .. } = self.parse_time_separator(input)?;
463 let Parsed { input, .. } = self.skip_whitespace(input);
464 let Parsed { value: second, input } = self.parse_second(input)?;
465 let Parsed { input, .. } =
466 self.parse_whitespace(input).with_context(|| {
467 err!("expected whitespace after parsing time")
468 })?;
469 (second, input)
470 };
471
472 let date =
473 Date::new_ranged(year, month, day).context("invalid date")?;
474 let time = Time::new_ranged(
475 hour,
476 minute,
477 second,
478 t::SubsecNanosecond::N::<0>(),
479 );
480 let dt = DateTime::from_parts(date, time);
481 if let Some(wd) = wd {
482 if !self.relaxed_weekday && wd != dt.weekday() {
483 return Err(err!(
484 "found parsed weekday of {parsed}, \
485 but parsed datetime of {dt} has weekday \
486 {has}",
487 parsed = weekday_abbrev(wd),
488 has = weekday_abbrev(dt.weekday()),
489 ));
490 }
491 }
492 Ok(Parsed { value: dt, input })
493 }
494
495 /// Parses an optional weekday at the beginning of an RFC 2822 datetime.
496 ///
497 /// This expects that any optional whitespace preceding the start of an
498 /// optional day has been stripped and that the input has at least one
499 /// byte.
500 ///
501 /// When the first byte of the given input is a digit (or is empty), then
502 /// this returns `None`, as it implies a day is not present. But if it
503 /// isn't a digit, then we assume that it must be a weekday and return an
504 /// error based on that assumption if we couldn't recognize a weekday.
505 ///
506 /// If a weekday is parsed, then this also skips any trailing whitespace
507 /// (and requires at least one whitespace character).
508 #[cfg_attr(feature = "perf-inline", inline(always))]
509 fn parse_weekday<'i>(
510 &self,
511 input: &'i [u8],
512 ) -> Result<Parsed<'i, Option<Weekday>>, Error> {
513 // An empty input is invalid, but we let that case be
514 // handled by the caller. Otherwise, we know there MUST
515 // be a present day if the first character isn't an ASCII
516 // digit.
517 if matches!(input[0], b'0'..=b'9') {
518 return Ok(Parsed { value: None, input });
519 }
520 if input.len() < 4 {
521 return Err(err!(
522 "expected day at beginning of RFC 2822 datetime \
523 since first non-whitespace byte, {first:?}, \
524 is not a digit, but given string is too short \
525 (length is {length})",
526 first = escape::Byte(input[0]),
527 length = input.len(),
528 ));
529 }
530 let b1 = input[0];
531 let b2 = input[1];
532 let b3 = input[2];
533 let wd = match &[
534 b1.to_ascii_lowercase(),
535 b2.to_ascii_lowercase(),
536 b3.to_ascii_lowercase(),
537 ] {
538 b"sun" => Weekday::Sunday,
539 b"mon" => Weekday::Monday,
540 b"tue" => Weekday::Tuesday,
541 b"wed" => Weekday::Wednesday,
542 b"thu" => Weekday::Thursday,
543 b"fri" => Weekday::Friday,
544 b"sat" => Weekday::Saturday,
545 _ => {
546 return Err(err!(
547 "expected day at beginning of RFC 2822 datetime \
548 since first non-whitespace byte, {first:?}, \
549 is not a digit, but did not recognize {got:?} \
550 as a valid weekday abbreviation",
551 first = escape::Byte(input[0]),
552 got = escape::Bytes(&input[..3]),
553 ));
554 }
555 };
556 let Parsed { input, .. } = self.skip_whitespace(&input[3..]);
557 let Some(should_be_comma) = input.get(0).copied() else {
558 return Err(err!(
559 "expected comma after parsed weekday `{weekday}` in \
560 RFC 2822 datetime, but found end of string instead",
561 weekday = escape::Bytes(&[b1, b2, b3]),
562 ));
563 };
564 if should_be_comma != b',' {
565 return Err(err!(
566 "expected comma after parsed weekday `{weekday}` in \
567 RFC 2822 datetime, but found `{got:?}` instead",
568 weekday = escape::Bytes(&[b1, b2, b3]),
569 got = escape::Byte(should_be_comma),
570 ));
571 }
572 let Parsed { input, .. } = self.skip_whitespace(&input[1..]);
573 Ok(Parsed { value: Some(wd), input })
574 }
575
576 /// Parses a 1 or 2 digit day.
577 ///
578 /// This assumes the input starts with what must be an ASCII digit (or it
579 /// may be empty).
580 ///
581 /// This also parses at least one mandatory whitespace character after the
582 /// day.
583 #[cfg_attr(feature = "perf-inline", inline(always))]
584 fn parse_day<'i>(
585 &self,
586 input: &'i [u8],
587 ) -> Result<Parsed<'i, t::Day>, Error> {
588 if input.is_empty() {
589 return Err(err!("expected day, but found end of input"));
590 }
591 let mut digits = 1;
592 if input.len() >= 2 && matches!(input[1], b'0'..=b'9') {
593 digits = 2;
594 }
595 let (day, input) = input.split_at(digits);
596 let day = parse::i64(day).with_context(|| {
597 err!("failed to parse {day:?} as day", day = escape::Bytes(day))
598 })?;
599 let day = t::Day::try_new("day", day).context("day is not valid")?;
600 let Parsed { input, .. } =
601 self.parse_whitespace(input).with_context(|| {
602 err!("expected whitespace after parsing day {day}")
603 })?;
604 Ok(Parsed { value: day, input })
605 }
606
607 /// Parses an abbreviated month name.
608 ///
609 /// This assumes the input starts with what must be the beginning of a
610 /// month name (or the input may be empty).
611 ///
612 /// This also parses at least one mandatory whitespace character after the
613 /// month name.
614 #[cfg_attr(feature = "perf-inline", inline(always))]
615 fn parse_month<'i>(
616 &self,
617 input: &'i [u8],
618 ) -> Result<Parsed<'i, t::Month>, Error> {
619 if input.is_empty() {
620 return Err(err!(
621 "expected abbreviated month name, but found end of input"
622 ));
623 }
624 if input.len() < 3 {
625 return Err(err!(
626 "expected abbreviated month name, but remaining input \
627 is too short (remaining bytes is {length})",
628 length = input.len(),
629 ));
630 }
631 let b1 = input[0].to_ascii_lowercase();
632 let b2 = input[1].to_ascii_lowercase();
633 let b3 = input[2].to_ascii_lowercase();
634 let month = match &[b1, b2, b3] {
635 b"jan" => 1,
636 b"feb" => 2,
637 b"mar" => 3,
638 b"apr" => 4,
639 b"may" => 5,
640 b"jun" => 6,
641 b"jul" => 7,
642 b"aug" => 8,
643 b"sep" => 9,
644 b"oct" => 10,
645 b"nov" => 11,
646 b"dec" => 12,
647 _ => {
648 return Err(err!(
649 "expected abbreviated month name, \
650 but did not recognize {got:?} \
651 as a valid month",
652 got = escape::Bytes(&input[..3]),
653 ));
654 }
655 };
656 // OK because we just assigned a numeric value ourselves
657 // above, and all values are valid months.
658 let month = t::Month::new(month).unwrap();
659 let Parsed { input, .. } =
660 self.parse_whitespace(&input[3..]).with_context(|| {
661 err!("expected whitespace after parsing month name")
662 })?;
663 Ok(Parsed { value: month, input })
664 }
665
666 /// Parses a 2, 3 or 4 digit year.
667 ///
668 /// This assumes the input starts with what must be an ASCII digit (or it
669 /// may be empty).
670 ///
671 /// This also parses at least one mandatory whitespace character after the
672 /// day.
673 ///
674 /// The 2 or 3 digit years are "obsolete," which we support by following
675 /// the rules in RFC 2822:
676 ///
677 /// > Where a two or three digit year occurs in a date, the year is to be
678 /// > interpreted as follows: If a two digit year is encountered whose
679 /// > value is between 00 and 49, the year is interpreted by adding 2000,
680 /// > ending up with a value between 2000 and 2049. If a two digit year is
681 /// > encountered with a value between 50 and 99, or any three digit year
682 /// > is encountered, the year is interpreted by adding 1900.
683 #[cfg_attr(feature = "perf-inline", inline(always))]
684 fn parse_year<'i>(
685 &self,
686 input: &'i [u8],
687 ) -> Result<Parsed<'i, t::Year>, Error> {
688 let mut digits = 0;
689 while digits <= 3
690 && !input[digits..].is_empty()
691 && matches!(input[digits], b'0'..=b'9')
692 {
693 digits += 1;
694 }
695 if digits <= 1 {
696 return Err(err!(
697 "expected at least two ASCII digits for parsing \
698 a year, but only found {digits}",
699 ));
700 }
701 let (year, input) = input.split_at(digits);
702 let year = parse::i64(year).with_context(|| {
703 err!(
704 "failed to parse {year:?} as year \
705 (a two, three or four digit integer)",
706 year = escape::Bytes(year),
707 )
708 })?;
709 let year = match digits {
710 2 if year <= 49 => year + 2000,
711 2 | 3 => year + 1900,
712 4 => year,
713 _ => unreachable!("digits={digits} must be 2, 3 or 4"),
714 };
715 let year =
716 t::Year::try_new("year", year).context("year is not valid")?;
717 let Parsed { input, .. } = self
718 .parse_whitespace(input)
719 .with_context(|| err!("expected whitespace after parsing year"))?;
720 Ok(Parsed { value: year, input })
721 }
722
723 /// Parses a 2-digit hour. This assumes the input begins with what should
724 /// be an ASCII digit. (i.e., It doesn't trim leading whitespace.)
725 ///
726 /// This parses a mandatory trailing `:`, advancing the input to
727 /// immediately after it.
728 #[cfg_attr(feature = "perf-inline", inline(always))]
729 fn parse_hour<'i>(
730 &self,
731 input: &'i [u8],
732 ) -> Result<Parsed<'i, t::Hour>, Error> {
733 let (hour, input) = parse::split(input, 2).ok_or_else(|| {
734 err!("expected two digit hour, but found end of input")
735 })?;
736 let hour = parse::i64(hour).with_context(|| {
737 err!(
738 "failed to parse {hour:?} as hour (a two digit integer)",
739 hour = escape::Bytes(hour),
740 )
741 })?;
742 let hour =
743 t::Hour::try_new("hour", hour).context("hour is not valid")?;
744 Ok(Parsed { value: hour, input })
745 }
746
747 /// Parses a 2-digit minute. This assumes the input begins with what should
748 /// be an ASCII digit. (i.e., It doesn't trim leading whitespace.)
749 #[cfg_attr(feature = "perf-inline", inline(always))]
750 fn parse_minute<'i>(
751 &self,
752 input: &'i [u8],
753 ) -> Result<Parsed<'i, t::Minute>, Error> {
754 let (minute, input) = parse::split(input, 2).ok_or_else(|| {
755 err!("expected two digit minute, but found end of input")
756 })?;
757 let minute = parse::i64(minute).with_context(|| {
758 err!(
759 "failed to parse {minute:?} as minute (a two digit integer)",
760 minute = escape::Bytes(minute),
761 )
762 })?;
763 let minute = t::Minute::try_new("minute", minute)
764 .context("minute is not valid")?;
765 Ok(Parsed { value: minute, input })
766 }
767
768 /// Parses a 2-digit second. This assumes the input begins with what should
769 /// be an ASCII digit. (i.e., It doesn't trim leading whitespace.)
770 #[cfg_attr(feature = "perf-inline", inline(always))]
771 fn parse_second<'i>(
772 &self,
773 input: &'i [u8],
774 ) -> Result<Parsed<'i, t::Second>, Error> {
775 let (second, input) = parse::split(input, 2).ok_or_else(|| {
776 err!("expected two digit second, but found end of input")
777 })?;
778 let mut second = parse::i64(second).with_context(|| {
779 err!(
780 "failed to parse {second:?} as second (a two digit integer)",
781 second = escape::Bytes(second),
782 )
783 })?;
784 if second == 60 {
785 second = 59;
786 }
787 let second = t::Second::try_new("second", second)
788 .context("second is not valid")?;
789 Ok(Parsed { value: second, input })
790 }
791
792 /// Parses a time zone offset (including obsolete offsets like EDT).
793 ///
794 /// This assumes the offset must begin at the beginning of `input`. That
795 /// is, any leading whitespace should already have been trimmed.
796 #[cfg_attr(feature = "perf-inline", inline(always))]
797 fn parse_offset<'i>(
798 &self,
799 input: &'i [u8],
800 ) -> Result<Parsed<'i, Offset>, Error> {
801 type ParsedOffsetHours = ri8<0, { t::SpanZoneOffsetHours::MAX }>;
802 type ParsedOffsetMinutes = ri8<0, { t::SpanZoneOffsetMinutes::MAX }>;
803
804 let sign = input.get(0).copied().ok_or_else(|| {
805 err!(
806 "expected sign for time zone offset, \
807 (or a legacy time zone name abbreviation), \
808 but found end of input",
809 )
810 })?;
811 let sign = if sign == b'+' {
812 t::Sign::N::<1>()
813 } else if sign == b'-' {
814 t::Sign::N::<-1>()
815 } else {
816 return self.parse_offset_obsolete(input);
817 };
818 let input = &input[1..];
819 let (hhmm, input) = parse::split(input, 4).ok_or_else(|| {
820 err!(
821 "expected at least 4 digits for time zone offset \
822 after sign, but found only {len} bytes remaining",
823 len = input.len(),
824 )
825 })?;
826
827 let hh = parse::i64(&hhmm[0..2]).with_context(|| {
828 err!(
829 "failed to parse hours from time zone offset {hhmm}",
830 hhmm = escape::Bytes(hhmm)
831 )
832 })?;
833 let hh = ParsedOffsetHours::try_new("zone-offset-hours", hh)
834 .context("time zone offset hours are not valid")?;
835 let hh = t::SpanZoneOffset::rfrom(hh);
836
837 let mm = parse::i64(&hhmm[2..4]).with_context(|| {
838 err!(
839 "failed to parse minutes from time zone offset {hhmm}",
840 hhmm = escape::Bytes(hhmm)
841 )
842 })?;
843 let mm = ParsedOffsetMinutes::try_new("zone-offset-minutes", mm)
844 .context("time zone offset minutes are not valid")?;
845 let mm = t::SpanZoneOffset::rfrom(mm);
846
847 let seconds = hh * C(3_600) + mm * C(60);
848 let offset = Offset::from_seconds_ranged(seconds * sign);
849 Ok(Parsed { value: offset, input })
850 }
851
852 /// Parses an obsolete time zone offset.
853 #[inline(never)]
854 fn parse_offset_obsolete<'i>(
855 &self,
856 input: &'i [u8],
857 ) -> Result<Parsed<'i, Offset>, Error> {
858 let mut letters = [0; 5];
859 let mut len = 0;
860 while len <= 4
861 && !input[len..].is_empty()
862 && !is_whitespace(input[len])
863 {
864 letters[len] = input[len].to_ascii_lowercase();
865 len += 1;
866 }
867 if len == 0 {
868 return Err(err!(
869 "expected obsolete RFC 2822 time zone abbreviation, \
870 but found no remaining non-whitespace characters \
871 after time",
872 ));
873 }
874 let offset = match &letters[..len] {
875 b"ut" | b"gmt" | b"z" => Offset::UTC,
876 b"est" => Offset::constant(-5),
877 b"edt" => Offset::constant(-4),
878 b"cst" => Offset::constant(-6),
879 b"cdt" => Offset::constant(-5),
880 b"mst" => Offset::constant(-7),
881 b"mdt" => Offset::constant(-6),
882 b"pst" => Offset::constant(-8),
883 b"pdt" => Offset::constant(-7),
884 name => {
885 if name.len() == 1
886 && matches!(name[0], b'a'..=b'i' | b'k'..=b'z')
887 {
888 // Section 4.3 indicates these as military time:
889 //
890 // > The 1 character military time zones were defined in
891 // > a non-standard way in [RFC822] and are therefore
892 // > unpredictable in their meaning. The original
893 // > definitions of the military zones "A" through "I" are
894 // > equivalent to "+0100" through "+0900" respectively;
895 // > "K", "L", and "M" are equivalent to "+1000", "+1100",
896 // > and "+1200" respectively; "N" through "Y" are
897 // > equivalent to "-0100" through "-1200" respectively;
898 // > and "Z" is equivalent to "+0000". However, because of
899 // > the error in [RFC822], they SHOULD all be considered
900 // > equivalent to "-0000" unless there is out-of-band
901 // > information confirming their meaning.
902 //
903 // So just treat them as UTC.
904 Offset::UTC
905 } else if name.len() >= 3
906 && name.iter().all(|&b| matches!(b, b'a'..=b'z'))
907 {
908 // Section 4.3 also says that anything that _looks_ like a
909 // zone name should just be -0000 too:
910 //
911 // > Other multi-character (usually between 3 and 5)
912 // > alphabetic time zones have been used in Internet
913 // > messages. Any such time zone whose meaning is not
914 // > known SHOULD be considered equivalent to "-0000"
915 // > unless there is out-of-band information confirming
916 // > their meaning.
917 Offset::UTC
918 } else {
919 // But anything else we throw our hands up I guess.
920 return Err(err!(
921 "expected obsolete RFC 2822 time zone abbreviation, \
922 but found {found:?}",
923 found = escape::Bytes(&input[..len]),
924 ));
925 }
926 }
927 };
928 Ok(Parsed { value: offset, input: &input[len..] })
929 }
930
931 /// Parses a time separator. This returns an error if one couldn't be
932 /// found.
933 #[cfg_attr(feature = "perf-inline", inline(always))]
934 fn parse_time_separator<'i>(
935 &self,
936 input: &'i [u8],
937 ) -> Result<Parsed<'i, ()>, Error> {
938 if input.is_empty() {
939 return Err(err!(
940 "expected time separator of ':', but found end of input",
941 ));
942 }
943 if input[0] != b':' {
944 return Err(err!(
945 "expected time separator of ':', but found {got}",
946 got = escape::Byte(input[0]),
947 ));
948 }
949 Ok(Parsed { value: (), input: &input[1..] })
950 }
951
952 /// Parses at least one whitespace character. If no whitespace was found,
953 /// then this returns an error.
954 #[cfg_attr(feature = "perf-inline", inline(always))]
955 fn parse_whitespace<'i>(
956 &self,
957 input: &'i [u8],
958 ) -> Result<Parsed<'i, ()>, Error> {
959 let Parsed { input, value: had_whitespace } =
960 self.skip_whitespace(input);
961 if !had_whitespace {
962 return Err(err!(
963 "expected at least one whitespace character (space or tab), \
964 but found none",
965 ));
966 }
967 Ok(Parsed { value: (), input })
968 }
969
970 /// Skips over any ASCII whitespace at the beginning of `input`.
971 ///
972 /// This returns the input unchanged if it does not begin with whitespace.
973 /// The resulting value is `true` if any whitespace was consumed,
974 /// and `false` if none was.
975 #[cfg_attr(feature = "perf-inline", inline(always))]
976 fn skip_whitespace<'i>(&self, mut input: &'i [u8]) -> Parsed<'i, bool> {
977 let mut found_whitespace = false;
978 while input.first().map_or(false, |&b| is_whitespace(b)) {
979 input = &input[1..];
980 found_whitespace = true;
981 }
982 Parsed { value: found_whitespace, input }
983 }
984
985 /// This attempts to parse and skip any trailing "comment" in an RFC 2822
986 /// datetime.
987 ///
988 /// This is a bit more relaxed than what RFC 2822 specifies. We basically
989 /// just try to balance parenthesis and skip over escapes.
990 ///
991 /// This assumes that if a comment exists, its opening parenthesis is at
992 /// the beginning of `input`. That is, any leading whitespace has been
993 /// stripped.
994 #[inline(never)]
995 fn skip_comment<'i>(
996 &self,
997 mut input: &'i [u8],
998 ) -> Result<Parsed<'i, ()>, Error> {
999 if !input.starts_with(b"(") {
1000 return Ok(Parsed { value: (), input });
1001 }
1002 input = &input[1..];
1003 let mut depth: u8 = 1;
1004 let mut escape = false;
1005 for byte in input.iter().copied() {
1006 input = &input[1..];
1007 if escape {
1008 escape = false;
1009 } else if byte == b'\\' {
1010 escape = true;
1011 } else if byte == b')' {
1012 // I believe this error case is actually impossible, since as
1013 // soon as we hit 0, we break out. If there is more "comment,"
1014 // then it will flag an error as unparsed input.
1015 depth = depth.checked_sub(1).ok_or_else(|| {
1016 err!(
1017 "found closing parenthesis in comment with \
1018 no matching opening parenthesis"
1019 )
1020 })?;
1021 if depth == 0 {
1022 break;
1023 }
1024 } else if byte == b'(' {
1025 depth = depth.checked_add(1).ok_or_else(|| {
1026 err!("found too many nested parenthesis in comment")
1027 })?;
1028 }
1029 }
1030 if depth > 0 {
1031 return Err(err!(
1032 "found opening parenthesis in comment with \
1033 no matching closing parenthesis"
1034 ));
1035 }
1036 let Parsed { input, .. } = self.skip_whitespace(input);
1037 Ok(Parsed { value: (), input })
1038 }
1039}
1040
1041/// A printer for [RFC 2822] datetimes.
1042///
1043/// This printer converts an in memory representation of a precise instant in
1044/// time to an RFC 2822 formatted string. That is, [`Zoned`] or [`Timestamp`],
1045/// since all other datetime types in Jiff are inexact.
1046///
1047/// [RFC 2822]: https://datatracker.ietf.org/doc/html/rfc2822
1048///
1049/// # Warning
1050///
1051/// The RFC 2822 format only supports writing a precise instant in time
1052/// expressed via a time zone offset. It does *not* support serializing
1053/// the time zone itself. This means that if you format a zoned datetime
1054/// in a time zone like `America/New_York` and then deserialize it, the
1055/// zoned datetime you get back will be a "fixed offset" zoned datetime.
1056/// This in turn means it will not perform daylight saving time safe
1057/// arithmetic.
1058///
1059/// Basically, you should use the RFC 2822 format if it's required (for
1060/// example, when dealing with email). But you should not choose it as a
1061/// general interchange format for new applications.
1062///
1063/// # Example
1064///
1065/// This example shows how to convert a zoned datetime to the RFC 2822 format:
1066///
1067/// ```
1068/// use jiff::{civil::date, fmt::rfc2822::DateTimePrinter};
1069///
1070/// const PRINTER: DateTimePrinter = DateTimePrinter::new();
1071///
1072/// let zdt = date(2024, 6, 15).at(7, 0, 0, 0).in_tz("Australia/Tasmania")?;
1073///
1074/// let mut buf = String::new();
1075/// PRINTER.print_zoned(&zdt, &mut buf)?;
1076/// assert_eq!(buf, "Sat, 15 Jun 2024 07:00:00 +1000");
1077///
1078/// # Ok::<(), Box<dyn std::error::Error>>(())
1079/// ```
1080///
1081/// # Example: using adapters with `std::io::Write` and `std::fmt::Write`
1082///
1083/// By using the [`StdIoWrite`](super::StdIoWrite) and
1084/// [`StdFmtWrite`](super::StdFmtWrite) adapters, one can print datetimes
1085/// directly to implementations of `std::io::Write` and `std::fmt::Write`,
1086/// respectively. The example below demonstrates writing to anything
1087/// that implements `std::io::Write`. Similar code can be written for
1088/// `std::fmt::Write`.
1089///
1090/// ```no_run
1091/// use std::{fs::File, io::{BufWriter, Write}, path::Path};
1092///
1093/// use jiff::{civil::date, fmt::{StdIoWrite, rfc2822::DateTimePrinter}};
1094///
1095/// let zdt = date(2024, 6, 15).at(7, 0, 0, 0).in_tz("Asia/Kolkata")?;
1096///
1097/// let path = Path::new("/tmp/output");
1098/// let mut file = BufWriter::new(File::create(path)?);
1099/// DateTimePrinter::new().print_zoned(&zdt, StdIoWrite(&mut file)).unwrap();
1100/// file.flush()?;
1101/// assert_eq!(
1102/// std::fs::read_to_string(path)?,
1103/// "Sat, 15 Jun 2024 07:00:00 +0530",
1104/// );
1105///
1106/// # Ok::<(), Box<dyn std::error::Error>>(())
1107/// ```
1108#[derive(Debug)]
1109pub struct DateTimePrinter {
1110 // The RFC 2822 printer has no configuration at present.
1111 _private: (),
1112}
1113
1114impl DateTimePrinter {
1115 /// Create a new RFC 2822 datetime printer with the default configuration.
1116 #[inline]
1117 pub const fn new() -> DateTimePrinter {
1118 DateTimePrinter { _private: () }
1119 }
1120
1121 /// Format a `Zoned` datetime into a string.
1122 ///
1123 /// This never emits `-0000` as the offset in the RFC 2822 format. If you
1124 /// desire a `-0000` offset, use [`DateTimePrinter::print_timestamp`] via
1125 /// [`Zoned::timestamp`].
1126 ///
1127 /// Moreover, since RFC 2822 does not support fractional seconds, this
1128 /// routine prints the zoned datetime as if truncating any fractional
1129 /// seconds.
1130 ///
1131 /// This is a convenience routine for [`DateTimePrinter::print_zoned`]
1132 /// with a `String`.
1133 ///
1134 /// # Warning
1135 ///
1136 /// The RFC 2822 format only supports writing a precise instant in time
1137 /// expressed via a time zone offset. It does *not* support serializing
1138 /// the time zone itself. This means that if you format a zoned datetime
1139 /// in a time zone like `America/New_York` and then deserialize it, the
1140 /// zoned datetime you get back will be a "fixed offset" zoned datetime.
1141 /// This in turn means it will not perform daylight saving time safe
1142 /// arithmetic.
1143 ///
1144 /// Basically, you should use the RFC 2822 format if it's required (for
1145 /// example, when dealing with email). But you should not choose it as a
1146 /// general interchange format for new applications.
1147 ///
1148 /// # Errors
1149 ///
1150 /// This can return an error if the year corresponding to this timestamp
1151 /// cannot be represented in the RFC 2822 format. For example, a negative
1152 /// year.
1153 ///
1154 /// # Example
1155 ///
1156 /// ```
1157 /// use jiff::{civil::date, fmt::rfc2822::DateTimePrinter};
1158 ///
1159 /// const PRINTER: DateTimePrinter = DateTimePrinter::new();
1160 ///
1161 /// let zdt = date(2024, 6, 15).at(7, 0, 0, 0).in_tz("America/New_York")?;
1162 /// assert_eq!(
1163 /// PRINTER.zoned_to_string(&zdt)?,
1164 /// "Sat, 15 Jun 2024 07:00:00 -0400",
1165 /// );
1166 ///
1167 /// # Ok::<(), Box<dyn std::error::Error>>(())
1168 /// ```
1169 #[cfg(feature = "alloc")]
1170 pub fn zoned_to_string(
1171 &self,
1172 zdt: &Zoned,
1173 ) -> Result<alloc::string::String, Error> {
1174 let mut buf = alloc::string::String::with_capacity(4);
1175 self.print_zoned(zdt, &mut buf)?;
1176 Ok(buf)
1177 }
1178
1179 /// Format a `Timestamp` datetime into a string.
1180 ///
1181 /// This always emits `-0000` as the offset in the RFC 2822 format. If you
1182 /// desire a `+0000` offset, use [`DateTimePrinter::print_zoned`] with a
1183 /// zoned datetime with [`TimeZone::UTC`].
1184 ///
1185 /// Moreover, since RFC 2822 does not support fractional seconds, this
1186 /// routine prints the timestamp as if truncating any fractional seconds.
1187 ///
1188 /// This is a convenience routine for [`DateTimePrinter::print_timestamp`]
1189 /// with a `String`.
1190 ///
1191 /// # Errors
1192 ///
1193 /// This returns an error if the year corresponding to this
1194 /// timestamp cannot be represented in the RFC 2822 format. For example, a
1195 /// negative year.
1196 ///
1197 /// # Example
1198 ///
1199 /// ```
1200 /// use jiff::{fmt::rfc2822::DateTimePrinter, Timestamp};
1201 ///
1202 /// let timestamp = Timestamp::from_second(1)
1203 /// .expect("one second after Unix epoch is always valid");
1204 /// assert_eq!(
1205 /// DateTimePrinter::new().timestamp_to_string(×tamp)?,
1206 /// "Thu, 1 Jan 1970 00:00:01 -0000",
1207 /// );
1208 ///
1209 /// # Ok::<(), Box<dyn std::error::Error>>(())
1210 /// ```
1211 #[cfg(feature = "alloc")]
1212 pub fn timestamp_to_string(
1213 &self,
1214 timestamp: &Timestamp,
1215 ) -> Result<alloc::string::String, Error> {
1216 let mut buf = alloc::string::String::with_capacity(4);
1217 self.print_timestamp(timestamp, &mut buf)?;
1218 Ok(buf)
1219 }
1220
1221 /// Format a `Timestamp` datetime into a string in a way that is explicitly
1222 /// compatible with [RFC 9110]. This is typically useful in contexts where
1223 /// strict compatibility with HTTP is desired.
1224 ///
1225 /// This always emits `GMT` as the offset and always uses two digits for
1226 /// the day. This results in a fixed length format that always uses 29
1227 /// characters.
1228 ///
1229 /// Since neither RFC 2822 nor RFC 9110 supports fractional seconds, this
1230 /// routine prints the timestamp as if truncating any fractional seconds.
1231 ///
1232 /// This is a convenience routine for
1233 /// [`DateTimePrinter::print_timestamp_rfc9110`] with a `String`.
1234 ///
1235 /// # Errors
1236 ///
1237 /// This returns an error if the year corresponding to this timestamp
1238 /// cannot be represented in the RFC 2822 or RFC 9110 format. For example,
1239 /// a negative year.
1240 ///
1241 /// # Example
1242 ///
1243 /// ```
1244 /// use jiff::{fmt::rfc2822::DateTimePrinter, Timestamp};
1245 ///
1246 /// let timestamp = Timestamp::from_second(1)
1247 /// .expect("one second after Unix epoch is always valid");
1248 /// assert_eq!(
1249 /// DateTimePrinter::new().timestamp_to_rfc9110_string(×tamp)?,
1250 /// "Thu, 01 Jan 1970 00:00:01 GMT",
1251 /// );
1252 ///
1253 /// # Ok::<(), Box<dyn std::error::Error>>(())
1254 /// ```
1255 ///
1256 /// [RFC 9110]: https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.7-15
1257 #[cfg(feature = "alloc")]
1258 pub fn timestamp_to_rfc9110_string(
1259 &self,
1260 timestamp: &Timestamp,
1261 ) -> Result<alloc::string::String, Error> {
1262 let mut buf = alloc::string::String::with_capacity(29);
1263 self.print_timestamp_rfc9110(timestamp, &mut buf)?;
1264 Ok(buf)
1265 }
1266
1267 /// Print a `Zoned` datetime to the given writer.
1268 ///
1269 /// This never emits `-0000` as the offset in the RFC 2822 format. If you
1270 /// desire a `-0000` offset, use [`DateTimePrinter::print_timestamp`] via
1271 /// [`Zoned::timestamp`].
1272 ///
1273 /// Moreover, since RFC 2822 does not support fractional seconds, this
1274 /// routine prints the zoned datetime as if truncating any fractional
1275 /// seconds.
1276 ///
1277 /// # Warning
1278 ///
1279 /// The RFC 2822 format only supports writing a precise instant in time
1280 /// expressed via a time zone offset. It does *not* support serializing
1281 /// the time zone itself. This means that if you format a zoned datetime
1282 /// in a time zone like `America/New_York` and then deserialize it, the
1283 /// zoned datetime you get back will be a "fixed offset" zoned datetime.
1284 /// This in turn means it will not perform daylight saving time safe
1285 /// arithmetic.
1286 ///
1287 /// Basically, you should use the RFC 2822 format if it's required (for
1288 /// example, when dealing with email). But you should not choose it as a
1289 /// general interchange format for new applications.
1290 ///
1291 /// # Errors
1292 ///
1293 /// This returns an error when writing to the given [`Write`]
1294 /// implementation would fail. Some such implementations, like for `String`
1295 /// and `Vec<u8>`, never fail (unless memory allocation fails).
1296 ///
1297 /// This can also return an error if the year corresponding to this
1298 /// timestamp cannot be represented in the RFC 2822 format. For example, a
1299 /// negative year.
1300 ///
1301 /// # Example
1302 ///
1303 /// ```
1304 /// use jiff::{civil::date, fmt::rfc2822::DateTimePrinter};
1305 ///
1306 /// const PRINTER: DateTimePrinter = DateTimePrinter::new();
1307 ///
1308 /// let zdt = date(2024, 6, 15).at(7, 0, 0, 0).in_tz("America/New_York")?;
1309 ///
1310 /// let mut buf = String::new();
1311 /// PRINTER.print_zoned(&zdt, &mut buf)?;
1312 /// assert_eq!(buf, "Sat, 15 Jun 2024 07:00:00 -0400");
1313 ///
1314 /// # Ok::<(), Box<dyn std::error::Error>>(())
1315 /// ```
1316 pub fn print_zoned<W: Write>(
1317 &self,
1318 zdt: &Zoned,
1319 wtr: W,
1320 ) -> Result<(), Error> {
1321 self.print_civil_with_offset(zdt.datetime(), Some(zdt.offset()), wtr)
1322 }
1323
1324 /// Print a `Timestamp` datetime to the given writer.
1325 ///
1326 /// This always emits `-0000` as the offset in the RFC 2822 format. If you
1327 /// desire a `+0000` offset, use [`DateTimePrinter::print_zoned`] with a
1328 /// zoned datetime with [`TimeZone::UTC`].
1329 ///
1330 /// Moreover, since RFC 2822 does not support fractional seconds, this
1331 /// routine prints the timestamp as if truncating any fractional seconds.
1332 ///
1333 /// # Errors
1334 ///
1335 /// This returns an error when writing to the given [`Write`]
1336 /// implementation would fail. Some such implementations, like for `String`
1337 /// and `Vec<u8>`, never fail (unless memory allocation fails).
1338 ///
1339 /// This can also return an error if the year corresponding to this
1340 /// timestamp cannot be represented in the RFC 2822 format. For example, a
1341 /// negative year.
1342 ///
1343 /// # Example
1344 ///
1345 /// ```
1346 /// use jiff::{fmt::rfc2822::DateTimePrinter, Timestamp};
1347 ///
1348 /// let timestamp = Timestamp::from_second(1)
1349 /// .expect("one second after Unix epoch is always valid");
1350 ///
1351 /// let mut buf = String::new();
1352 /// DateTimePrinter::new().print_timestamp(×tamp, &mut buf)?;
1353 /// assert_eq!(buf, "Thu, 1 Jan 1970 00:00:01 -0000");
1354 ///
1355 /// # Ok::<(), Box<dyn std::error::Error>>(())
1356 /// ```
1357 pub fn print_timestamp<W: Write>(
1358 &self,
1359 timestamp: &Timestamp,
1360 wtr: W,
1361 ) -> Result<(), Error> {
1362 let dt = TimeZone::UTC.to_datetime(*timestamp);
1363 self.print_civil_with_offset(dt, None, wtr)
1364 }
1365
1366 /// Print a `Timestamp` datetime to the given writer in a way that is
1367 /// explicitly compatible with [RFC 9110]. This is typically useful in
1368 /// contexts where strict compatibility with HTTP is desired.
1369 ///
1370 /// This always emits `GMT` as the offset and always uses two digits for
1371 /// the day. This results in a fixed length format that always uses 29
1372 /// characters.
1373 ///
1374 /// Since neither RFC 2822 nor RFC 9110 supports fractional seconds, this
1375 /// routine prints the timestamp as if truncating any fractional seconds.
1376 ///
1377 /// # Errors
1378 ///
1379 /// This returns an error when writing to the given [`Write`]
1380 /// implementation would fail. Some such implementations, like for `String`
1381 /// and `Vec<u8>`, never fail (unless memory allocation fails).
1382 ///
1383 /// This can also return an error if the year corresponding to this
1384 /// timestamp cannot be represented in the RFC 2822 or RFC 9110 format. For
1385 /// example, a negative year.
1386 ///
1387 /// # Example
1388 ///
1389 /// ```
1390 /// use jiff::{fmt::rfc2822::DateTimePrinter, Timestamp};
1391 ///
1392 /// let timestamp = Timestamp::from_second(1)
1393 /// .expect("one second after Unix epoch is always valid");
1394 ///
1395 /// let mut buf = String::new();
1396 /// DateTimePrinter::new().print_timestamp_rfc9110(×tamp, &mut buf)?;
1397 /// assert_eq!(buf, "Thu, 01 Jan 1970 00:00:01 GMT");
1398 ///
1399 /// # Ok::<(), Box<dyn std::error::Error>>(())
1400 /// ```
1401 ///
1402 /// [RFC 9110]: https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.7-15
1403 pub fn print_timestamp_rfc9110<W: Write>(
1404 &self,
1405 timestamp: &Timestamp,
1406 wtr: W,
1407 ) -> Result<(), Error> {
1408 self.print_civil_always_utc(timestamp, wtr)
1409 }
1410
1411 fn print_civil_with_offset<W: Write>(
1412 &self,
1413 dt: DateTime,
1414 offset: Option<Offset>,
1415 mut wtr: W,
1416 ) -> Result<(), Error> {
1417 static FMT_DAY: DecimalFormatter = DecimalFormatter::new();
1418 static FMT_YEAR: DecimalFormatter = DecimalFormatter::new().padding(4);
1419 static FMT_TIME_UNIT: DecimalFormatter =
1420 DecimalFormatter::new().padding(2);
1421
1422 if dt.year() < 0 {
1423 // RFC 2822 actually says the year must be at least 1900, but
1424 // other implementations (like Chrono) allow any positive 4-digit
1425 // year.
1426 return Err(err!(
1427 "datetime {dt} has negative year, \
1428 which cannot be formatted with RFC 2822",
1429 ));
1430 }
1431
1432 wtr.write_str(weekday_abbrev(dt.weekday()))?;
1433 wtr.write_str(", ")?;
1434 wtr.write_int(&FMT_DAY, dt.day())?;
1435 wtr.write_str(" ")?;
1436 wtr.write_str(month_name(dt.month()))?;
1437 wtr.write_str(" ")?;
1438 wtr.write_int(&FMT_YEAR, dt.year())?;
1439 wtr.write_str(" ")?;
1440 wtr.write_int(&FMT_TIME_UNIT, dt.hour())?;
1441 wtr.write_str(":")?;
1442 wtr.write_int(&FMT_TIME_UNIT, dt.minute())?;
1443 wtr.write_str(":")?;
1444 wtr.write_int(&FMT_TIME_UNIT, dt.second())?;
1445 wtr.write_str(" ")?;
1446
1447 let Some(offset) = offset else {
1448 wtr.write_str("-0000")?;
1449 return Ok(());
1450 };
1451 wtr.write_str(if offset.is_negative() { "-" } else { "+" })?;
1452 let mut hours = offset.part_hours_ranged().abs().get();
1453 let mut minutes = offset.part_minutes_ranged().abs().get();
1454 // RFC 2822, like RFC 3339, requires that time zone offsets are an
1455 // integral number of minutes. While rounding based on seconds doesn't
1456 // seem clearly indicated, we choose to do that here. An alternative
1457 // would be to return an error. It isn't clear how important this is in
1458 // practice though.
1459 if offset.part_seconds_ranged().abs() >= C(30) {
1460 if minutes == 59 {
1461 hours = hours.saturating_add(1);
1462 minutes = 0;
1463 } else {
1464 minutes = minutes.saturating_add(1);
1465 }
1466 }
1467 wtr.write_int(&FMT_TIME_UNIT, hours)?;
1468 wtr.write_int(&FMT_TIME_UNIT, minutes)?;
1469 Ok(())
1470 }
1471
1472 fn print_civil_always_utc<W: Write>(
1473 &self,
1474 timestamp: &Timestamp,
1475 mut wtr: W,
1476 ) -> Result<(), Error> {
1477 static FMT_DAY: DecimalFormatter = DecimalFormatter::new().padding(2);
1478 static FMT_YEAR: DecimalFormatter = DecimalFormatter::new().padding(4);
1479 static FMT_TIME_UNIT: DecimalFormatter =
1480 DecimalFormatter::new().padding(2);
1481
1482 let dt = TimeZone::UTC.to_datetime(*timestamp);
1483 if dt.year() < 0 {
1484 // RFC 2822 actually says the year must be at least 1900, but
1485 // other implementations (like Chrono) allow any positive 4-digit
1486 // year.
1487 return Err(err!(
1488 "datetime {dt} has negative year, \
1489 which cannot be formatted with RFC 2822",
1490 ));
1491 }
1492
1493 wtr.write_str(weekday_abbrev(dt.weekday()))?;
1494 wtr.write_str(", ")?;
1495 wtr.write_int(&FMT_DAY, dt.day())?;
1496 wtr.write_str(" ")?;
1497 wtr.write_str(month_name(dt.month()))?;
1498 wtr.write_str(" ")?;
1499 wtr.write_int(&FMT_YEAR, dt.year())?;
1500 wtr.write_str(" ")?;
1501 wtr.write_int(&FMT_TIME_UNIT, dt.hour())?;
1502 wtr.write_str(":")?;
1503 wtr.write_int(&FMT_TIME_UNIT, dt.minute())?;
1504 wtr.write_str(":")?;
1505 wtr.write_int(&FMT_TIME_UNIT, dt.second())?;
1506 wtr.write_str(" ")?;
1507 wtr.write_str("GMT")?;
1508 Ok(())
1509 }
1510}
1511
1512fn weekday_abbrev(wd: Weekday) -> &'static str {
1513 match wd {
1514 Weekday::Sunday => "Sun",
1515 Weekday::Monday => "Mon",
1516 Weekday::Tuesday => "Tue",
1517 Weekday::Wednesday => "Wed",
1518 Weekday::Thursday => "Thu",
1519 Weekday::Friday => "Fri",
1520 Weekday::Saturday => "Sat",
1521 }
1522}
1523
1524fn month_name(month: i8) -> &'static str {
1525 match month {
1526 1 => "Jan",
1527 2 => "Feb",
1528 3 => "Mar",
1529 4 => "Apr",
1530 5 => "May",
1531 6 => "Jun",
1532 7 => "Jul",
1533 8 => "Aug",
1534 9 => "Sep",
1535 10 => "Oct",
1536 11 => "Nov",
1537 12 => "Dec",
1538 _ => unreachable!("invalid month value {month}"),
1539 }
1540}
1541
1542/// Returns true if the given byte is "whitespace" as defined by RFC 2822.
1543///
1544/// From S2.2.2:
1545///
1546/// > Many of these tokens are allowed (according to their syntax) to be
1547/// > introduced or end with comments (as described in section 3.2.3) as well
1548/// > as the space (SP, ASCII value 32) and horizontal tab (HTAB, ASCII value
1549/// > 9) characters (together known as the white space characters, WSP), and
1550/// > those WSP characters are subject to header "folding" and "unfolding" as
1551/// > described in section 2.2.3.
1552///
1553/// In other words, ASCII space or tab.
1554///
1555/// With all that said, it seems odd to limit this to just spaces or tabs, so
1556/// we relax this and let it absorb any kind of ASCII whitespace. This also
1557/// handles, I believe, most cases of "folding" whitespace. (By treating `\r`
1558/// and `\n` as whitespace.)
1559fn is_whitespace(byte: u8) -> bool {
1560 byte.is_ascii_whitespace()
1561}
1562
1563#[cfg(feature = "alloc")]
1564#[cfg(test)]
1565mod tests {
1566 use alloc::string::{String, ToString};
1567
1568 use crate::civil::date;
1569
1570 use super::*;
1571
1572 #[test]
1573 fn ok_parse_basic() {
1574 let p = |input| DateTimeParser::new().parse_zoned(input).unwrap();
1575
1576 insta::assert_debug_snapshot!(
1577 p("Wed, 10 Jan 2024 05:34:45 -0500"),
1578 @"2024-01-10T05:34:45-05:00[-05:00]",
1579 );
1580 insta::assert_debug_snapshot!(
1581 p("Tue, 9 Jan 2024 05:34:45 -0500"),
1582 @"2024-01-09T05:34:45-05:00[-05:00]",
1583 );
1584 insta::assert_debug_snapshot!(
1585 p("Tue, 09 Jan 2024 05:34:45 -0500"),
1586 @"2024-01-09T05:34:45-05:00[-05:00]",
1587 );
1588 insta::assert_debug_snapshot!(
1589 p("10 Jan 2024 05:34:45 -0500"),
1590 @"2024-01-10T05:34:45-05:00[-05:00]",
1591 );
1592 insta::assert_debug_snapshot!(
1593 p("10 Jan 2024 05:34 -0500"),
1594 @"2024-01-10T05:34:00-05:00[-05:00]",
1595 );
1596 insta::assert_debug_snapshot!(
1597 p("10 Jan 2024 05:34:45 +0500"),
1598 @"2024-01-10T05:34:45+05:00[+05:00]",
1599 );
1600 insta::assert_debug_snapshot!(
1601 p("Thu, 29 Feb 2024 05:34 -0500"),
1602 @"2024-02-29T05:34:00-05:00[-05:00]",
1603 );
1604
1605 // leap second constraining
1606 insta::assert_debug_snapshot!(
1607 p("10 Jan 2024 05:34:60 -0500"),
1608 @"2024-01-10T05:34:59-05:00[-05:00]",
1609 );
1610 }
1611
1612 #[test]
1613 fn ok_parse_obsolete_zone() {
1614 let p = |input| DateTimeParser::new().parse_zoned(input).unwrap();
1615
1616 insta::assert_debug_snapshot!(
1617 p("Wed, 10 Jan 2024 05:34:45 EST"),
1618 @"2024-01-10T05:34:45-05:00[-05:00]",
1619 );
1620 insta::assert_debug_snapshot!(
1621 p("Wed, 10 Jan 2024 05:34:45 EDT"),
1622 @"2024-01-10T05:34:45-04:00[-04:00]",
1623 );
1624 insta::assert_debug_snapshot!(
1625 p("Wed, 10 Jan 2024 05:34:45 CST"),
1626 @"2024-01-10T05:34:45-06:00[-06:00]",
1627 );
1628 insta::assert_debug_snapshot!(
1629 p("Wed, 10 Jan 2024 05:34:45 CDT"),
1630 @"2024-01-10T05:34:45-05:00[-05:00]",
1631 );
1632 insta::assert_debug_snapshot!(
1633 p("Wed, 10 Jan 2024 05:34:45 mst"),
1634 @"2024-01-10T05:34:45-07:00[-07:00]",
1635 );
1636 insta::assert_debug_snapshot!(
1637 p("Wed, 10 Jan 2024 05:34:45 mdt"),
1638 @"2024-01-10T05:34:45-06:00[-06:00]",
1639 );
1640 insta::assert_debug_snapshot!(
1641 p("Wed, 10 Jan 2024 05:34:45 pst"),
1642 @"2024-01-10T05:34:45-08:00[-08:00]",
1643 );
1644 insta::assert_debug_snapshot!(
1645 p("Wed, 10 Jan 2024 05:34:45 pdt"),
1646 @"2024-01-10T05:34:45-07:00[-07:00]",
1647 );
1648
1649 // Various things that mean UTC.
1650 insta::assert_debug_snapshot!(
1651 p("Wed, 10 Jan 2024 05:34:45 UT"),
1652 @"2024-01-10T05:34:45+00:00[UTC]",
1653 );
1654 insta::assert_debug_snapshot!(
1655 p("Wed, 10 Jan 2024 05:34:45 Z"),
1656 @"2024-01-10T05:34:45+00:00[UTC]",
1657 );
1658 insta::assert_debug_snapshot!(
1659 p("Wed, 10 Jan 2024 05:34:45 gmt"),
1660 @"2024-01-10T05:34:45+00:00[UTC]",
1661 );
1662
1663 // Even things that are unrecognized just get treated as having
1664 // an offset of 0.
1665 insta::assert_debug_snapshot!(
1666 p("Wed, 10 Jan 2024 05:34:45 XXX"),
1667 @"2024-01-10T05:34:45+00:00[UTC]",
1668 );
1669 insta::assert_debug_snapshot!(
1670 p("Wed, 10 Jan 2024 05:34:45 ABCDE"),
1671 @"2024-01-10T05:34:45+00:00[UTC]",
1672 );
1673 insta::assert_debug_snapshot!(
1674 p("Wed, 10 Jan 2024 05:34:45 FUCK"),
1675 @"2024-01-10T05:34:45+00:00[UTC]",
1676 );
1677 }
1678
1679 // whyyyyyyyyyyyyy
1680 #[test]
1681 fn ok_parse_comment() {
1682 let p = |input| DateTimeParser::new().parse_zoned(input).unwrap();
1683
1684 insta::assert_debug_snapshot!(
1685 p("Wed, 10 Jan 2024 05:34:45 -0500 (wat)"),
1686 @"2024-01-10T05:34:45-05:00[-05:00]",
1687 );
1688 insta::assert_debug_snapshot!(
1689 p("Wed, 10 Jan 2024 05:34:45 -0500 (w(a)t)"),
1690 @"2024-01-10T05:34:45-05:00[-05:00]",
1691 );
1692 insta::assert_debug_snapshot!(
1693 p(r"Wed, 10 Jan 2024 05:34:45 -0500 (w\(a\)t)"),
1694 @"2024-01-10T05:34:45-05:00[-05:00]",
1695 );
1696 }
1697
1698 #[test]
1699 fn ok_parse_whitespace() {
1700 let p = |input| DateTimeParser::new().parse_zoned(input).unwrap();
1701
1702 insta::assert_debug_snapshot!(
1703 p("Wed, 10 \t Jan \n\r\n\n 2024 05:34:45 -0500"),
1704 @"2024-01-10T05:34:45-05:00[-05:00]",
1705 );
1706 insta::assert_debug_snapshot!(
1707 p("Wed, 10 Jan 2024 05:34:45 -0500 "),
1708 @"2024-01-10T05:34:45-05:00[-05:00]",
1709 );
1710 // Whitespace around the comma is optional
1711 insta::assert_debug_snapshot!(
1712 p("Wed,10 Jan 2024 05:34:45 -0500"),
1713 @"2024-01-10T05:34:45-05:00[-05:00]",
1714 );
1715 insta::assert_debug_snapshot!(
1716 p("Wed , 10 Jan 2024 05:34:45 -0500"),
1717 @"2024-01-10T05:34:45-05:00[-05:00]",
1718 );
1719 insta::assert_debug_snapshot!(
1720 p("Wed ,10 Jan 2024 05:34:45 -0500"),
1721 @"2024-01-10T05:34:45-05:00[-05:00]",
1722 );
1723 // Whitespace is allowed around the time components
1724 insta::assert_debug_snapshot!(
1725 p("Wed, 10 Jan 2024 05 :34: 45 -0500"),
1726 @"2024-01-10T05:34:45-05:00[-05:00]",
1727 );
1728 insta::assert_debug_snapshot!(
1729 p("Wed, 10 Jan 2024 05: 34 :45 -0500"),
1730 @"2024-01-10T05:34:45-05:00[-05:00]",
1731 );
1732 insta::assert_debug_snapshot!(
1733 p("Wed, 10 Jan 2024 05 : 34 : 45 -0500"),
1734 @"2024-01-10T05:34:45-05:00[-05:00]",
1735 );
1736 }
1737
1738 #[test]
1739 fn err_parse_invalid() {
1740 let p = |input| {
1741 DateTimeParser::new().parse_zoned(input).unwrap_err().to_string()
1742 };
1743
1744 insta::assert_snapshot!(
1745 p("Thu, 10 Jan 2024 05:34:45 -0500"),
1746 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: found parsed weekday of Thu, but parsed datetime of 2024-01-10T05:34:45 has weekday Wed",
1747 );
1748 insta::assert_snapshot!(
1749 p("Wed, 29 Feb 2023 05:34:45 -0500"),
1750 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: invalid date: parameter 'day' with value 29 is not in the required range of 1..=28",
1751 );
1752 insta::assert_snapshot!(
1753 p("Mon, 31 Jun 2024 05:34:45 -0500"),
1754 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: invalid date: parameter 'day' with value 31 is not in the required range of 1..=30",
1755 );
1756 insta::assert_snapshot!(
1757 p("Tue, 32 Jun 2024 05:34:45 -0500"),
1758 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: day is not valid: parameter 'day' with value 32 is not in the required range of 1..=31",
1759 );
1760 insta::assert_snapshot!(
1761 p("Sun, 30 Jun 2024 24:00:00 -0500"),
1762 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: hour is not valid: parameter 'hour' with value 24 is not in the required range of 0..=23",
1763 );
1764 // No whitespace after time
1765 insta::assert_snapshot!(
1766 p("Wed, 10 Jan 2024 05:34MST"),
1767 @r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing time: expected at least one whitespace character (space or tab), but found none"###,
1768 );
1769 }
1770
1771 #[test]
1772 fn err_parse_incomplete() {
1773 let p = |input| {
1774 DateTimeParser::new().parse_zoned(input).unwrap_err().to_string()
1775 };
1776
1777 insta::assert_snapshot!(
1778 p(""),
1779 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected RFC 2822 datetime, but got empty string",
1780 );
1781 insta::assert_snapshot!(
1782 p(" "),
1783 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected RFC 2822 datetime, but got empty string after trimming whitespace",
1784 );
1785 insta::assert_snapshot!(
1786 p("Wat"),
1787 @r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, "W", is not a digit, but given string is too short (length is 3)"###,
1788 );
1789 insta::assert_snapshot!(
1790 p("Wed"),
1791 @r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, "W", is not a digit, but given string is too short (length is 3)"###,
1792 );
1793 insta::assert_snapshot!(
1794 p("Wed "),
1795 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected comma after parsed weekday `Wed` in RFC 2822 datetime, but found end of string instead",
1796 );
1797 insta::assert_snapshot!(
1798 p("Wed ,"),
1799 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day, but found end of input",
1800 );
1801 insta::assert_snapshot!(
1802 p("Wed , "),
1803 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day, but found end of input",
1804 );
1805 insta::assert_snapshot!(
1806 p("Wat, "),
1807 @r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, "W", is not a digit, but did not recognize "Wat" as a valid weekday abbreviation"###,
1808 );
1809 insta::assert_snapshot!(
1810 p("Wed, "),
1811 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day, but found end of input",
1812 );
1813 insta::assert_snapshot!(
1814 p("Wed, 1"),
1815 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing day 1: expected at least one whitespace character (space or tab), but found none",
1816 );
1817 insta::assert_snapshot!(
1818 p("Wed, 10"),
1819 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing day 10: expected at least one whitespace character (space or tab), but found none",
1820 );
1821 insta::assert_snapshot!(
1822 p("Wed, 10 J"),
1823 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected abbreviated month name, but remaining input is too short (remaining bytes is 1)",
1824 );
1825 insta::assert_snapshot!(
1826 p("Wed, 10 Wat"),
1827 @r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected abbreviated month name, but did not recognize "Wat" as a valid month"###,
1828 );
1829 insta::assert_snapshot!(
1830 p("Wed, 10 Jan"),
1831 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing month name: expected at least one whitespace character (space or tab), but found none",
1832 );
1833 insta::assert_snapshot!(
1834 p("Wed, 10 Jan 2"),
1835 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected at least two ASCII digits for parsing a year, but only found 1",
1836 );
1837 insta::assert_snapshot!(
1838 p("Wed, 10 Jan 2024"),
1839 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing year: expected at least one whitespace character (space or tab), but found none",
1840 );
1841 insta::assert_snapshot!(
1842 p("Wed, 10 Jan 2024 05"),
1843 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected time separator of ':', but found end of input",
1844 );
1845 insta::assert_snapshot!(
1846 p("Wed, 10 Jan 2024 053"),
1847 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected time separator of ':', but found 3",
1848 );
1849 insta::assert_snapshot!(
1850 p("Wed, 10 Jan 2024 05:34"),
1851 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing time: expected at least one whitespace character (space or tab), but found none",
1852 );
1853 insta::assert_snapshot!(
1854 p("Wed, 10 Jan 2024 05:34:"),
1855 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected two digit second, but found end of input",
1856 );
1857 insta::assert_snapshot!(
1858 p("Wed, 10 Jan 2024 05:34:45"),
1859 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing time: expected at least one whitespace character (space or tab), but found none",
1860 );
1861 insta::assert_snapshot!(
1862 p("Wed, 10 Jan 2024 05:34:45 J"),
1863 @r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected obsolete RFC 2822 time zone abbreviation, but found "J""###,
1864 );
1865 }
1866
1867 #[test]
1868 fn err_parse_comment() {
1869 let p = |input| {
1870 DateTimeParser::new().parse_zoned(input).unwrap_err().to_string()
1871 };
1872
1873 insta::assert_snapshot!(
1874 p(r"Wed, 10 Jan 2024 05:34:45 -0500 (wa)t)"),
1875 @r###"parsed value '2024-01-10T05:34:45-05:00[-05:00]', but unparsed input "t)" remains (expected no unparsed input)"###,
1876 );
1877 insta::assert_snapshot!(
1878 p(r"Wed, 10 Jan 2024 05:34:45 -0500 (wa(t)"),
1879 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: found opening parenthesis in comment with no matching closing parenthesis",
1880 );
1881 insta::assert_snapshot!(
1882 p(r"Wed, 10 Jan 2024 05:34:45 -0500 (w"),
1883 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: found opening parenthesis in comment with no matching closing parenthesis",
1884 );
1885 insta::assert_snapshot!(
1886 p(r"Wed, 10 Jan 2024 05:34:45 -0500 ("),
1887 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: found opening parenthesis in comment with no matching closing parenthesis",
1888 );
1889 insta::assert_snapshot!(
1890 p(r"Wed, 10 Jan 2024 05:34:45 -0500 ( "),
1891 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: found opening parenthesis in comment with no matching closing parenthesis",
1892 );
1893 }
1894
1895 #[test]
1896 fn ok_print_zoned() {
1897 if crate::tz::db().is_definitively_empty() {
1898 return;
1899 }
1900
1901 let p = |zdt: &Zoned| -> String {
1902 let mut buf = String::new();
1903 DateTimePrinter::new().print_zoned(&zdt, &mut buf).unwrap();
1904 buf
1905 };
1906
1907 let zdt = date(2024, 1, 10)
1908 .at(5, 34, 45, 0)
1909 .in_tz("America/New_York")
1910 .unwrap();
1911 insta::assert_snapshot!(p(&zdt), @"Wed, 10 Jan 2024 05:34:45 -0500");
1912
1913 let zdt = date(2024, 2, 5)
1914 .at(5, 34, 45, 0)
1915 .in_tz("America/New_York")
1916 .unwrap();
1917 insta::assert_snapshot!(p(&zdt), @"Mon, 5 Feb 2024 05:34:45 -0500");
1918
1919 let zdt = date(2024, 7, 31)
1920 .at(5, 34, 45, 0)
1921 .in_tz("America/New_York")
1922 .unwrap();
1923 insta::assert_snapshot!(p(&zdt), @"Wed, 31 Jul 2024 05:34:45 -0400");
1924
1925 let zdt = date(2024, 3, 5).at(5, 34, 45, 0).in_tz("UTC").unwrap();
1926 // Notice that this prints a +0000 offset.
1927 // But when printing a Timestamp, a -0000 offset is used.
1928 // This is because in the case of Timestamp, the "true"
1929 // offset is not known.
1930 insta::assert_snapshot!(p(&zdt), @"Tue, 5 Mar 2024 05:34:45 +0000");
1931 }
1932
1933 #[test]
1934 fn ok_print_timestamp() {
1935 if crate::tz::db().is_definitively_empty() {
1936 return;
1937 }
1938
1939 let p = |ts: Timestamp| -> String {
1940 let mut buf = String::new();
1941 DateTimePrinter::new().print_timestamp(&ts, &mut buf).unwrap();
1942 buf
1943 };
1944
1945 let ts = date(2024, 1, 10)
1946 .at(5, 34, 45, 0)
1947 .in_tz("America/New_York")
1948 .unwrap()
1949 .timestamp();
1950 insta::assert_snapshot!(p(ts), @"Wed, 10 Jan 2024 10:34:45 -0000");
1951
1952 let ts = date(2024, 2, 5)
1953 .at(5, 34, 45, 0)
1954 .in_tz("America/New_York")
1955 .unwrap()
1956 .timestamp();
1957 insta::assert_snapshot!(p(ts), @"Mon, 5 Feb 2024 10:34:45 -0000");
1958
1959 let ts = date(2024, 7, 31)
1960 .at(5, 34, 45, 0)
1961 .in_tz("America/New_York")
1962 .unwrap()
1963 .timestamp();
1964 insta::assert_snapshot!(p(ts), @"Wed, 31 Jul 2024 09:34:45 -0000");
1965
1966 let ts = date(2024, 3, 5)
1967 .at(5, 34, 45, 0)
1968 .in_tz("UTC")
1969 .unwrap()
1970 .timestamp();
1971 // Notice that this prints a +0000 offset.
1972 // But when printing a Timestamp, a -0000 offset is used.
1973 // This is because in the case of Timestamp, the "true"
1974 // offset is not known.
1975 insta::assert_snapshot!(p(ts), @"Tue, 5 Mar 2024 05:34:45 -0000");
1976 }
1977
1978 #[test]
1979 fn ok_print_rfc9110_timestamp() {
1980 if crate::tz::db().is_definitively_empty() {
1981 return;
1982 }
1983
1984 let p = |ts: Timestamp| -> String {
1985 let mut buf = String::new();
1986 DateTimePrinter::new()
1987 .print_timestamp_rfc9110(&ts, &mut buf)
1988 .unwrap();
1989 buf
1990 };
1991
1992 let ts = date(2024, 1, 10)
1993 .at(5, 34, 45, 0)
1994 .in_tz("America/New_York")
1995 .unwrap()
1996 .timestamp();
1997 insta::assert_snapshot!(p(ts), @"Wed, 10 Jan 2024 10:34:45 GMT");
1998
1999 let ts = date(2024, 2, 5)
2000 .at(5, 34, 45, 0)
2001 .in_tz("America/New_York")
2002 .unwrap()
2003 .timestamp();
2004 insta::assert_snapshot!(p(ts), @"Mon, 05 Feb 2024 10:34:45 GMT");
2005
2006 let ts = date(2024, 7, 31)
2007 .at(5, 34, 45, 0)
2008 .in_tz("America/New_York")
2009 .unwrap()
2010 .timestamp();
2011 insta::assert_snapshot!(p(ts), @"Wed, 31 Jul 2024 09:34:45 GMT");
2012
2013 let ts = date(2024, 3, 5)
2014 .at(5, 34, 45, 0)
2015 .in_tz("UTC")
2016 .unwrap()
2017 .timestamp();
2018 // Notice that this prints a +0000 offset.
2019 // But when printing a Timestamp, a -0000 offset is used.
2020 // This is because in the case of Timestamp, the "true"
2021 // offset is not known.
2022 insta::assert_snapshot!(p(ts), @"Tue, 05 Mar 2024 05:34:45 GMT");
2023 }
2024
2025 #[test]
2026 fn err_print_zoned() {
2027 if crate::tz::db().is_definitively_empty() {
2028 return;
2029 }
2030
2031 let p = |zdt: &Zoned| -> String {
2032 let mut buf = String::new();
2033 DateTimePrinter::new()
2034 .print_zoned(&zdt, &mut buf)
2035 .unwrap_err()
2036 .to_string()
2037 };
2038
2039 let zdt = date(-1, 1, 10)
2040 .at(5, 34, 45, 0)
2041 .in_tz("America/New_York")
2042 .unwrap();
2043 insta::assert_snapshot!(p(&zdt), @"datetime -000001-01-10T05:34:45 has negative year, which cannot be formatted with RFC 2822");
2044 }
2045
2046 #[test]
2047 fn err_print_timestamp() {
2048 if crate::tz::db().is_definitively_empty() {
2049 return;
2050 }
2051
2052 let p = |ts: Timestamp| -> String {
2053 let mut buf = String::new();
2054 DateTimePrinter::new()
2055 .print_timestamp(&ts, &mut buf)
2056 .unwrap_err()
2057 .to_string()
2058 };
2059
2060 let ts = date(-1, 1, 10)
2061 .at(5, 34, 45, 0)
2062 .in_tz("America/New_York")
2063 .unwrap()
2064 .timestamp();
2065 insta::assert_snapshot!(p(ts), @"datetime -000001-01-10T10:30:47 has negative year, which cannot be formatted with RFC 2822");
2066 }
2067}