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.parse_time_separator(input)?;
446        let Parsed { value: minute, input } = self.parse_minute(input)?;
447        let (second, input) = if !input.starts_with(b":") {
448            (t::Second::N::<0>(), input)
449        } else {
450            let Parsed { input, .. } = self.parse_time_separator(input)?;
451            let Parsed { value: second, input } = self.parse_second(input)?;
452            (second, input)
453        };
454        let Parsed { input, .. } = self
455            .parse_whitespace(input)
456            .with_context(|| err!("expected whitespace after parsing time"))?;
457
458        let date =
459            Date::new_ranged(year, month, day).context("invalid date")?;
460        let time = Time::new_ranged(
461            hour,
462            minute,
463            second,
464            t::SubsecNanosecond::N::<0>(),
465        );
466        let dt = DateTime::from_parts(date, time);
467        if let Some(wd) = wd {
468            if !self.relaxed_weekday && wd != dt.weekday() {
469                return Err(err!(
470                    "found parsed weekday of {parsed}, \
471                     but parsed datetime of {dt} has weekday \
472                     {has}",
473                    parsed = weekday_abbrev(wd),
474                    has = weekday_abbrev(dt.weekday()),
475                ));
476            }
477        }
478        Ok(Parsed { value: dt, input })
479    }
480
481    /// Parses an optional weekday at the beginning of an RFC 2822 datetime.
482    ///
483    /// This expects that any optional whitespace preceding the start of an
484    /// optional day has been stripped and that the input has at least one
485    /// byte.
486    ///
487    /// When the first byte of the given input is a digit (or is empty), then
488    /// this returns `None`, as it implies a day is not present. But if it
489    /// isn't a digit, then we assume that it must be a weekday and return an
490    /// error based on that assumption if we couldn't recognize a weekday.
491    ///
492    /// If a weekday is parsed, then this also skips any trailing whitespace
493    /// (and requires at least one whitespace character).
494    #[cfg_attr(feature = "perf-inline", inline(always))]
495    fn parse_weekday<'i>(
496        &self,
497        input: &'i [u8],
498    ) -> Result<Parsed<'i, Option<Weekday>>, Error> {
499        // An empty input is invalid, but we let that case be
500        // handled by the caller. Otherwise, we know there MUST
501        // be a present day if the first character isn't an ASCII
502        // digit.
503        if matches!(input[0], b'0'..=b'9') {
504            return Ok(Parsed { value: None, input });
505        }
506        if input.len() < 4 {
507            return Err(err!(
508                "expected day at beginning of RFC 2822 datetime \
509                 since first non-whitespace byte, {first:?}, \
510                 is not a digit, but given string is too short \
511                 (length is {length})",
512                first = escape::Byte(input[0]),
513                length = input.len(),
514            ));
515        }
516        let b1 = input[0].to_ascii_lowercase();
517        let b2 = input[1].to_ascii_lowercase();
518        let b3 = input[2].to_ascii_lowercase();
519        let wd = match &[b1, b2, b3] {
520            b"sun" => Weekday::Sunday,
521            b"mon" => Weekday::Monday,
522            b"tue" => Weekday::Tuesday,
523            b"wed" => Weekday::Wednesday,
524            b"thu" => Weekday::Thursday,
525            b"fri" => Weekday::Friday,
526            b"sat" => Weekday::Saturday,
527            _ => {
528                return Err(err!(
529                    "expected day at beginning of RFC 2822 datetime \
530                     since first non-whitespace byte, {first:?}, \
531                     is not a digit, but did not recognize {got:?} \
532                     as a valid weekday abbreviation",
533                    first = escape::Byte(input[0]),
534                    got = escape::Bytes(&input[..3]),
535                ));
536            }
537        };
538        if input[3] != b',' {
539            return Err(err!(
540                "expected day at beginning of RFC 2822 datetime \
541                 since first non-whitespace byte, {first:?}, \
542                 is not a digit, but found {got:?} after parsed \
543                 weekday {wd:?} and expected a comma",
544                first = escape::Byte(input[0]),
545                got = escape::Byte(input[3]),
546                wd = escape::Bytes(&input[..3]),
547            ));
548        }
549        let Parsed { input, .. } =
550            self.parse_whitespace(&input[4..]).with_context(|| {
551                err!(
552                    "expected whitespace after parsing {got:?}",
553                    got = escape::Bytes(&input[..4]),
554                )
555            })?;
556        Ok(Parsed { value: Some(wd), input })
557    }
558
559    /// Parses a 1 or 2 digit day.
560    ///
561    /// This assumes the input starts with what must be an ASCII digit (or it
562    /// may be empty).
563    ///
564    /// This also parses at least one mandatory whitespace character after the
565    /// day.
566    #[cfg_attr(feature = "perf-inline", inline(always))]
567    fn parse_day<'i>(
568        &self,
569        input: &'i [u8],
570    ) -> Result<Parsed<'i, t::Day>, Error> {
571        if input.is_empty() {
572            return Err(err!("expected day, but found end of input"));
573        }
574        let mut digits = 1;
575        if input.len() >= 2 && matches!(input[1], b'0'..=b'9') {
576            digits = 2;
577        }
578        let (day, input) = input.split_at(digits);
579        let day = parse::i64(day).with_context(|| {
580            err!("failed to parse {day:?} as day", day = escape::Bytes(day))
581        })?;
582        let day = t::Day::try_new("day", day).context("day is not valid")?;
583        let Parsed { input, .. } =
584            self.parse_whitespace(input).with_context(|| {
585                err!("expected whitespace after parsing day {day}")
586            })?;
587        Ok(Parsed { value: day, input })
588    }
589
590    /// Parses an abbreviated month name.
591    ///
592    /// This assumes the input starts with what must be the beginning of a
593    /// month name (or the input may be empty).
594    ///
595    /// This also parses at least one mandatory whitespace character after the
596    /// month name.
597    #[cfg_attr(feature = "perf-inline", inline(always))]
598    fn parse_month<'i>(
599        &self,
600        input: &'i [u8],
601    ) -> Result<Parsed<'i, t::Month>, Error> {
602        if input.is_empty() {
603            return Err(err!(
604                "expected abbreviated month name, but found end of input"
605            ));
606        }
607        if input.len() < 3 {
608            return Err(err!(
609                "expected abbreviated month name, but remaining input \
610                 is too short (remaining bytes is {length})",
611                length = input.len(),
612            ));
613        }
614        let b1 = input[0].to_ascii_lowercase();
615        let b2 = input[1].to_ascii_lowercase();
616        let b3 = input[2].to_ascii_lowercase();
617        let month = match &[b1, b2, b3] {
618            b"jan" => 1,
619            b"feb" => 2,
620            b"mar" => 3,
621            b"apr" => 4,
622            b"may" => 5,
623            b"jun" => 6,
624            b"jul" => 7,
625            b"aug" => 8,
626            b"sep" => 9,
627            b"oct" => 10,
628            b"nov" => 11,
629            b"dec" => 12,
630            _ => {
631                return Err(err!(
632                    "expected abbreviated month name, \
633                     but did not recognize {got:?} \
634                     as a valid month",
635                    got = escape::Bytes(&input[..3]),
636                ));
637            }
638        };
639        // OK because we just assigned a numeric value ourselves
640        // above, and all values are valid months.
641        let month = t::Month::new(month).unwrap();
642        let Parsed { input, .. } =
643            self.parse_whitespace(&input[3..]).with_context(|| {
644                err!("expected whitespace after parsing month name")
645            })?;
646        Ok(Parsed { value: month, input })
647    }
648
649    /// Parses a 2, 3 or 4 digit year.
650    ///
651    /// This assumes the input starts with what must be an ASCII digit (or it
652    /// may be empty).
653    ///
654    /// This also parses at least one mandatory whitespace character after the
655    /// day.
656    ///
657    /// The 2 or 3 digit years are "obsolete," which we support by following
658    /// the rules in RFC 2822:
659    ///
660    /// > Where a two or three digit year occurs in a date, the year is to be
661    /// > interpreted as follows: If a two digit year is encountered whose
662    /// > value is between 00 and 49, the year is interpreted by adding 2000,
663    /// > ending up with a value between 2000 and 2049. If a two digit year is
664    /// > encountered with a value between 50 and 99, or any three digit year
665    /// > is encountered, the year is interpreted by adding 1900.
666    #[cfg_attr(feature = "perf-inline", inline(always))]
667    fn parse_year<'i>(
668        &self,
669        input: &'i [u8],
670    ) -> Result<Parsed<'i, t::Year>, Error> {
671        let mut digits = 0;
672        while digits <= 3
673            && !input[digits..].is_empty()
674            && matches!(input[digits], b'0'..=b'9')
675        {
676            digits += 1;
677        }
678        if digits <= 1 {
679            return Err(err!(
680                "expected at least two ASCII digits for parsing \
681                 a year, but only found {digits}",
682            ));
683        }
684        let (year, input) = input.split_at(digits);
685        let year = parse::i64(year).with_context(|| {
686            err!(
687                "failed to parse {year:?} as year \
688                 (a two, three or four digit integer)",
689                year = escape::Bytes(year),
690            )
691        })?;
692        let year = match digits {
693            2 if year <= 49 => year + 2000,
694            2 | 3 => year + 1900,
695            4 => year,
696            _ => unreachable!("digits={digits} must be 2, 3 or 4"),
697        };
698        let year =
699            t::Year::try_new("year", year).context("year is not valid")?;
700        let Parsed { input, .. } = self
701            .parse_whitespace(input)
702            .with_context(|| err!("expected whitespace after parsing year"))?;
703        Ok(Parsed { value: year, input })
704    }
705
706    /// Parses a 2-digit hour. This assumes the input begins with what should
707    /// be an ASCII digit. (i.e., It doesn't trim leading whitespace.)
708    ///
709    /// This parses a mandatory trailing `:`, advancing the input to
710    /// immediately after it.
711    #[cfg_attr(feature = "perf-inline", inline(always))]
712    fn parse_hour<'i>(
713        &self,
714        input: &'i [u8],
715    ) -> Result<Parsed<'i, t::Hour>, Error> {
716        let (hour, input) = parse::split(input, 2).ok_or_else(|| {
717            err!("expected two digit hour, but found end of input")
718        })?;
719        let hour = parse::i64(hour).with_context(|| {
720            err!(
721                "failed to parse {hour:?} as hour (a two digit integer)",
722                hour = escape::Bytes(hour),
723            )
724        })?;
725        let hour =
726            t::Hour::try_new("hour", hour).context("hour is not valid")?;
727        Ok(Parsed { value: hour, input })
728    }
729
730    /// Parses a 2-digit minute. This assumes the input begins with what should
731    /// be an ASCII digit. (i.e., It doesn't trim leading whitespace.)
732    #[cfg_attr(feature = "perf-inline", inline(always))]
733    fn parse_minute<'i>(
734        &self,
735        input: &'i [u8],
736    ) -> Result<Parsed<'i, t::Minute>, Error> {
737        let (minute, input) = parse::split(input, 2).ok_or_else(|| {
738            err!("expected two digit minute, but found end of input")
739        })?;
740        let minute = parse::i64(minute).with_context(|| {
741            err!(
742                "failed to parse {minute:?} as minute (a two digit integer)",
743                minute = escape::Bytes(minute),
744            )
745        })?;
746        let minute = t::Minute::try_new("minute", minute)
747            .context("minute is not valid")?;
748        Ok(Parsed { value: minute, input })
749    }
750
751    /// Parses a 2-digit second. This assumes the input begins with what should
752    /// be an ASCII digit. (i.e., It doesn't trim leading whitespace.)
753    #[cfg_attr(feature = "perf-inline", inline(always))]
754    fn parse_second<'i>(
755        &self,
756        input: &'i [u8],
757    ) -> Result<Parsed<'i, t::Second>, Error> {
758        let (second, input) = parse::split(input, 2).ok_or_else(|| {
759            err!("expected two digit second, but found end of input")
760        })?;
761        let mut second = parse::i64(second).with_context(|| {
762            err!(
763                "failed to parse {second:?} as second (a two digit integer)",
764                second = escape::Bytes(second),
765            )
766        })?;
767        if second == 60 {
768            second = 59;
769        }
770        let second = t::Second::try_new("second", second)
771            .context("second is not valid")?;
772        Ok(Parsed { value: second, input })
773    }
774
775    /// Parses a time zone offset (including obsolete offsets like EDT).
776    ///
777    /// This assumes the offset must begin at the beginning of `input`. That
778    /// is, any leading whitespace should already have been trimmed.
779    #[cfg_attr(feature = "perf-inline", inline(always))]
780    fn parse_offset<'i>(
781        &self,
782        input: &'i [u8],
783    ) -> Result<Parsed<'i, Offset>, Error> {
784        type ParsedOffsetHours = ri8<0, { t::SpanZoneOffsetHours::MAX }>;
785        type ParsedOffsetMinutes = ri8<0, { t::SpanZoneOffsetMinutes::MAX }>;
786
787        let sign = input.get(0).copied().ok_or_else(|| {
788            err!(
789                "expected sign for time zone offset, \
790                 (or a legacy time zone name abbreviation), \
791                 but found end of input",
792            )
793        })?;
794        let sign = if sign == b'+' {
795            t::Sign::N::<1>()
796        } else if sign == b'-' {
797            t::Sign::N::<-1>()
798        } else {
799            return self.parse_offset_obsolete(input);
800        };
801        let input = &input[1..];
802        let (hhmm, input) = parse::split(input, 4).ok_or_else(|| {
803            err!(
804                "expected at least 4 digits for time zone offset \
805                 after sign, but found only {len} bytes remaining",
806                len = input.len(),
807            )
808        })?;
809
810        let hh = parse::i64(&hhmm[0..2]).with_context(|| {
811            err!(
812                "failed to parse hours from time zone offset {hhmm}",
813                hhmm = escape::Bytes(hhmm)
814            )
815        })?;
816        let hh = ParsedOffsetHours::try_new("zone-offset-hours", hh)
817            .context("time zone offset hours are not valid")?;
818        let hh = t::SpanZoneOffset::rfrom(hh);
819
820        let mm = parse::i64(&hhmm[2..4]).with_context(|| {
821            err!(
822                "failed to parse minutes from time zone offset {hhmm}",
823                hhmm = escape::Bytes(hhmm)
824            )
825        })?;
826        let mm = ParsedOffsetMinutes::try_new("zone-offset-minutes", mm)
827            .context("time zone offset minutes are not valid")?;
828        let mm = t::SpanZoneOffset::rfrom(mm);
829
830        let seconds = hh * C(3_600) + mm * C(60);
831        let offset = Offset::from_seconds_ranged(seconds * sign);
832        Ok(Parsed { value: offset, input })
833    }
834
835    /// Parses an obsolete time zone offset.
836    #[inline(never)]
837    fn parse_offset_obsolete<'i>(
838        &self,
839        input: &'i [u8],
840    ) -> Result<Parsed<'i, Offset>, Error> {
841        let mut letters = [0; 5];
842        let mut len = 0;
843        while len <= 4
844            && !input[len..].is_empty()
845            && !is_whitespace(input[len])
846        {
847            letters[len] = input[len].to_ascii_lowercase();
848            len += 1;
849        }
850        if len == 0 {
851            return Err(err!(
852                "expected obsolete RFC 2822 time zone abbreviation, \
853                 but found no remaining non-whitespace characters \
854                 after time",
855            ));
856        }
857        let offset = match &letters[..len] {
858            b"ut" | b"gmt" | b"z" => Offset::UTC,
859            b"est" => Offset::constant(-5),
860            b"edt" => Offset::constant(-4),
861            b"cst" => Offset::constant(-6),
862            b"cdt" => Offset::constant(-5),
863            b"mst" => Offset::constant(-7),
864            b"mdt" => Offset::constant(-6),
865            b"pst" => Offset::constant(-8),
866            b"pdt" => Offset::constant(-7),
867            name => {
868                if name.len() == 1
869                    && matches!(name[0], b'a'..=b'i' | b'k'..=b'z')
870                {
871                    // Section 4.3 indicates these as military time:
872                    //
873                    // > The 1 character military time zones were defined in
874                    // > a non-standard way in [RFC822] and are therefore
875                    // > unpredictable in their meaning. The original
876                    // > definitions of the military zones "A" through "I" are
877                    // > equivalent to "+0100" through "+0900" respectively;
878                    // > "K", "L", and "M" are equivalent to "+1000", "+1100",
879                    // > and "+1200" respectively; "N" through "Y" are
880                    // > equivalent to "-0100" through "-1200" respectively;
881                    // > and "Z" is equivalent to "+0000". However, because of
882                    // > the error in [RFC822], they SHOULD all be considered
883                    // > equivalent to "-0000" unless there is out-of-band
884                    // > information confirming their meaning.
885                    //
886                    // So just treat them as UTC.
887                    Offset::UTC
888                } else if name.len() >= 3
889                    && name.iter().all(|&b| matches!(b, b'a'..=b'z'))
890                {
891                    // Section 4.3 also says that anything that _looks_ like a
892                    // zone name should just be -0000 too:
893                    //
894                    // > Other multi-character (usually between 3 and 5)
895                    // > alphabetic time zones have been used in Internet
896                    // > messages. Any such time zone whose meaning is not
897                    // > known SHOULD be considered equivalent to "-0000"
898                    // > unless there is out-of-band information confirming
899                    // > their meaning.
900                    Offset::UTC
901                } else {
902                    // But anything else we throw our hands up I guess.
903                    return Err(err!(
904                        "expected obsolete RFC 2822 time zone abbreviation, \
905                         but found {found:?}",
906                        found = escape::Bytes(&input[..len]),
907                    ));
908                }
909            }
910        };
911        Ok(Parsed { value: offset, input: &input[len..] })
912    }
913
914    /// Parses a time separator. This returns an error if one couldn't be
915    /// found.
916    #[cfg_attr(feature = "perf-inline", inline(always))]
917    fn parse_time_separator<'i>(
918        &self,
919        input: &'i [u8],
920    ) -> Result<Parsed<'i, ()>, Error> {
921        if input.is_empty() {
922            return Err(err!(
923                "expected time separator of ':', but found end of input",
924            ));
925        }
926        if input[0] != b':' {
927            return Err(err!(
928                "expected time separator of ':', but found {got}",
929                got = escape::Byte(input[0]),
930            ));
931        }
932        Ok(Parsed { value: (), input: &input[1..] })
933    }
934
935    /// Parses at least one whitespace character. If no whitespace was found,
936    /// then this returns an error.
937    #[cfg_attr(feature = "perf-inline", inline(always))]
938    fn parse_whitespace<'i>(
939        &self,
940        input: &'i [u8],
941    ) -> Result<Parsed<'i, ()>, Error> {
942        let oldlen = input.len();
943        let parsed = self.skip_whitespace(input);
944        let newlen = parsed.input.len();
945        if oldlen == newlen {
946            return Err(err!(
947                "expected at least one whitespace character (space or tab), \
948                 but found none",
949            ));
950        }
951        Ok(parsed)
952    }
953
954    /// Skips over any ASCII whitespace at the beginning of `input`.
955    ///
956    /// This returns the input unchanged if it does not begin with whitespace.
957    #[cfg_attr(feature = "perf-inline", inline(always))]
958    fn skip_whitespace<'i>(&self, mut input: &'i [u8]) -> Parsed<'i, ()> {
959        while input.first().map_or(false, |&b| is_whitespace(b)) {
960            input = &input[1..];
961        }
962        Parsed { value: (), input }
963    }
964
965    /// This attempts to parse and skip any trailing "comment" in an RFC 2822
966    /// datetime.
967    ///
968    /// This is a bit more relaxed than what RFC 2822 specifies. We basically
969    /// just try to balance parenthesis and skip over escapes.
970    ///
971    /// This assumes that if a comment exists, its opening parenthesis is at
972    /// the beginning of `input`. That is, any leading whitespace has been
973    /// stripped.
974    #[inline(never)]
975    fn skip_comment<'i>(
976        &self,
977        mut input: &'i [u8],
978    ) -> Result<Parsed<'i, ()>, Error> {
979        if !input.starts_with(b"(") {
980            return Ok(Parsed { value: (), input });
981        }
982        input = &input[1..];
983        let mut depth: u8 = 1;
984        let mut escape = false;
985        for byte in input.iter().copied() {
986            input = &input[1..];
987            if escape {
988                escape = false;
989            } else if byte == b'\\' {
990                escape = true;
991            } else if byte == b')' {
992                // I believe this error case is actually impossible, since as
993                // soon as we hit 0, we break out. If there is more "comment,"
994                // then it will flag an error as unparsed input.
995                depth = depth.checked_sub(1).ok_or_else(|| {
996                    err!(
997                        "found closing parenthesis in comment with \
998                         no matching opening parenthesis"
999                    )
1000                })?;
1001                if depth == 0 {
1002                    break;
1003                }
1004            } else if byte == b'(' {
1005                depth = depth.checked_add(1).ok_or_else(|| {
1006                    err!("found too many nested parenthesis in comment")
1007                })?;
1008            }
1009        }
1010        if depth > 0 {
1011            return Err(err!(
1012                "found opening parenthesis in comment with \
1013                 no matching closing parenthesis"
1014            ));
1015        }
1016        Ok(self.skip_whitespace(input))
1017    }
1018}
1019
1020/// A printer for [RFC 2822] datetimes.
1021///
1022/// This printer converts an in memory representation of a precise instant in
1023/// time to an RFC 2822 formatted string. That is, [`Zoned`] or [`Timestamp`],
1024/// since all other datetime types in Jiff are inexact.
1025///
1026/// [RFC 2822]: https://datatracker.ietf.org/doc/html/rfc2822
1027///
1028/// # Warning
1029///
1030/// The RFC 2822 format only supports writing a precise instant in time
1031/// expressed via a time zone offset. It does *not* support serializing
1032/// the time zone itself. This means that if you format a zoned datetime
1033/// in a time zone like `America/New_York` and then deserialize it, the
1034/// zoned datetime you get back will be a "fixed offset" zoned datetime.
1035/// This in turn means it will not perform daylight saving time safe
1036/// arithmetic.
1037///
1038/// Basically, you should use the RFC 2822 format if it's required (for
1039/// example, when dealing with email). But you should not choose it as a
1040/// general interchange format for new applications.
1041///
1042/// # Example
1043///
1044/// This example shows how to convert a zoned datetime to the RFC 2822 format:
1045///
1046/// ```
1047/// use jiff::{civil::date, fmt::rfc2822::DateTimePrinter};
1048///
1049/// const PRINTER: DateTimePrinter = DateTimePrinter::new();
1050///
1051/// let zdt = date(2024, 6, 15).at(7, 0, 0, 0).in_tz("Australia/Tasmania")?;
1052///
1053/// let mut buf = String::new();
1054/// PRINTER.print_zoned(&zdt, &mut buf)?;
1055/// assert_eq!(buf, "Sat, 15 Jun 2024 07:00:00 +1000");
1056///
1057/// # Ok::<(), Box<dyn std::error::Error>>(())
1058/// ```
1059///
1060/// # Example: using adapters with `std::io::Write` and `std::fmt::Write`
1061///
1062/// By using the [`StdIoWrite`](super::StdIoWrite) and
1063/// [`StdFmtWrite`](super::StdFmtWrite) adapters, one can print datetimes
1064/// directly to implementations of `std::io::Write` and `std::fmt::Write`,
1065/// respectively. The example below demonstrates writing to anything
1066/// that implements `std::io::Write`. Similar code can be written for
1067/// `std::fmt::Write`.
1068///
1069/// ```no_run
1070/// use std::{fs::File, io::{BufWriter, Write}, path::Path};
1071///
1072/// use jiff::{civil::date, fmt::{StdIoWrite, rfc2822::DateTimePrinter}};
1073///
1074/// let zdt = date(2024, 6, 15).at(7, 0, 0, 0).in_tz("Asia/Kolkata")?;
1075///
1076/// let path = Path::new("/tmp/output");
1077/// let mut file = BufWriter::new(File::create(path)?);
1078/// DateTimePrinter::new().print_zoned(&zdt, StdIoWrite(&mut file)).unwrap();
1079/// file.flush()?;
1080/// assert_eq!(
1081///     std::fs::read_to_string(path)?,
1082///     "Sat, 15 Jun 2024 07:00:00 +0530",
1083/// );
1084///
1085/// # Ok::<(), Box<dyn std::error::Error>>(())
1086/// ```
1087#[derive(Debug)]
1088pub struct DateTimePrinter {
1089    // The RFC 2822 printer has no configuration at present.
1090    _private: (),
1091}
1092
1093impl DateTimePrinter {
1094    /// Create a new RFC 2822 datetime printer with the default configuration.
1095    #[inline]
1096    pub const fn new() -> DateTimePrinter {
1097        DateTimePrinter { _private: () }
1098    }
1099
1100    /// Format a `Zoned` datetime into a string.
1101    ///
1102    /// This never emits `-0000` as the offset in the RFC 2822 format. If you
1103    /// desire a `-0000` offset, use [`DateTimePrinter::print_timestamp`] via
1104    /// [`Zoned::timestamp`].
1105    ///
1106    /// Moreover, since RFC 2822 does not support fractional seconds, this
1107    /// routine prints the zoned datetime as if truncating any fractional
1108    /// seconds.
1109    ///
1110    /// This is a convenience routine for [`DateTimePrinter::print_zoned`]
1111    /// with a `String`.
1112    ///
1113    /// # Warning
1114    ///
1115    /// The RFC 2822 format only supports writing a precise instant in time
1116    /// expressed via a time zone offset. It does *not* support serializing
1117    /// the time zone itself. This means that if you format a zoned datetime
1118    /// in a time zone like `America/New_York` and then deserialize it, the
1119    /// zoned datetime you get back will be a "fixed offset" zoned datetime.
1120    /// This in turn means it will not perform daylight saving time safe
1121    /// arithmetic.
1122    ///
1123    /// Basically, you should use the RFC 2822 format if it's required (for
1124    /// example, when dealing with email). But you should not choose it as a
1125    /// general interchange format for new applications.
1126    ///
1127    /// # Errors
1128    ///
1129    /// This can return an error if the year corresponding to this timestamp
1130    /// cannot be represented in the RFC 2822 format. For example, a negative
1131    /// year.
1132    ///
1133    /// # Example
1134    ///
1135    /// ```
1136    /// use jiff::{civil::date, fmt::rfc2822::DateTimePrinter};
1137    ///
1138    /// const PRINTER: DateTimePrinter = DateTimePrinter::new();
1139    ///
1140    /// let zdt = date(2024, 6, 15).at(7, 0, 0, 0).in_tz("America/New_York")?;
1141    /// assert_eq!(
1142    ///     PRINTER.zoned_to_string(&zdt)?,
1143    ///     "Sat, 15 Jun 2024 07:00:00 -0400",
1144    /// );
1145    ///
1146    /// # Ok::<(), Box<dyn std::error::Error>>(())
1147    /// ```
1148    #[cfg(feature = "alloc")]
1149    pub fn zoned_to_string(
1150        &self,
1151        zdt: &Zoned,
1152    ) -> Result<alloc::string::String, Error> {
1153        let mut buf = alloc::string::String::with_capacity(4);
1154        self.print_zoned(zdt, &mut buf)?;
1155        Ok(buf)
1156    }
1157
1158    /// Format a `Timestamp` datetime into a string.
1159    ///
1160    /// This always emits `-0000` as the offset in the RFC 2822 format. If you
1161    /// desire a `+0000` offset, use [`DateTimePrinter::print_zoned`] with a
1162    /// zoned datetime with [`TimeZone::UTC`].
1163    ///
1164    /// Moreover, since RFC 2822 does not support fractional seconds, this
1165    /// routine prints the timestamp as if truncating any fractional seconds.
1166    ///
1167    /// This is a convenience routine for [`DateTimePrinter::print_timestamp`]
1168    /// with a `String`.
1169    ///
1170    /// # Errors
1171    ///
1172    /// This returns an error if the year corresponding to this
1173    /// timestamp cannot be represented in the RFC 2822 format. For example, a
1174    /// negative year.
1175    ///
1176    /// # Example
1177    ///
1178    /// ```
1179    /// use jiff::{fmt::rfc2822::DateTimePrinter, Timestamp};
1180    ///
1181    /// let timestamp = Timestamp::from_second(1)
1182    ///     .expect("one second after Unix epoch is always valid");
1183    /// assert_eq!(
1184    ///     DateTimePrinter::new().timestamp_to_string(&timestamp)?,
1185    ///     "Thu, 1 Jan 1970 00:00:01 -0000",
1186    /// );
1187    ///
1188    /// # Ok::<(), Box<dyn std::error::Error>>(())
1189    /// ```
1190    #[cfg(feature = "alloc")]
1191    pub fn timestamp_to_string(
1192        &self,
1193        timestamp: &Timestamp,
1194    ) -> Result<alloc::string::String, Error> {
1195        let mut buf = alloc::string::String::with_capacity(4);
1196        self.print_timestamp(timestamp, &mut buf)?;
1197        Ok(buf)
1198    }
1199
1200    /// Format a `Timestamp` datetime into a string in a way that is explicitly
1201    /// compatible with [RFC 9110]. This is typically useful in contexts where
1202    /// strict compatibility with HTTP is desired.
1203    ///
1204    /// This always emits `GMT` as the offset and always uses two digits for
1205    /// the day. This results in a fixed length format that always uses 29
1206    /// characters.
1207    ///
1208    /// Since neither RFC 2822 nor RFC 9110 supports fractional seconds, this
1209    /// routine prints the timestamp as if truncating any fractional seconds.
1210    ///
1211    /// This is a convenience routine for
1212    /// [`DateTimePrinter::print_timestamp_rfc9110`] with a `String`.
1213    ///
1214    /// # Errors
1215    ///
1216    /// This returns an error if the year corresponding to this timestamp
1217    /// cannot be represented in the RFC 2822 or RFC 9110 format. For example,
1218    /// a negative year.
1219    ///
1220    /// # Example
1221    ///
1222    /// ```
1223    /// use jiff::{fmt::rfc2822::DateTimePrinter, Timestamp};
1224    ///
1225    /// let timestamp = Timestamp::from_second(1)
1226    ///     .expect("one second after Unix epoch is always valid");
1227    /// assert_eq!(
1228    ///     DateTimePrinter::new().timestamp_to_rfc9110_string(&timestamp)?,
1229    ///     "Thu, 01 Jan 1970 00:00:01 GMT",
1230    /// );
1231    ///
1232    /// # Ok::<(), Box<dyn std::error::Error>>(())
1233    /// ```
1234    ///
1235    /// [RFC 9110]: https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.7-15
1236    #[cfg(feature = "alloc")]
1237    pub fn timestamp_to_rfc9110_string(
1238        &self,
1239        timestamp: &Timestamp,
1240    ) -> Result<alloc::string::String, Error> {
1241        let mut buf = alloc::string::String::with_capacity(29);
1242        self.print_timestamp_rfc9110(timestamp, &mut buf)?;
1243        Ok(buf)
1244    }
1245
1246    /// Print a `Zoned` datetime to the given writer.
1247    ///
1248    /// This never emits `-0000` as the offset in the RFC 2822 format. If you
1249    /// desire a `-0000` offset, use [`DateTimePrinter::print_timestamp`] via
1250    /// [`Zoned::timestamp`].
1251    ///
1252    /// Moreover, since RFC 2822 does not support fractional seconds, this
1253    /// routine prints the zoned datetime as if truncating any fractional
1254    /// seconds.
1255    ///
1256    /// # Warning
1257    ///
1258    /// The RFC 2822 format only supports writing a precise instant in time
1259    /// expressed via a time zone offset. It does *not* support serializing
1260    /// the time zone itself. This means that if you format a zoned datetime
1261    /// in a time zone like `America/New_York` and then deserialize it, the
1262    /// zoned datetime you get back will be a "fixed offset" zoned datetime.
1263    /// This in turn means it will not perform daylight saving time safe
1264    /// arithmetic.
1265    ///
1266    /// Basically, you should use the RFC 2822 format if it's required (for
1267    /// example, when dealing with email). But you should not choose it as a
1268    /// general interchange format for new applications.
1269    ///
1270    /// # Errors
1271    ///
1272    /// This returns an error when writing to the given [`Write`]
1273    /// implementation would fail. Some such implementations, like for `String`
1274    /// and `Vec<u8>`, never fail (unless memory allocation fails).
1275    ///
1276    /// This can also return an error if the year corresponding to this
1277    /// timestamp cannot be represented in the RFC 2822 format. For example, a
1278    /// negative year.
1279    ///
1280    /// # Example
1281    ///
1282    /// ```
1283    /// use jiff::{civil::date, fmt::rfc2822::DateTimePrinter};
1284    ///
1285    /// const PRINTER: DateTimePrinter = DateTimePrinter::new();
1286    ///
1287    /// let zdt = date(2024, 6, 15).at(7, 0, 0, 0).in_tz("America/New_York")?;
1288    ///
1289    /// let mut buf = String::new();
1290    /// PRINTER.print_zoned(&zdt, &mut buf)?;
1291    /// assert_eq!(buf, "Sat, 15 Jun 2024 07:00:00 -0400");
1292    ///
1293    /// # Ok::<(), Box<dyn std::error::Error>>(())
1294    /// ```
1295    pub fn print_zoned<W: Write>(
1296        &self,
1297        zdt: &Zoned,
1298        wtr: W,
1299    ) -> Result<(), Error> {
1300        self.print_civil_with_offset(zdt.datetime(), Some(zdt.offset()), wtr)
1301    }
1302
1303    /// Print a `Timestamp` datetime to the given writer.
1304    ///
1305    /// This always emits `-0000` as the offset in the RFC 2822 format. If you
1306    /// desire a `+0000` offset, use [`DateTimePrinter::print_zoned`] with a
1307    /// zoned datetime with [`TimeZone::UTC`].
1308    ///
1309    /// Moreover, since RFC 2822 does not support fractional seconds, this
1310    /// routine prints the timestamp as if truncating any fractional seconds.
1311    ///
1312    /// # Errors
1313    ///
1314    /// This returns an error when writing to the given [`Write`]
1315    /// implementation would fail. Some such implementations, like for `String`
1316    /// and `Vec<u8>`, never fail (unless memory allocation fails).
1317    ///
1318    /// This can also return an error if the year corresponding to this
1319    /// timestamp cannot be represented in the RFC 2822 format. For example, a
1320    /// negative year.
1321    ///
1322    /// # Example
1323    ///
1324    /// ```
1325    /// use jiff::{fmt::rfc2822::DateTimePrinter, Timestamp};
1326    ///
1327    /// let timestamp = Timestamp::from_second(1)
1328    ///     .expect("one second after Unix epoch is always valid");
1329    ///
1330    /// let mut buf = String::new();
1331    /// DateTimePrinter::new().print_timestamp(&timestamp, &mut buf)?;
1332    /// assert_eq!(buf, "Thu, 1 Jan 1970 00:00:01 -0000");
1333    ///
1334    /// # Ok::<(), Box<dyn std::error::Error>>(())
1335    /// ```
1336    pub fn print_timestamp<W: Write>(
1337        &self,
1338        timestamp: &Timestamp,
1339        wtr: W,
1340    ) -> Result<(), Error> {
1341        let dt = TimeZone::UTC.to_datetime(*timestamp);
1342        self.print_civil_with_offset(dt, None, wtr)
1343    }
1344
1345    /// Print a `Timestamp` datetime to the given writer in a way that is
1346    /// explicitly compatible with [RFC 9110]. This is typically useful in
1347    /// contexts where strict compatibility with HTTP is desired.
1348    ///
1349    /// This always emits `GMT` as the offset and always uses two digits for
1350    /// the day. This results in a fixed length format that always uses 29
1351    /// characters.
1352    ///
1353    /// Since neither RFC 2822 nor RFC 9110 supports fractional seconds, this
1354    /// routine prints the timestamp as if truncating any fractional seconds.
1355    ///
1356    /// # Errors
1357    ///
1358    /// This returns an error when writing to the given [`Write`]
1359    /// implementation would fail. Some such implementations, like for `String`
1360    /// and `Vec<u8>`, never fail (unless memory allocation fails).
1361    ///
1362    /// This can also return an error if the year corresponding to this
1363    /// timestamp cannot be represented in the RFC 2822 or RFC 9110 format. For
1364    /// example, a negative year.
1365    ///
1366    /// # Example
1367    ///
1368    /// ```
1369    /// use jiff::{fmt::rfc2822::DateTimePrinter, Timestamp};
1370    ///
1371    /// let timestamp = Timestamp::from_second(1)
1372    ///     .expect("one second after Unix epoch is always valid");
1373    ///
1374    /// let mut buf = String::new();
1375    /// DateTimePrinter::new().print_timestamp_rfc9110(&timestamp, &mut buf)?;
1376    /// assert_eq!(buf, "Thu, 01 Jan 1970 00:00:01 GMT");
1377    ///
1378    /// # Ok::<(), Box<dyn std::error::Error>>(())
1379    /// ```
1380    ///
1381    /// [RFC 9110]: https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.7-15
1382    pub fn print_timestamp_rfc9110<W: Write>(
1383        &self,
1384        timestamp: &Timestamp,
1385        wtr: W,
1386    ) -> Result<(), Error> {
1387        self.print_civil_always_utc(timestamp, wtr)
1388    }
1389
1390    fn print_civil_with_offset<W: Write>(
1391        &self,
1392        dt: DateTime,
1393        offset: Option<Offset>,
1394        mut wtr: W,
1395    ) -> Result<(), Error> {
1396        static FMT_DAY: DecimalFormatter = DecimalFormatter::new();
1397        static FMT_YEAR: DecimalFormatter = DecimalFormatter::new().padding(4);
1398        static FMT_TIME_UNIT: DecimalFormatter =
1399            DecimalFormatter::new().padding(2);
1400
1401        if dt.year() < 0 {
1402            // RFC 2822 actually says the year must be at least 1900, but
1403            // other implementations (like Chrono) allow any positive 4-digit
1404            // year.
1405            return Err(err!(
1406                "datetime {dt} has negative year, \
1407                 which cannot be formatted with RFC 2822",
1408            ));
1409        }
1410
1411        wtr.write_str(weekday_abbrev(dt.weekday()))?;
1412        wtr.write_str(", ")?;
1413        wtr.write_int(&FMT_DAY, dt.day())?;
1414        wtr.write_str(" ")?;
1415        wtr.write_str(month_name(dt.month()))?;
1416        wtr.write_str(" ")?;
1417        wtr.write_int(&FMT_YEAR, dt.year())?;
1418        wtr.write_str(" ")?;
1419        wtr.write_int(&FMT_TIME_UNIT, dt.hour())?;
1420        wtr.write_str(":")?;
1421        wtr.write_int(&FMT_TIME_UNIT, dt.minute())?;
1422        wtr.write_str(":")?;
1423        wtr.write_int(&FMT_TIME_UNIT, dt.second())?;
1424        wtr.write_str(" ")?;
1425
1426        let Some(offset) = offset else {
1427            wtr.write_str("-0000")?;
1428            return Ok(());
1429        };
1430        wtr.write_str(if offset.is_negative() { "-" } else { "+" })?;
1431        let mut hours = offset.part_hours_ranged().abs().get();
1432        let mut minutes = offset.part_minutes_ranged().abs().get();
1433        // RFC 2822, like RFC 3339, requires that time zone offsets are an
1434        // integral number of minutes. While rounding based on seconds doesn't
1435        // seem clearly indicated, we choose to do that here. An alternative
1436        // would be to return an error. It isn't clear how important this is in
1437        // practice though.
1438        if offset.part_seconds_ranged().abs() >= C(30) {
1439            if minutes == 59 {
1440                hours = hours.saturating_add(1);
1441                minutes = 0;
1442            } else {
1443                minutes = minutes.saturating_add(1);
1444            }
1445        }
1446        wtr.write_int(&FMT_TIME_UNIT, hours)?;
1447        wtr.write_int(&FMT_TIME_UNIT, minutes)?;
1448        Ok(())
1449    }
1450
1451    fn print_civil_always_utc<W: Write>(
1452        &self,
1453        timestamp: &Timestamp,
1454        mut wtr: W,
1455    ) -> Result<(), Error> {
1456        static FMT_DAY: DecimalFormatter = DecimalFormatter::new().padding(2);
1457        static FMT_YEAR: DecimalFormatter = DecimalFormatter::new().padding(4);
1458        static FMT_TIME_UNIT: DecimalFormatter =
1459            DecimalFormatter::new().padding(2);
1460
1461        let dt = TimeZone::UTC.to_datetime(*timestamp);
1462        if dt.year() < 0 {
1463            // RFC 2822 actually says the year must be at least 1900, but
1464            // other implementations (like Chrono) allow any positive 4-digit
1465            // year.
1466            return Err(err!(
1467                "datetime {dt} has negative year, \
1468                 which cannot be formatted with RFC 2822",
1469            ));
1470        }
1471
1472        wtr.write_str(weekday_abbrev(dt.weekday()))?;
1473        wtr.write_str(", ")?;
1474        wtr.write_int(&FMT_DAY, dt.day())?;
1475        wtr.write_str(" ")?;
1476        wtr.write_str(month_name(dt.month()))?;
1477        wtr.write_str(" ")?;
1478        wtr.write_int(&FMT_YEAR, dt.year())?;
1479        wtr.write_str(" ")?;
1480        wtr.write_int(&FMT_TIME_UNIT, dt.hour())?;
1481        wtr.write_str(":")?;
1482        wtr.write_int(&FMT_TIME_UNIT, dt.minute())?;
1483        wtr.write_str(":")?;
1484        wtr.write_int(&FMT_TIME_UNIT, dt.second())?;
1485        wtr.write_str(" ")?;
1486        wtr.write_str("GMT")?;
1487        Ok(())
1488    }
1489}
1490
1491fn weekday_abbrev(wd: Weekday) -> &'static str {
1492    match wd {
1493        Weekday::Sunday => "Sun",
1494        Weekday::Monday => "Mon",
1495        Weekday::Tuesday => "Tue",
1496        Weekday::Wednesday => "Wed",
1497        Weekday::Thursday => "Thu",
1498        Weekday::Friday => "Fri",
1499        Weekday::Saturday => "Sat",
1500    }
1501}
1502
1503fn month_name(month: i8) -> &'static str {
1504    match month {
1505        1 => "Jan",
1506        2 => "Feb",
1507        3 => "Mar",
1508        4 => "Apr",
1509        5 => "May",
1510        6 => "Jun",
1511        7 => "Jul",
1512        8 => "Aug",
1513        9 => "Sep",
1514        10 => "Oct",
1515        11 => "Nov",
1516        12 => "Dec",
1517        _ => unreachable!("invalid month value {month}"),
1518    }
1519}
1520
1521/// Returns true if the given byte is "whitespace" as defined by RFC 2822.
1522///
1523/// From S2.2.2:
1524///
1525/// > Many of these tokens are allowed (according to their syntax) to be
1526/// > introduced or end with comments (as described in section 3.2.3) as well
1527/// > as the space (SP, ASCII value 32) and horizontal tab (HTAB, ASCII value
1528/// > 9) characters (together known as the white space characters, WSP), and
1529/// > those WSP characters are subject to header "folding" and "unfolding" as
1530/// > described in section 2.2.3.
1531///
1532/// In other words, ASCII space or tab.
1533///
1534/// With all that said, it seems odd to limit this to just spaces or tabs, so
1535/// we relax this and let it absorb any kind of ASCII whitespace. This also
1536/// handles, I believe, most cases of "folding" whitespace. (By treating `\r`
1537/// and `\n` as whitespace.)
1538fn is_whitespace(byte: u8) -> bool {
1539    byte.is_ascii_whitespace()
1540}
1541
1542#[cfg(feature = "alloc")]
1543#[cfg(test)]
1544mod tests {
1545    use alloc::string::{String, ToString};
1546
1547    use crate::civil::date;
1548
1549    use super::*;
1550
1551    #[test]
1552    fn ok_parse_basic() {
1553        let p = |input| DateTimeParser::new().parse_zoned(input).unwrap();
1554
1555        insta::assert_debug_snapshot!(
1556            p("Wed, 10 Jan 2024 05:34:45 -0500"),
1557            @"2024-01-10T05:34:45-05:00[-05:00]",
1558        );
1559        insta::assert_debug_snapshot!(
1560            p("Tue, 9 Jan 2024 05:34:45 -0500"),
1561            @"2024-01-09T05:34:45-05:00[-05:00]",
1562        );
1563        insta::assert_debug_snapshot!(
1564            p("Tue, 09 Jan 2024 05:34:45 -0500"),
1565            @"2024-01-09T05:34:45-05:00[-05:00]",
1566        );
1567        insta::assert_debug_snapshot!(
1568            p("10 Jan 2024 05:34:45 -0500"),
1569            @"2024-01-10T05:34:45-05:00[-05:00]",
1570        );
1571        insta::assert_debug_snapshot!(
1572            p("10 Jan 2024 05:34 -0500"),
1573            @"2024-01-10T05:34:00-05:00[-05:00]",
1574        );
1575        insta::assert_debug_snapshot!(
1576            p("10 Jan 2024 05:34:45 +0500"),
1577            @"2024-01-10T05:34:45+05:00[+05:00]",
1578        );
1579        insta::assert_debug_snapshot!(
1580            p("Thu, 29 Feb 2024 05:34 -0500"),
1581            @"2024-02-29T05:34:00-05:00[-05:00]",
1582        );
1583
1584        // leap second constraining
1585        insta::assert_debug_snapshot!(
1586            p("10 Jan 2024 05:34:60 -0500"),
1587            @"2024-01-10T05:34:59-05:00[-05:00]",
1588        );
1589    }
1590
1591    #[test]
1592    fn ok_parse_obsolete_zone() {
1593        let p = |input| DateTimeParser::new().parse_zoned(input).unwrap();
1594
1595        insta::assert_debug_snapshot!(
1596            p("Wed, 10 Jan 2024 05:34:45 EST"),
1597            @"2024-01-10T05:34:45-05:00[-05:00]",
1598        );
1599        insta::assert_debug_snapshot!(
1600            p("Wed, 10 Jan 2024 05:34:45 EDT"),
1601            @"2024-01-10T05:34:45-04:00[-04:00]",
1602        );
1603        insta::assert_debug_snapshot!(
1604            p("Wed, 10 Jan 2024 05:34:45 CST"),
1605            @"2024-01-10T05:34:45-06:00[-06:00]",
1606        );
1607        insta::assert_debug_snapshot!(
1608            p("Wed, 10 Jan 2024 05:34:45 CDT"),
1609            @"2024-01-10T05:34:45-05:00[-05:00]",
1610        );
1611        insta::assert_debug_snapshot!(
1612            p("Wed, 10 Jan 2024 05:34:45 mst"),
1613            @"2024-01-10T05:34:45-07:00[-07:00]",
1614        );
1615        insta::assert_debug_snapshot!(
1616            p("Wed, 10 Jan 2024 05:34:45 mdt"),
1617            @"2024-01-10T05:34:45-06:00[-06:00]",
1618        );
1619        insta::assert_debug_snapshot!(
1620            p("Wed, 10 Jan 2024 05:34:45 pst"),
1621            @"2024-01-10T05:34:45-08:00[-08:00]",
1622        );
1623        insta::assert_debug_snapshot!(
1624            p("Wed, 10 Jan 2024 05:34:45 pdt"),
1625            @"2024-01-10T05:34:45-07:00[-07:00]",
1626        );
1627
1628        // Various things that mean UTC.
1629        insta::assert_debug_snapshot!(
1630            p("Wed, 10 Jan 2024 05:34:45 UT"),
1631            @"2024-01-10T05:34:45+00:00[UTC]",
1632        );
1633        insta::assert_debug_snapshot!(
1634            p("Wed, 10 Jan 2024 05:34:45 Z"),
1635            @"2024-01-10T05:34:45+00:00[UTC]",
1636        );
1637        insta::assert_debug_snapshot!(
1638            p("Wed, 10 Jan 2024 05:34:45 gmt"),
1639            @"2024-01-10T05:34:45+00:00[UTC]",
1640        );
1641
1642        // Even things that are unrecognized just get treated as having
1643        // an offset of 0.
1644        insta::assert_debug_snapshot!(
1645            p("Wed, 10 Jan 2024 05:34:45 XXX"),
1646            @"2024-01-10T05:34:45+00:00[UTC]",
1647        );
1648        insta::assert_debug_snapshot!(
1649            p("Wed, 10 Jan 2024 05:34:45 ABCDE"),
1650            @"2024-01-10T05:34:45+00:00[UTC]",
1651        );
1652        insta::assert_debug_snapshot!(
1653            p("Wed, 10 Jan 2024 05:34:45 FUCK"),
1654            @"2024-01-10T05:34:45+00:00[UTC]",
1655        );
1656    }
1657
1658    // whyyyyyyyyyyyyy
1659    #[test]
1660    fn ok_parse_comment() {
1661        let p = |input| DateTimeParser::new().parse_zoned(input).unwrap();
1662
1663        insta::assert_debug_snapshot!(
1664            p("Wed, 10 Jan 2024 05:34:45 -0500 (wat)"),
1665            @"2024-01-10T05:34:45-05:00[-05:00]",
1666        );
1667        insta::assert_debug_snapshot!(
1668            p("Wed, 10 Jan 2024 05:34:45 -0500 (w(a)t)"),
1669            @"2024-01-10T05:34:45-05:00[-05:00]",
1670        );
1671        insta::assert_debug_snapshot!(
1672            p(r"Wed, 10 Jan 2024 05:34:45 -0500 (w\(a\)t)"),
1673            @"2024-01-10T05:34:45-05:00[-05:00]",
1674        );
1675    }
1676
1677    #[test]
1678    fn ok_parse_whitespace() {
1679        let p = |input| DateTimeParser::new().parse_zoned(input).unwrap();
1680
1681        insta::assert_debug_snapshot!(
1682            p("Wed, 10 \t   Jan \n\r\n\n 2024       05:34:45    -0500"),
1683            @"2024-01-10T05:34:45-05:00[-05:00]",
1684        );
1685        insta::assert_debug_snapshot!(
1686            p("Wed, 10 Jan 2024 05:34:45 -0500 "),
1687            @"2024-01-10T05:34:45-05:00[-05:00]",
1688        );
1689    }
1690
1691    #[test]
1692    fn err_parse_invalid() {
1693        let p = |input| {
1694            DateTimeParser::new().parse_zoned(input).unwrap_err().to_string()
1695        };
1696
1697        insta::assert_snapshot!(
1698            p("Thu, 10 Jan 2024 05:34:45 -0500"),
1699            @"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",
1700        );
1701        insta::assert_snapshot!(
1702            p("Wed, 29 Feb 2023 05:34:45 -0500"),
1703            @"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",
1704        );
1705        insta::assert_snapshot!(
1706            p("Mon, 31 Jun 2024 05:34:45 -0500"),
1707            @"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",
1708        );
1709        insta::assert_snapshot!(
1710            p("Tue, 32 Jun 2024 05:34:45 -0500"),
1711            @"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",
1712        );
1713        insta::assert_snapshot!(
1714            p("Sun, 30 Jun 2024 24:00:00 -0500"),
1715            @"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",
1716        );
1717    }
1718
1719    #[test]
1720    fn err_parse_incomplete() {
1721        let p = |input| {
1722            DateTimeParser::new().parse_zoned(input).unwrap_err().to_string()
1723        };
1724
1725        insta::assert_snapshot!(
1726            p(""),
1727            @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected RFC 2822 datetime, but got empty string",
1728        );
1729        insta::assert_snapshot!(
1730            p(" "),
1731            @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected RFC 2822 datetime, but got empty string after trimming whitespace",
1732        );
1733        insta::assert_snapshot!(
1734            p("Wat"),
1735            @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)"###,
1736        );
1737        insta::assert_snapshot!(
1738            p("Wed"),
1739            @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)"###,
1740        );
1741        insta::assert_snapshot!(
1742            p("Wat, "),
1743            @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"###,
1744        );
1745        insta::assert_snapshot!(
1746            p("Wed, "),
1747            @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day, but found end of input",
1748        );
1749        insta::assert_snapshot!(
1750            p("Wed, 1"),
1751            @"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",
1752        );
1753        insta::assert_snapshot!(
1754            p("Wed, 10"),
1755            @"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",
1756        );
1757        insta::assert_snapshot!(
1758            p("Wed, 10 J"),
1759            @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected abbreviated month name, but remaining input is too short (remaining bytes is 1)",
1760        );
1761        insta::assert_snapshot!(
1762            p("Wed, 10 Wat"),
1763            @r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected abbreviated month name, but did not recognize "Wat" as a valid month"###,
1764        );
1765        insta::assert_snapshot!(
1766            p("Wed, 10 Jan"),
1767            @"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",
1768        );
1769        insta::assert_snapshot!(
1770            p("Wed, 10 Jan 2"),
1771            @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected at least two ASCII digits for parsing a year, but only found 1",
1772        );
1773        insta::assert_snapshot!(
1774            p("Wed, 10 Jan 2024"),
1775            @"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",
1776        );
1777        insta::assert_snapshot!(
1778            p("Wed, 10 Jan 2024 05"),
1779            @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected time separator of ':', but found end of input",
1780        );
1781        insta::assert_snapshot!(
1782            p("Wed, 10 Jan 2024 053"),
1783            @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected time separator of ':', but found 3",
1784        );
1785        insta::assert_snapshot!(
1786            p("Wed, 10 Jan 2024 05:34"),
1787            @"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",
1788        );
1789        insta::assert_snapshot!(
1790            p("Wed, 10 Jan 2024 05:34:"),
1791            @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected two digit second, but found end of input",
1792        );
1793        insta::assert_snapshot!(
1794            p("Wed, 10 Jan 2024 05:34:45"),
1795            @"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",
1796        );
1797        insta::assert_snapshot!(
1798            p("Wed, 10 Jan 2024 05:34:45 J"),
1799            @r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected obsolete RFC 2822 time zone abbreviation, but found "J""###,
1800        );
1801    }
1802
1803    #[test]
1804    fn err_parse_comment() {
1805        let p = |input| {
1806            DateTimeParser::new().parse_zoned(input).unwrap_err().to_string()
1807        };
1808
1809        insta::assert_snapshot!(
1810            p(r"Wed, 10 Jan 2024 05:34:45 -0500 (wa)t)"),
1811            @r###"parsed value '2024-01-10T05:34:45-05:00[-05:00]', but unparsed input "t)" remains (expected no unparsed input)"###,
1812        );
1813        insta::assert_snapshot!(
1814            p(r"Wed, 10 Jan 2024 05:34:45 -0500 (wa(t)"),
1815            @"failed to parse RFC 2822 datetime into Jiff zoned datetime: found opening parenthesis in comment with no matching closing parenthesis",
1816        );
1817        insta::assert_snapshot!(
1818            p(r"Wed, 10 Jan 2024 05:34:45 -0500 (w"),
1819            @"failed to parse RFC 2822 datetime into Jiff zoned datetime: found opening parenthesis in comment with no matching closing parenthesis",
1820        );
1821        insta::assert_snapshot!(
1822            p(r"Wed, 10 Jan 2024 05:34:45 -0500 ("),
1823            @"failed to parse RFC 2822 datetime into Jiff zoned datetime: found opening parenthesis in comment with no matching closing parenthesis",
1824        );
1825        insta::assert_snapshot!(
1826            p(r"Wed, 10 Jan 2024 05:34:45 -0500 (  "),
1827            @"failed to parse RFC 2822 datetime into Jiff zoned datetime: found opening parenthesis in comment with no matching closing parenthesis",
1828        );
1829    }
1830
1831    #[test]
1832    fn ok_print_zoned() {
1833        if crate::tz::db().is_definitively_empty() {
1834            return;
1835        }
1836
1837        let p = |zdt: &Zoned| -> String {
1838            let mut buf = String::new();
1839            DateTimePrinter::new().print_zoned(&zdt, &mut buf).unwrap();
1840            buf
1841        };
1842
1843        let zdt = date(2024, 1, 10)
1844            .at(5, 34, 45, 0)
1845            .in_tz("America/New_York")
1846            .unwrap();
1847        insta::assert_snapshot!(p(&zdt), @"Wed, 10 Jan 2024 05:34:45 -0500");
1848
1849        let zdt = date(2024, 2, 5)
1850            .at(5, 34, 45, 0)
1851            .in_tz("America/New_York")
1852            .unwrap();
1853        insta::assert_snapshot!(p(&zdt), @"Mon, 5 Feb 2024 05:34:45 -0500");
1854
1855        let zdt = date(2024, 7, 31)
1856            .at(5, 34, 45, 0)
1857            .in_tz("America/New_York")
1858            .unwrap();
1859        insta::assert_snapshot!(p(&zdt), @"Wed, 31 Jul 2024 05:34:45 -0400");
1860
1861        let zdt = date(2024, 3, 5).at(5, 34, 45, 0).in_tz("UTC").unwrap();
1862        // Notice that this prints a +0000 offset.
1863        // But when printing a Timestamp, a -0000 offset is used.
1864        // This is because in the case of Timestamp, the "true"
1865        // offset is not known.
1866        insta::assert_snapshot!(p(&zdt), @"Tue, 5 Mar 2024 05:34:45 +0000");
1867    }
1868
1869    #[test]
1870    fn ok_print_timestamp() {
1871        if crate::tz::db().is_definitively_empty() {
1872            return;
1873        }
1874
1875        let p = |ts: Timestamp| -> String {
1876            let mut buf = String::new();
1877            DateTimePrinter::new().print_timestamp(&ts, &mut buf).unwrap();
1878            buf
1879        };
1880
1881        let ts = date(2024, 1, 10)
1882            .at(5, 34, 45, 0)
1883            .in_tz("America/New_York")
1884            .unwrap()
1885            .timestamp();
1886        insta::assert_snapshot!(p(ts), @"Wed, 10 Jan 2024 10:34:45 -0000");
1887
1888        let ts = date(2024, 2, 5)
1889            .at(5, 34, 45, 0)
1890            .in_tz("America/New_York")
1891            .unwrap()
1892            .timestamp();
1893        insta::assert_snapshot!(p(ts), @"Mon, 5 Feb 2024 10:34:45 -0000");
1894
1895        let ts = date(2024, 7, 31)
1896            .at(5, 34, 45, 0)
1897            .in_tz("America/New_York")
1898            .unwrap()
1899            .timestamp();
1900        insta::assert_snapshot!(p(ts), @"Wed, 31 Jul 2024 09:34:45 -0000");
1901
1902        let ts = date(2024, 3, 5)
1903            .at(5, 34, 45, 0)
1904            .in_tz("UTC")
1905            .unwrap()
1906            .timestamp();
1907        // Notice that this prints a +0000 offset.
1908        // But when printing a Timestamp, a -0000 offset is used.
1909        // This is because in the case of Timestamp, the "true"
1910        // offset is not known.
1911        insta::assert_snapshot!(p(ts), @"Tue, 5 Mar 2024 05:34:45 -0000");
1912    }
1913
1914    #[test]
1915    fn ok_print_rfc9110_timestamp() {
1916        if crate::tz::db().is_definitively_empty() {
1917            return;
1918        }
1919
1920        let p = |ts: Timestamp| -> String {
1921            let mut buf = String::new();
1922            DateTimePrinter::new()
1923                .print_timestamp_rfc9110(&ts, &mut buf)
1924                .unwrap();
1925            buf
1926        };
1927
1928        let ts = date(2024, 1, 10)
1929            .at(5, 34, 45, 0)
1930            .in_tz("America/New_York")
1931            .unwrap()
1932            .timestamp();
1933        insta::assert_snapshot!(p(ts), @"Wed, 10 Jan 2024 10:34:45 GMT");
1934
1935        let ts = date(2024, 2, 5)
1936            .at(5, 34, 45, 0)
1937            .in_tz("America/New_York")
1938            .unwrap()
1939            .timestamp();
1940        insta::assert_snapshot!(p(ts), @"Mon, 05 Feb 2024 10:34:45 GMT");
1941
1942        let ts = date(2024, 7, 31)
1943            .at(5, 34, 45, 0)
1944            .in_tz("America/New_York")
1945            .unwrap()
1946            .timestamp();
1947        insta::assert_snapshot!(p(ts), @"Wed, 31 Jul 2024 09:34:45 GMT");
1948
1949        let ts = date(2024, 3, 5)
1950            .at(5, 34, 45, 0)
1951            .in_tz("UTC")
1952            .unwrap()
1953            .timestamp();
1954        // Notice that this prints a +0000 offset.
1955        // But when printing a Timestamp, a -0000 offset is used.
1956        // This is because in the case of Timestamp, the "true"
1957        // offset is not known.
1958        insta::assert_snapshot!(p(ts), @"Tue, 05 Mar 2024 05:34:45 GMT");
1959    }
1960
1961    #[test]
1962    fn err_print_zoned() {
1963        if crate::tz::db().is_definitively_empty() {
1964            return;
1965        }
1966
1967        let p = |zdt: &Zoned| -> String {
1968            let mut buf = String::new();
1969            DateTimePrinter::new()
1970                .print_zoned(&zdt, &mut buf)
1971                .unwrap_err()
1972                .to_string()
1973        };
1974
1975        let zdt = date(-1, 1, 10)
1976            .at(5, 34, 45, 0)
1977            .in_tz("America/New_York")
1978            .unwrap();
1979        insta::assert_snapshot!(p(&zdt), @"datetime -000001-01-10T05:34:45 has negative year, which cannot be formatted with RFC 2822");
1980    }
1981
1982    #[test]
1983    fn err_print_timestamp() {
1984        if crate::tz::db().is_definitively_empty() {
1985            return;
1986        }
1987
1988        let p = |ts: Timestamp| -> String {
1989            let mut buf = String::new();
1990            DateTimePrinter::new()
1991                .print_timestamp(&ts, &mut buf)
1992                .unwrap_err()
1993                .to_string()
1994        };
1995
1996        let ts = date(-1, 1, 10)
1997            .at(5, 34, 45, 0)
1998            .in_tz("America/New_York")
1999            .unwrap()
2000            .timestamp();
2001        insta::assert_snapshot!(p(ts), @"datetime -000001-01-10T10:30:47 has negative year, which cannot be formatted with RFC 2822");
2002    }
2003}