jiff/fmt/
rfc2822.rs

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