Skip to main content

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