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