Repository: time-rs/time (language: rust)

Issue:
Add RFC2822 well-known format description 
RFC2822 formatted dates are used in many places in web contexts, one of the more noteworthy of those being `Date` headers in the HTTP protocol. An example of a date in this format could be `Fri, 21 Nov 1997 09:55:06 -0600`.

As an additional note, the `chrono` crate has 2822 as a well known format, and adding it to `time` could be a big help to those interesting in migrating to `time`.

[The RFC2822 specification](https://datatracker.ietf.org/doc/html/rfc2822)


Discussion / hints:
A PR implementing this would be accepted. From quickly looking at the spec, it should largely be able to be parsed using pre-existing components.

Fix patch:
diff --git a/src/format_description/mod.rs b/src/format_description/mod.rs
index f3aaaa75e..8832c3442 100644
--- a/src/format_description/mod.rs
+++ b/src/format_description/mod.rs
@@ -61,6 +61,31 @@ pub mod well_known {
     /// ```
     #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     pub struct Rfc3339;
+
+    /// The format described in [RFC 2822](https://tools.ietf.org/html/rfc2822#section-3.3).
+    ///
+    /// Example: Fri, 21 Nov 1997 09:55:06 -0600
+    ///
+    /// # Examples
+    /// ```rust
+    /// # use time::{format_description::well_known::Rfc2822, macros::datetime, OffsetDateTime};
+    /// assert_eq!(
+    ///     OffsetDateTime::parse("Sat, 12 Jun 1993 13:25:19 GMT", &Rfc2822)?,
+    ///     datetime!(1993-06-12 13:25:19 +00:00)
+    /// );
+    /// # Ok::<_, time::Error>(())
+    /// ```
+    ///
+    /// ```rust
+    /// # use time::{format_description::well_known::Rfc2822, macros::datetime};
+    /// assert_eq!(
+    ///     datetime!(1997-11-21 09:55:06 -06:00).format(&Rfc2822)?,
+    ///     "Fri, 21 Nov 1997 09:55:06 -0600"
+    /// );
+    /// # Ok::<_, time::Error>(())
+    /// ```
+    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
+    pub struct Rfc2822;
 }
 
 /// A complete description of how to format and parse a type.
diff --git a/src/formatting/formattable.rs b/src/formatting/formattable.rs
index 036796e4b..8c51bfa27 100644
--- a/src/formatting/formattable.rs
+++ b/src/formatting/formattable.rs
@@ -3,9 +3,11 @@
 use core::ops::Deref;
 use std::io;
 
-use crate::format_description::well_known::Rfc3339;
+use crate::format_description::well_known::{Rfc2822, Rfc3339};
 use crate::format_description::FormatItem;
-use crate::formatting::{format_component, format_number_pad_zero, write};
+use crate::formatting::{
+    format_component, format_number_pad_zero, write, MONTH_NAMES, WEEKDAY_NAMES,
+};
 use crate::{error, Date, Time, UtcOffset};
 
 /// A type that can be formatted.
@@ -14,6 +16,7 @@ pub trait Formattable: sealed::Sealed {}
 impl Formattable for FormatItem<'_> {}
 impl Formattable for [FormatItem<'_>] {}
 impl Formattable for Rfc3339 {}
+impl Formattable for Rfc2822 {}
 impl<T: Deref> Formattable for T where T::Target: Formattable {}
 
 /// Seal the trait to prevent downstream users from implementing it.
@@ -102,6 +105,54 @@ where
 // endregion custom formats
 
 // region: well-known formats
+impl sealed::Sealed for Rfc2822 {
+    fn format_into(
+        &self,
+        output: &mut impl io::Write,
+        date: Option<Date>,
+        time: Option<Time>,
+        offset: Option<UtcOffset>,
+    ) -> Result<usize, error::Format> {
+        let date = date.ok_or(error::Format::InsufficientTypeInformation)?;
+        let time = time.ok_or(error::Format::InsufficientTypeInformation)?;
+        let offset = offset.ok_or(error::Format::InsufficientTypeInformation)?;
+
+        let mut bytes = 0;
+
+        let (year, month, day) = date.to_calendar_date();
+
+        if year < 1900 {
+            return Err(error::Format::InvalidComponent("year"));
+        }
+        if offset.seconds_past_minute() != 0 {
+            return Err(error::Format::InvalidComponent("offset_second"));
+        }
+
+        bytes += write(
+            output,
+            &WEEKDAY_NAMES[date.weekday().number_days_from_monday() as usize][..3],
+        )?;
+        bytes += write(output, b", ")?;
+        bytes += format_number_pad_zero::<_, _, 2>(output, day)?;
+        bytes += write(output, b" ")?;
+        bytes += write(output, &MONTH_NAMES[month as usize - 1][..3])?;
+        bytes += write(output, b" ")?;
+        bytes += format_number_pad_zero::<_, _, 4>(output, year as u32)?;
+        bytes += write(output, b" ")?;
+        bytes += format_number_pad_zero::<_, _, 2>(output, time.hour())?;
+        bytes += write(output, b":")?;
+        bytes += format_number_pad_zero::<_, _, 2>(output, time.minute())?;
+        bytes += write(output, b":")?;
+        bytes += format_number_pad_zero::<_, _, 2>(output, time.second())?;
+        bytes += write(output, b" ")?;
+        bytes += write(output, if offset.is_negative() { b"-" } else { b"+" })?;
+        bytes += format_number_pad_zero::<_, _, 2>(output, offset.whole_hours().unsigned_abs())?;
+        bytes +=
+            format_number_pad_zero::<_, _, 2>(output, offset.minutes_past_hour().unsigned_abs())?;
+
+        Ok(bytes)
+    }
+}
 impl sealed::Sealed for Rfc3339 {
     fn format_into(
         &self,
diff --git a/src/parsing/combinator.rs b/src/parsing/combinator/mod.rs
similarity index 86%
rename from src/parsing/combinator.rs
rename to src/parsing/combinator/mod.rs
index 92b012143..64a587cd2 100644
--- a/src/parsing/combinator.rs
+++ b/src/parsing/combinator/mod.rs
@@ -1,5 +1,7 @@
 //! Implementations of the low-level parser combinators.
 
+pub(crate) mod rfc;
+
 use crate::format_description::modifier::Padding;
 use crate::parsing::shim::{Integer, IntegerParseBytes};
 use crate::parsing::ParsedItem;
@@ -37,6 +39,31 @@ pub(crate) fn first_match<'a, 'b: 'a, T: Copy + 'a>(
     }
 }
 
+/// Consume zero or more instances of the provided parser. The parser must return the unit value.
+pub(crate) fn zero_or_more<'a, P: Fn(&'a [u8]) -> Option<ParsedItem<'a, ()>>>(
+    parser: P,
+) -> impl FnMut(&'a [u8]) -> ParsedItem<'a, ()> {
+    move |mut input| {
+        while let Some(remaining) = parser(input) {
+            input = remaining.into_inner();
+        }
+        ParsedItem(input, ())
+    }
+}
+
+/// Consume one of or more instances of the provided parser. The parser must produce the unit value.
+pub(crate) fn one_or_more<'a, P: Fn(&'a [u8]) -> Option<ParsedItem<'a, ()>>>(
+    parser: P,
+) -> impl Fn(&'a [u8]) -> Option<ParsedItem<'a, ()>> {
+    move |mut input| {
+        input = parser(input)?.into_inner();
+        while let Some(remaining) = parser(input) {
+            input = remaining.into_inner();
+        }
+        Some(ParsedItem(input, ()))
+    }
+}
+
 /// Consume between `n` and `m` instances of the provided parser.
 pub(crate) fn n_to_m<
     'a,
diff --git a/src/parsing/combinator/rfc/mod.rs b/src/parsing/combinator/rfc/mod.rs
new file mode 100644
index 000000000..44b9c66b3
--- /dev/null
+++ b/src/parsing/combinator/rfc/mod.rs
@@ -0,0 +1,8 @@
+//! Combinators for rules as defined in an RFC.
+//!
+//! These rules have been converted strictly following the ABNF syntax as specified in [RFC 2234].
+//!
+//! [RFC 2234]: https://datatracker.ietf.org/doc/html/rfc2234
+
+pub(crate) mod rfc2234;
+pub(crate) mod rfc2822;
diff --git a/src/parsing/combinator/rfc/rfc2234.rs b/src/parsing/combinator/rfc/rfc2234.rs
new file mode 100644
index 000000000..799d25e83
--- /dev/null
+++ b/src/parsing/combinator/rfc/rfc2234.rs
@@ -0,0 +1,13 @@
+//! Rules defined in [RFC 2234].
+//!
+//! [RFC 2234]: https://datatracker.ietf.org/doc/html/rfc2234
+
+use crate::parsing::ParsedItem;
+
+/// Consume exactly one space or tab.
+pub(crate) const fn wsp(input: &[u8]) -> Option<ParsedItem<'_, ()>> {
+    match input {
+        [b' ', rest @ ..] | [b'\t', rest @ ..] => Some(ParsedItem(rest, ())),
+        _ => None,
+    }
+}
diff --git a/src/parsing/combinator/rfc/rfc2822.rs b/src/parsing/combinator/rfc/rfc2822.rs
new file mode 100644
index 000000000..100edd5a1
--- /dev/null
+++ b/src/parsing/combinator/rfc/rfc2822.rs
@@ -0,0 +1,120 @@
+//! Rules defined in [RFC 2822].
+//!
+//! [RFC 2822]: https://datatracker.ietf.org/doc/html/rfc2822
+
+use crate::parsing::combinator::rfc::rfc2234::wsp;
+use crate::parsing::combinator::{ascii_char, one_or_more, zero_or_more};
+use crate::parsing::ParsedItem;
+
+/// Consume the `fws` rule.
+// The full rule is equivalent to /\r\n[ \t]+|[ \t]+(?:\r\n[ \t]+)*/
+pub(crate) fn fws(mut input: &[u8]) -> Option<ParsedItem<'_, ()>> {
+    if let [b'\r', b'\n', rest @ ..] = input {
+        one_or_more(wsp)(rest)
+    } else {
+        input = one_or_more(wsp)(input)?.into_inner();
+        while let [b'\r', b'\n', rest @ ..] = input {
+            input = one_or_more(wsp)(rest)?.into_inner();
+        }
+        Some(ParsedItem(input, ()))
+    }
+}
+
+/// Consume the `cfws` rule.
+// The full rule is equivalent to any combination of `fws` and `comment` so long as it is not empty.
+pub(crate) fn cfws(input: &[u8]) -> Option<ParsedItem<'_, ()>> {
+    one_or_more(|input| fws(input).or_else(|| comment(input)))(input)
+}
+
+/// Consume the `comment` rule.
+fn comment(mut input: &[u8]) -> Option<ParsedItem<'_, ()>> {
+    input = ascii_char::<b'('>(input)?.into_inner();
+    input = zero_or_more(fws)(input).into_inner();
+    while let Some(rest) = ccontent(input) {
+        input = rest.into_inner();
+        input = zero_or_more(fws)(input).into_inner();
+    }
+    input = ascii_char::<b')'>(input)?.into_inner();
+
+    Some(ParsedItem(input, ()))
+}
+
+/// Consume the `ccontent` rule.
+fn ccontent(input: &[u8]) -> Option<ParsedItem<'_, ()>> {
+    ctext(input)
+        .or_else(|| quoted_pair(input))
+        .or_else(|| comment(input))
+}
+
+/// Consume the `ctext` rule.
+fn ctext(input: &[u8]) -> Option<ParsedItem<'_, ()>> {
+    no_ws_ctl(input).or_else(|| match input {
+        [33..=39, rest @ ..] | [42..=91, rest @ ..] | [93..=126, rest @ ..] => {
+            Some(ParsedItem(rest, ()))
+        }
+        _ => None,
+    })
+}
+
+/// Consume the `quoted_pair` rule.
+fn quoted_pair(mut input: &[u8]) -> Option<ParsedItem<'_, ()>> {
+    input = ascii_char::<b'\\'>(input)?.into_inner();
+
+    let old_input_len = input.len();
+
+    input = text(input).into_inner();
+
+    // If nothing is parsed, this means we hit the `obs-text` rule and nothing matched. This is
+    // technically a success, but we should still check the `obs-qp` rule to ensure we consume
+    // everything possible.
+    if input.len() == old_input_len {
+        match input {
+            [0..=127, rest @ ..] => Some(ParsedItem(rest, ())),
+            _ => Some(ParsedItem(input, ())),
+        }
+    } else {
+        Some(ParsedItem(input, ()))
+    }
+}
+
+/// Consume the `no_ws_ctl` rule.
+const fn no_ws_ctl(input: &[u8]) -> Option<ParsedItem<'_, ()>> {
+    match input {
+        [1..=8, rest @ ..] | [11..=12, rest @ ..] | [14..=31, rest @ ..] | [127, rest @ ..] => {
+            Some(ParsedItem(rest, ()))
+        }
+        _ => None,
+    }
+}
+
+/// Consume the `text` rule.
+fn text<'a>(input: &'a [u8]) -> ParsedItem<'a, ()> {
+    let new_text = |input: &'a [u8]| match input {
+        [1..=9, rest @ ..] | [11..=12, rest @ ..] | [14..=127, rest @ ..] => {
+            Some(ParsedItem(rest, ()))
+        }
+        _ => None,
+    };
+
+    let obs_char = |input: &'a [u8]| match input {
+        // This is technically allowed, but consuming this would mean the rest of the string is
+        // eagerly consumed without consideration for where the comment actually ends.
+        [b')', ..] => None,
+        [0..=9, rest @ ..] | [11..=12, rest @ ..] | [14..=127, rest @ ..] => Some(rest),
+        _ => None,
+    };
+
+    let obs_text = |mut input| {
+        input = zero_or_more(ascii_char::<b'\n'>)(input).into_inner();
+        input = zero_or_more(ascii_char::<b'\r'>)(input).into_inner();
+        while let Some(rest) = obs_char(input) {
+            input = rest;
+            input = zero_or_more(ascii_char::<b'\n'>)(input).into_inner();
+            input = zero_or_more(ascii_char::<b'\r'>)(input).into_inner();
+        }
+
+        ParsedItem(input, ())
+    };
+
+    new_text(input).unwrap_or_else(|| obs_text(input))
+}
diff --git a/src/parsing/mod.rs b/src/parsing/mod.rs
index 297710257..46624bafb 100644
--- a/src/parsing/mod.rs
+++ b/src/parsing/mod.rs
@@ -33,6 +33,7 @@ impl<'a, T> ParsedItem<'a, T> {
     }
 
     /// Assign the stored value to the provided target. The remaining input is returned.
+    #[must_use = "this returns the remaining input"]
     pub(crate) fn assign_value_to(self, target: &mut Option<T>) -> &'a [u8] {
         *target = Some(self.1);
         self.0
@@ -41,6 +42,15 @@ impl<'a, T> ParsedItem<'a, T> {
 
 impl<'a> ParsedItem<'a, ()> {
     /// Discard the unit value, returning the remaining input.
+    #[must_use = "this returns the remaining input"]
+    pub(crate) const fn into_inner(self) -> &'a [u8] {
+        self.0
+    }
+}
+
+impl<'a> ParsedItem<'a, Option<()>> {
+    /// Discard the potential unit value, returning the remaining input.
+    #[must_use = "this returns the remaining input"]
     pub(crate) const fn into_inner(self) -> &'a [u8] {
         self.0
     }
diff --git a/src/parsing/parsable.rs b/src/parsing/parsable.rs
index 4a83df0ae..b68ee5863 100644
--- a/src/parsing/parsable.rs
+++ b/src/parsing/parsable.rs
@@ -4,16 +4,17 @@ use core::convert::TryInto;
 use core::ops::Deref;
 
 use crate::error::TryFromParsed;
-use crate::format_description::well_known::Rfc3339;
+use crate::format_description::well_known::{Rfc2822, Rfc3339};
 use crate::format_description::FormatItem;
 use crate::parsing::{Parsed, ParsedItem};
-use crate::{error, Date, Month, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset};
+use crate::{error, Date, Month, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset, Weekday};
 
 /// A type that can be parsed.
 #[cfg_attr(__time_03_docs, doc(notable_trait))]
 pub trait Parsable: sealed::Sealed {}
 impl Parsable for FormatItem<'_> {}
 impl Parsable for [FormatItem<'_>] {}
+impl Parsable for Rfc2822 {}
 impl Parsable for Rfc3339 {}
 impl<T: Deref> Parsable for T where T::Target: Parsable {}
 
@@ -112,6 +113,168 @@ where
 // endregion custom formats
 
 // region: well-known formats
+impl sealed::Sealed for Rfc2822 {
+    fn parse_into<'a>(
+        &self,
+        input: &'a [u8],
+        parsed: &mut Parsed,
+    ) -> Result<&'a [u8], error::Parse> {
+        use crate::error::ParseFromDescription::{InvalidComponent, InvalidLiteral};
+        use crate::parsing::combinator::rfc::rfc2822::{cfws, fws};
+        use crate::parsing::combinator::{
+            ascii_char, exactly_n_digits, first_match, n_to_m_digits, opt, sign,
+        };
+
+        let colon = ascii_char::<b':'>;
+        let comma = ascii_char::<b','>;
+
+        let input = opt(fws)(input).into_inner();
+        let input = first_match(
+            [
+                ("Mon", Weekday::Monday),
+                ("Tue", Weekday::Tuesday),
+                ("Wed", Weekday::Wednesday),
+                ("Thu", Weekday::Thursday),
+                ("Fri", Weekday::Friday),
+                ("Sat", Weekday::Saturday),
+                ("Sun", Weekday::Sunday),
+            ]
+            .iter(),
+            false,
+        )(input)
+        .ok_or(InvalidComponent("weekday"))?
+        .assign_value_to(&mut parsed.weekday);
+        let input = comma(input).ok_or(InvalidLiteral)?.into_inner();
+        let input = cfws(input).ok_or(InvalidLiteral)?.into_inner();
+        let input = n_to_m_digits::<_, 1, 2>(input)
+            .ok_or(InvalidComponent("day"))?
+            .assign_value_to(&mut parsed.day);
+        let input = cfws(input).ok_or(InvalidLiteral)?.into_inner();
+        let input = first_match(
+            [
+                ("Jan", Month::January),
+                ("Feb", Month::February),
+                ("Mar", Month::March),
+                ("Apr", Month::April),
+                ("May", Month::May),
+                ("Jun", Month::June),
+                ("Jul", Month::July),
+                ("Aug", Month::August),
+                ("Sep", Month::September),
+                ("Oct", Month::October),
+                ("Nov", Month::November),
+                ("Dec", Month::December),
+            ]
+            .iter(),
+            false,
+        )(input)
+        .ok_or(InvalidComponent("month"))?
+        .assign_value_to(&mut parsed.month);
+        let input = cfws(input).ok_or(InvalidLiteral)?.into_inner();
+        let input = match exactly_n_digits::<u32, 4>(input) {
+            Some(item) => {
+                let input = item
+                    .flat_map_res(|year| {
+                        if year >= 1900 {
+                            Ok(year)
+                        } else {
+                            Err(InvalidComponent("year"))
+                        }
+                    })?
+                    .map(|year| year as _)
+                    .assign_value_to(&mut parsed.year);
+                let input = fws(input).ok_or(InvalidLiteral)?.into_inner();
+                input
+            }
+            None => {
+                let input = exactly_n_digits::<u32, 2>(input)
+                    .ok_or(InvalidComponent("year"))?
+                    .map(|year| if year < 50 { year + 2000 } else { year + 1900 })
+                    .map(|year| year as _)
+                    .assign_value_to(&mut parsed.year);
+                let input = cfws(input).ok_or(InvalidLiteral)?.into_inner();
+                input
+            }
+        };
+
+        let input = exactly_n_digits::<_, 2>(input)
+            .ok_or(InvalidComponent("hour"))?
+            .assign_value_to(&mut parsed.hour_24);
+        let input = opt(cfws)(input).into_inner();
+        let input = colon(input).ok_or(InvalidLiteral)?.into_inner();
+        let input = opt(cfws)(input).into_inner();
+        let input = exactly_n_digits::<_, 2>(input)
+            .ok_or(InvalidComponent("minute"))?
+            .assign_value_to(&mut parsed.minute);
+
+        let input = if let Some(input) = colon(opt(cfws)(input).into_inner()) {
+            let input = input.into_inner(); // discard the colon
+            let input = opt(cfws)(input).into_inner();
+            let input = exactly_n_digits::<_, 2>(input)
+                .ok_or(InvalidComponent("second"))?
+                .assign_value_to(&mut parsed.second);
+            let input = cfws(input).ok_or(InvalidLiteral)?.into_inner();
+            input
+        } else {
+            cfws(input).ok_or(InvalidLiteral)?.into_inner()
+        };
+
+        // The RFC explicitly allows leap seconds. We don't currently support them, so treat it as
+        // the previous moment.
+        if parsed.second == Some(60) {
+            parsed.second = Some(59);
+            parsed.subsecond = Some(999_999_999);
+        }
+
+        let zone_literal = first_match(
+            [
+                ("UT", 0),
+                ("GMT", 0),
+                ("EST", -5),
+                ("EDT", -4),
+                ("CST", -6),
+                ("CDT", -5),
+                ("MST", -7),
+                ("MDT", -6),
+                ("PST", -8),
+                ("PDT", -7),
+            ]
+            .iter(),
+            false,
+        )(input)
+        .or_else(|| match input {
+            [b'a'..=b'i', rest @ ..]
+            | [b'k'..=b'z', rest @ ..]
+            | [b'A'..=b'I', rest @ ..]
+            | [b'K'..=b'Z', rest @ ..] => Some(ParsedItem(rest, 0)),
+            _ => None,
+        });
+        if let Some(zone_literal) = zone_literal {
+            let input = zone_literal.assign_value_to(&mut parsed.offset_hour);
+            parsed.offset_minute = Some(0);
+            parsed.offset_second = Some(0);
+            return Ok(input);
+        }
+
+        let ParsedItem(input, offset_sign) = sign(input).ok_or(InvalidComponent("offset hour"))?;
+        let input = exactly_n_digits::<u8, 2>(input)
+            .ok_or(InvalidComponent("offset hour"))?
+            .map(|offset_hour| {
+                if offset_sign == b'-' {
+                    -(offset_hour as i8)
+                } else {
+                    offset_hour as _
+                }
+            })
+            .assign_value_to(&mut parsed.offset_hour);
+        let input = exactly_n_digits::<_, 2>(input)
+            .ok_or(InvalidComponent("offset minute"))?
+            .assign_value_to(&mut parsed.offset_minute);
+
+        Ok(input)
+    }
+}
+
 impl sealed::Sealed for Rfc3339 {
     fn parse_into<'a>(
         &self,


Test patch:
diff --git a/tests/integration/derives.rs b/tests/integration/derives.rs
index 5ad5ab16c..b0130c4ee 100644
--- a/tests/integration/derives.rs
+++ b/tests/integration/derives.rs
@@ -47,6 +47,7 @@ fn clone() {
     assert_cloned_eq!(error::DifferentVariant);
     assert_cloned_eq!(error::ParseFromDescription::InvalidComponent("foo"));
     assert_cloned_eq!(Component::OffsetSecond(modifier::OffsetSecond::default()));
+    assert_cloned_eq!(well_known::Rfc2822);
     assert_cloned_eq!(well_known::Rfc3339);
     assert_cloned_eq!(component_range_error());
     assert_cloned_eq!(FormatItem::Literal(b""));
@@ -130,6 +131,7 @@ fn debug() {
         Instant::now();
         error::ParseFromDescription::InvalidComponent("foo");
         error::Format::InvalidComponent("foo");
+        well_known::Rfc2822;
         well_known::Rfc3339;
         component_range_error();
         Error::ConversionRange(ConversionRange);
diff --git a/tests/integration/formatting.rs b/tests/integration/formatting.rs
index 8dafa718a..2537a9f3f 100644
--- a/tests/integration/formatting.rs
+++ b/tests/integration/formatting.rs
@@ -1,10 +1,37 @@
 use std::io::{self, ErrorKind};
 
-use time::format_description::well_known::Rfc3339;
+use time::format_description::well_known::{Rfc2822, Rfc3339};
 use time::format_description::{self, FormatItem};
 use time::macros::{date, datetime, format_description as fd, offset, time};
 use time::{OffsetDateTime, Time};
 
+#[test]
+fn rfc_2822() -> time::Result<()> {
+    assert_eq!(
+        datetime!(2021-01-02 03:04:05 UTC).format(&Rfc2822)?,
+        "Sat, 02 Jan 2021 03:04:05 +0000"
+    );
+    assert_eq!(
+        datetime!(2021-01-02 03:04:05 +06:07).format(&Rfc2822)?,
+        "Sat, 02 Jan 2021 03:04:05 +0607"
+    );
+    assert_eq!(
+        datetime!(2021-01-02 03:04:05 -06:07).format(&Rfc2822)?,
+        "Sat, 02 Jan 2021 03:04:05 -0607"
+    );
+
+    assert!(matches!(
+        datetime!(1885-01-01 01:01:01 UTC).format(&Rfc2822),
+        Err(time::error::Format::InvalidComponent("year"))
+    ));
+    assert!(matches!(
+        datetime!(2000-01-01 00:00:00 +00:00:01).format(&Rfc2822),
+        Err(time::error::Format::InvalidComponent("offset_second"))
+    ));
+
+    Ok(())
+}
+
 #[test]
 fn rfc_3339() -> time::Result<()> {
     assert_eq!(
@@ -350,6 +377,9 @@ fn insufficient_type_information() {
     assert_insufficient_type_information(Time::MIDNIGHT.format(&Rfc3339));
     assert_insufficient_type_information(date!(2021 - 001).format(&Rfc3339));
     assert_insufficient_type_information(datetime!(2021 - 001 0:00).format(&Rfc3339));
+    assert_insufficient_type_information(Time::MIDNIGHT.format(&Rfc2822));
+    assert_insufficient_type_information(date!(2021 - 001).format(&Rfc2822));
+    assert_insufficient_type_information(datetime!(2021 - 001 0:00).format(&Rfc2822));
     assert_insufficient_type_information(
         Time::MIDNIGHT.format(&FormatItem::First(&[FormatItem::Compound(fd!("[year]"))])),
     );
@@ -393,6 +423,26 @@ fn failed_write() -> time::Result<()> {
     assert_err(datetime!(2021-001 0:00 +0:01).format_into(bytes!(20), &Rfc3339));
     assert_err(datetime!(2021-001 0:00 +0:01).format_into(bytes!(22), &Rfc3339));
     assert_err(datetime!(2021-001 0:00 +0:01).format_into(bytes!(23), &Rfc3339));
+
+    assert_err(OffsetDateTime::UNIX_EPOCH.format_into(bytes!(0), &Rfc2822));
+    assert_err(OffsetDateTime::UNIX_EPOCH.format_into(bytes!(4), &Rfc2822));
+    assert_err(OffsetDateTime::UNIX_EPOCH.format_into(bytes!(6), &Rfc2822));
+    assert_err(OffsetDateTime::UNIX_EPOCH.format_into(bytes!(7), &Rfc2822));
+    assert_err(OffsetDateTime::UNIX_EPOCH.format_into(bytes!(8), &Rfc2822));
+    assert_err(OffsetDateTime::UNIX_EPOCH.format_into(bytes!(11), &Rfc2822));
+    assert_err(OffsetDateTime::UNIX_EPOCH.format_into(bytes!(12), &Rfc2822));
+    assert_err(OffsetDateTime::UNIX_EPOCH.format_into(bytes!(16), &Rfc2822));
+    assert_err(OffsetDateTime::UNIX_EPOCH.format_into(bytes!(17), &Rfc2822));
+    assert_err(OffsetDateTime::UNIX_EPOCH.format_into(bytes!(19), &Rfc2822));
+    assert_err(OffsetDateTime::UNIX_EPOCH.format_into(bytes!(20), &Rfc2822));
+    assert_err(OffsetDateTime::UNIX_EPOCH.format_into(bytes!(22), &Rfc2822));
+    assert_err(OffsetDateTime::UNIX_EPOCH.format_into(bytes!(23), &Rfc2822));
+    assert_err(OffsetDateTime::UNIX_EPOCH.format_into(bytes!(25), &Rfc2822));
+    assert_err(OffsetDateTime::UNIX_EPOCH.format_into(bytes!(26), &Rfc2822));
+    assert_err(OffsetDateTime::UNIX_EPOCH.format_into(bytes!(27), &Rfc2822));
+    assert_err(OffsetDateTime::UNIX_EPOCH.format_into(bytes!(29), &Rfc2822));
+    assert_err(OffsetDateTime::UNIX_EPOCH.format_into(bytes!(30), &Rfc2822));
+
     assert_err(Time::MIDNIGHT.format_into(bytes!(0), fd!("[hour padding:space]")));
     assert_err(Time::MIDNIGHT.format_into(bytes!(1), fd!("[hour padding:space]")));
     assert_err(offset!(+1).format_into(bytes!(0), fd!("[offset_hour sign:mandatory]")));
diff --git a/tests/integration/parsing.rs b/tests/integration/parsing.rs
index 8d3e51471..08fb1859c 100644
--- a/tests/integration/parsing.rs
+++ b/tests/integration/parsing.rs
@@ -1,7 +1,7 @@
 use core::convert::{TryFrom, TryInto};
 use std::num::NonZeroU8;
 
-use time::format_description::well_known::Rfc3339;
+use time::format_description::well_known::{Rfc2822, Rfc3339};
 use time::format_description::{modifier, Component, FormatItem};
 use time::macros::{date, datetime, time};
 use time::parsing::Parsed;
@@ -10,6 +10,211 @@ use time::{
     UtcOffset, Weekday,
 };
 
+macro_rules! invalid_literal {
+    () => {
+        Err(error::Parse::ParseFromDescription(
+            error::ParseFromDescription::InvalidLiteral { .. },
+        ))
+    };
+}
+macro_rules! invalid_component {
+    ($name:literal) => {
+        Err(error::Parse::ParseFromDescription(
+            error::ParseFromDescription::InvalidComponent($name),
+        ))
+    };
+}
+
+#[test]
+fn rfc_2822() -> time::Result<()> {
+    assert_eq!(
+        OffsetDateTime::parse("Sat, 02 Jan 2021 03:04:05 GMT", &Rfc2822)?,
+        datetime!(2021-01-02 03:04:05 UTC),
+    );
+    assert_eq!(
+        OffsetDateTime::parse("Sat, 02 Jan 2021 03:04:05 UT", &Rfc2822)?,
+        datetime!(2021-01-02 03:04:05 UTC),
+    );
+    assert_eq!(
+        OffsetDateTime::parse("Sat, 02 Jan 2021 03:04:05 +0000", &Rfc2822)?,
+        datetime!(2021-01-02 03:04:05 UTC),
+    );
+    assert_eq!(
+        OffsetDateTime::parse("Sat, 02 Jan 2021 03:04:05 +0607", &Rfc2822)?,
+        datetime!(2021-01-02 03:04:05 +06:07),
+    );
+    assert_eq!(
+        OffsetDateTime::parse("Sat, 02 Jan 2021 03:04:05 -0607", &Rfc2822)?,
+        datetime!(2021-01-02 03:04:05 -06:07),
+    );
+    assert_eq!(
+        OffsetDateTime::parse("Fri, 31 Dec 2021 23:59:60 Z", &Rfc2822)?,
+        datetime!(2021-12-31 23:59:59.999_999_999 UTC),
+    );
+    assert_eq!(
+        OffsetDateTime::parse("Fri, 31 Dec 2021 23:59:60 z", &Rfc2822)?,
+        datetime!(2021-12-31 23:59:59.999_999_999 UTC),
+    );
+    assert_eq!(
+        OffsetDateTime::parse("Fri, 31 Dec 2021 23:59:60 a", &Rfc2822)?,
+        datetime!(2021-12-31 23:59:59.999_999_999 UTC),
+    );
+    assert_eq!(
+        OffsetDateTime::parse("Fri, 31 Dec 2021 23:59:60 A", &Rfc2822)?,
+        datetime!(2021-12-31 23:59:59.999_999_999 UTC),
+    );
+    assert_eq!(
+        OffsetDateTime::parse("Fri, 31 Dec 2021 17:52:60 -0607", &Rfc2822)?,
+        datetime!(2021-12-31 17:52:59.999_999_999 -06:07),
+    );
+    assert_eq!(
+        OffsetDateTime::parse("Sat, 01 Jan 2022 06:06:60 +0607", &Rfc2822)?,
+        datetime!(2022-01-01 06:06:59.999_999_999 +06:07),
+    );
+
+    assert_eq!(
+        Date::parse("Sat, 02 Jan 2021 03:04:05 GMT", &Rfc2822)?,
+        date!(2021 - 01 - 02)
+    );
+    assert_eq!(
+        Date::parse("Sat, 02 Jan 2021 03:04:05 +0607", &Rfc2822)?,
+        date!(2021 - 01 - 02)
+    );
+    assert_eq!(
+        Date::parse("Sat, 02 Jan 2021 03:04:05 -0607", &Rfc2822)?,
+        date!(2021 - 01 - 02)
+    );
+    assert_eq!(
+        Date::parse("Sat, 02 Jan 21 03:04:05 -0607", &Rfc2822)?,
+        date!(2021 - 01 - 02)
+    );
+    assert_eq!(
+        Date::parse("Sat, 02 Jan 71 03:04:05 -0607", &Rfc2822)?,
+        date!(1971 - 01 - 02)
+    );
+
+    assert_eq!(
+        OffsetDateTime::parse("Sat,(\\\u{a})02 Jan 2021 03:04:05 GMT", &Rfc2822)?,
+        datetime!(2021 - 01 - 02 03:04:05 UTC),
+    );
+    #[rustfmt::skip]
+    assert_eq!(
+        Time::parse(
+            "  \t Sat,\r\n \
+            (\tfoo012FOO!)\
+            (\u{1}\u{b}\u{e}\u{7f})\
+            (\\\u{1}\\\u{9}\\\u{28}\\\u{29}\\\\u{5c}\\\u{7f})\
+            (\\\n\\\u{b})\
+            02 \r\n  \r\n Jan 2021 03:04:05 GMT",
+            &Rfc2822
+        )?,
+        time!(03:04:05)
+    );
+
+    Ok(())
+}
+
+#[test]
+fn rfc_2822_err() {
+    // In the first test, the "weekday" component is invalid, we're actually testing the whitespace
+    // parser. The error is because the parser attempts and fails to parse the whitespace, but it's
+    // optional so it backtracks and attempts to parse the weekday (while still having leading
+    // whitespace), thus failing.
+    assert!(matches!(
+        OffsetDateTime::parse(" \r\nM", &Rfc2822),
+        invalid_component!("weekday")
+    ));
+    assert!(matches!(
+        OffsetDateTime::parse("Mon,(\u{0}", &Rfc2822),
+        invalid_literal!()
+    ));
+    assert!(matches!(
+        OffsetDateTime::parse("Mon,(", &Rfc2822),
+        invalid_literal!()
+    ));
+    assert!(matches!(
+        OffsetDateTime::parse("Mon,(\\\u{ff}", &Rfc2822),
+        invalid_literal!()
+    ));
+    assert!(matches!(
+        OffsetDateTime::parse("Mon,((\\\u{0})(\\\u{b} )(\\\u{d})", &Rfc2822),
+        invalid_literal!()
+    ));
+
+    assert!(matches!(
+        OffsetDateTime::parse("Mon:", &Rfc2822),
+        invalid_literal!()
+    ));
+    assert!(matches!(
+        OffsetDateTime::parse("Mon, o2", &Rfc2822),
+        invalid_component!("day")
+    ));
+    assert!(matches!(
+        OffsetDateTime::parse("Mon, 02_", &Rfc2822),
+        invalid_literal!()
+    ));
+    assert!(matches!(
+        OffsetDateTime::parse("Mon, 02 jxn", &Rfc2822),
+        invalid_component!("month")
+    ));
+    assert!(matches!(
+        OffsetDateTime::parse("Mon, 02 Jan_", &Rfc2822),
+        invalid_literal!()
+    ));
+    assert!(matches!(
+        OffsetDateTime::parse("Mon, 02 Jan abcd", &Rfc2822),
+        invalid_component!("year")
+    ));
+    assert!(matches!(
+        OffsetDateTime::parse("Mon, 02 Jan 1899", &Rfc2822),
+        invalid_component!("year")
+    ));
+    assert!(matches!(
+        OffsetDateTime::parse("Mon, 02 Jan 2021_", &Rfc2822),
+        invalid_literal!()
+    ));
+    assert!(matches!(
+        OffsetDateTime::parse("Mon, 02 Jan 21_", &Rfc2822),
+        invalid_literal!()
+    ));
+    assert!(matches!(
+        OffsetDateTime::parse("Mon, 02 Jan 2021 ab", &Rfc2822),
+        invalid_component!("hour")
+    ));
+    assert!(matches!(
+        OffsetDateTime::parse("Mon, 02 Jan 2021 03_", &Rfc2822),
+        invalid_literal!()
+    ));
+    assert!(matches!(
+        OffsetDateTime::parse("Mon, 02 Jan 2021 03:ab", &Rfc2822),
+        invalid_component!("minute")
+    ));
+    assert!(matches!(
+        OffsetDateTime::parse("Mon, 02 Jan 2021 03:04_", &Rfc2822),
+        invalid_literal!()
+    ));
+    assert!(matches!(
+        OffsetDateTime::parse("Mon, 02 Jan 2021 03:04:ab", &Rfc2822),
+        invalid_component!("second")
+    ));
+    assert!(matches!(
+        OffsetDateTime::parse("Mon, 02 Jan 2021 03:04:05_", &Rfc2822),
+        invalid_literal!()
+    ));
+    assert!(matches!(
+        dbg!(OffsetDateTime::parse("Mon, 02 Jan 2021 03:04 6", &Rfc2822)),
+        invalid_component!("offset hour")
+    ));
+    assert!(matches!(
+        OffsetDateTime::parse("Mon, 02 Jan 2021 03:04:05 -6", &Rfc2822),
+        invalid_component!("offset hour")
+    ));
+    assert!(matches!(
+        OffsetDateTime::parse("Mon, 02 Jan 2021 03:04:05 -060", &Rfc2822),
+        invalid_component!("offset minute")
+    ));
+}
+
 #[test]
 fn rfc_3339() -> time::Result<()> {
     assert_eq!(
@@ -87,21 +292,6 @@ fn rfc_3339() -> time::Result<()> {
 
 #[test]
 fn rfc_3339_err() {
-    macro_rules! invalid_literal {
-        () => {
-            Err(error::Parse::ParseFromDescription(
-                error::ParseFromDescription::InvalidLiteral { .. },
-            ))
-        };
-    }
-    macro_rules! invalid_component {
-        ($name:literal) => {
-            Err(error::Parse::ParseFromDescription(
-                error::ParseFromDescription::InvalidComponent($name),
-            ))
-        };
-    }
-
     assert!(matches!(
         PrimitiveDateTime::parse("x", &Rfc3339),
         invalid_component!("year")