Coverage Report

Created: 2025-11-16 06:22

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