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