Coverage Report

Created: 2026-08-05 07:37

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