Coverage Report

Created: 2025-11-16 07:09

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/jiff-0.2.16/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 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
14.3k
    pub fn parse_zoned<I: AsRef<[u8]>>(
310
14.3k
        &self,
311
14.3k
        input: I,
312
14.3k
    ) -> Result<Zoned, Error> {
313
14.3k
        let input = input.as_ref();
314
14.3k
        let zdt = self
315
14.3k
            .parse_zoned_internal(input)
316
14.3k
            .context(
317
                "failed to parse RFC 2822 datetime into Jiff zoned datetime",
318
13.4k
            )?
319
941
            .into_full()?;
320
509
        Ok(zdt)
321
14.3k
    }
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_zoned::<&str>
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_zoned::<_>
<jiff::fmt::rfc2822::DateTimeParser>::parse_zoned::<&str>
Line
Count
Source
309
9.36k
    pub fn parse_zoned<I: AsRef<[u8]>>(
310
9.36k
        &self,
311
9.36k
        input: I,
312
9.36k
    ) -> Result<Zoned, Error> {
313
9.36k
        let input = input.as_ref();
314
9.36k
        let zdt = self
315
9.36k
            .parse_zoned_internal(input)
316
9.36k
            .context(
317
                "failed to parse RFC 2822 datetime into Jiff zoned datetime",
318
8.89k
            )?
319
478
            .into_full()?;
320
292
        Ok(zdt)
321
9.36k
    }
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_zoned::<&str>
<jiff::fmt::rfc2822::DateTimeParser>::parse_zoned::<&str>
Line
Count
Source
309
5.02k
    pub fn parse_zoned<I: AsRef<[u8]>>(
310
5.02k
        &self,
311
5.02k
        input: I,
312
5.02k
    ) -> Result<Zoned, Error> {
313
5.02k
        let input = input.as_ref();
314
5.02k
        let zdt = self
315
5.02k
            .parse_zoned_internal(input)
316
5.02k
            .context(
317
                "failed to parse RFC 2822 datetime into Jiff zoned datetime",
318
4.56k
            )?
319
463
            .into_full()?;
320
217
        Ok(zdt)
321
5.02k
    }
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
14.3k
    fn parse_zoned_internal<'i>(
365
14.3k
        &self,
366
14.3k
        input: &'i [u8],
367
14.3k
    ) -> Result<Parsed<'i, Zoned>, Error> {
368
951
        let Parsed { value: (dt, offset), input } =
369
14.3k
            self.parse_datetime_offset(input)?;
370
951
        let ts = offset
371
951
            .to_timestamp(dt)
372
951
            .context("RFC 2822 datetime out of Jiff's range")?;
373
941
        let zdt = ts.to_zoned(TimeZone::fixed(offset));
374
941
        Ok(Parsed { value: zdt, input })
375
14.3k
    }
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
14.3k
    fn parse_datetime_offset<'i>(
400
14.3k
        &self,
401
14.3k
        input: &'i [u8],
402
14.3k
    ) -> Result<Parsed<'i, (DateTime, Offset)>, Error> {
403
14.3k
        let input = input.as_ref();
404
14.3k
        let Parsed { value: dt, input } = self.parse_datetime(input)?;
405
1.57k
        let Parsed { value: offset, input } = self.parse_offset(input)?;
406
1.07k
        let Parsed { input, .. } = self.skip_whitespace(input);
407
1.07k
        let input = if input.is_empty() {
408
405
            input
409
        } else {
410
665
            self.skip_comment(input)?.input
411
        };
412
951
        Ok(Parsed { value: (dt, offset), input })
413
14.3k
    }
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
14.3k
    fn parse_datetime<'i>(
424
14.3k
        &self,
425
14.3k
        input: &'i [u8],
426
14.3k
    ) -> Result<Parsed<'i, DateTime>, Error> {
427
14.3k
        if input.is_empty() {
428
55
            return Err(err!(
429
55
                "expected RFC 2822 datetime, but got empty string"
430
55
            ));
431
14.3k
        }
432
14.3k
        let Parsed { input, .. } = self.skip_whitespace(input);
433
14.3k
        if input.is_empty() {
434
201
            return Err(err!(
435
201
                "expected RFC 2822 datetime, but got empty string after \
436
201
                 trimming whitespace",
437
201
            ));
438
14.1k
        }
439
14.1k
        let Parsed { value: wd, input } = self.parse_weekday(input)?;
440
9.20k
        let Parsed { value: day, input } = self.parse_day(input)?;
441
4.00k
        let Parsed { value: month, input } = self.parse_month(input)?;
442
2.83k
        let Parsed { value: year, input } = self.parse_year(input)?;
443
444
2.38k
        let Parsed { value: hour, input } = self.parse_hour(input)?;
445
2.08k
        let Parsed { input, .. } = self.skip_whitespace(input);
446
2.08k
        let Parsed { input, .. } = self.parse_time_separator(input)?;
447
1.89k
        let Parsed { input, .. } = self.skip_whitespace(input);
448
1.89k
        let Parsed { value: minute, input } = self.parse_minute(input)?;
449
450
1.75k
        let Parsed { value: whitespace_after_minute, input } =
451
1.75k
            self.skip_whitespace(input);
452
1.75k
        let (second, input) = if !input.starts_with(b":") {
453
1.34k
            if !whitespace_after_minute {
454
9
                return Err(err!(
455
9
                    "expected whitespace after parsing time: \
456
9
                     expected at least one whitespace character \
457
9
                     (space or tab), but found none",
458
9
                ));
459
1.34k
            }
460
1.34k
            (t::Second::N::<0>(), input)
461
        } else {
462
401
            let Parsed { input, .. } = self.parse_time_separator(input)?;
463
401
            let Parsed { input, .. } = self.skip_whitespace(input);
464
401
            let Parsed { value: second, input } = self.parse_second(input)?;
465
238
            let Parsed { input, .. } =
466
255
                self.parse_whitespace(input).with_context(|| {
467
17
                    err!("expected whitespace after parsing time")
468
17
                })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_datetime::{closure#0}
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_datetime::{closure#0}
<jiff::fmt::rfc2822::DateTimeParser>::parse_datetime::{closure#0}
Line
Count
Source
466
7
                self.parse_whitespace(input).with_context(|| {
467
7
                    err!("expected whitespace after parsing time")
468
7
                })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_datetime::{closure#0}
<jiff::fmt::rfc2822::DateTimeParser>::parse_datetime::{closure#0}
Line
Count
Source
466
10
                self.parse_whitespace(input).with_context(|| {
467
10
                    err!("expected whitespace after parsing time")
468
10
                })?;
469
238
            (second, input)
470
        };
471
472
1.57k
        let date =
473
1.57k
            Date::new_ranged(year, month, day).context("invalid date")?;
474
1.57k
        let time = Time::new_ranged(
475
1.57k
            hour,
476
1.57k
            minute,
477
1.57k
            second,
478
1.57k
            t::SubsecNanosecond::N::<0>(),
479
        );
480
1.57k
        let dt = DateTime::from_parts(date, time);
481
1.57k
        if let Some(wd) = wd {
482
11
            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
11
            }
491
1.56k
        }
492
1.57k
        Ok(Parsed { value: dt, input })
493
14.3k
    }
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
14.1k
    fn parse_weekday<'i>(
510
14.1k
        &self,
511
14.1k
        input: &'i [u8],
512
14.1k
    ) -> 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
14.1k
        if matches!(input[0], b'0'..=b'9') {
518
8.97k
            return Ok(Parsed { value: None, input });
519
5.16k
        }
520
5.16k
        if input.len() < 4 {
521
583
            return Err(err!(
522
583
                "expected day at beginning of RFC 2822 datetime \
523
583
                 since first non-whitespace byte, {first:?}, \
524
583
                 is not a digit, but given string is too short \
525
583
                 (length is {length})",
526
583
                first = escape::Byte(input[0]),
527
583
                length = input.len(),
528
583
            ));
529
4.57k
        }
530
4.57k
        let b1 = input[0];
531
4.57k
        let b2 = input[1];
532
4.57k
        let b3 = input[2];
533
4.57k
        let wd = match &[
534
4.57k
            b1.to_ascii_lowercase(),
535
4.57k
            b2.to_ascii_lowercase(),
536
4.57k
            b3.to_ascii_lowercase(),
537
4.57k
        ] {
538
53
            b"sun" => Weekday::Sunday,
539
451
            b"mon" => Weekday::Monday,
540
134
            b"tue" => Weekday::Tuesday,
541
58
            b"wed" => Weekday::Wednesday,
542
50
            b"thu" => Weekday::Thursday,
543
110
            b"fri" => Weekday::Friday,
544
122
            b"sat" => Weekday::Saturday,
545
            _ => {
546
3.59k
                return Err(err!(
547
3.59k
                    "expected day at beginning of RFC 2822 datetime \
548
3.59k
                     since first non-whitespace byte, {first:?}, \
549
3.59k
                     is not a digit, but did not recognize {got:?} \
550
3.59k
                     as a valid weekday abbreviation",
551
3.59k
                    first = escape::Byte(input[0]),
552
3.59k
                    got = escape::Bytes(&input[..3]),
553
3.59k
                ));
554
            }
555
        };
556
978
        let Parsed { input, .. } = self.skip_whitespace(&input[3..]);
557
978
        let Some(should_be_comma) = input.get(0).copied() else {
558
157
            return Err(err!(
559
157
                "expected comma after parsed weekday `{weekday}` in \
560
157
                 RFC 2822 datetime, but found end of string instead",
561
157
                weekday = escape::Bytes(&[b1, b2, b3]),
562
157
            ));
563
        };
564
821
        if should_be_comma != b',' {
565
595
            return Err(err!(
566
595
                "expected comma after parsed weekday `{weekday}` in \
567
595
                 RFC 2822 datetime, but found `{got:?}` instead",
568
595
                weekday = escape::Bytes(&[b1, b2, b3]),
569
595
                got = escape::Byte(should_be_comma),
570
595
            ));
571
226
        }
572
226
        let Parsed { input, .. } = self.skip_whitespace(&input[1..]);
573
226
        Ok(Parsed { value: Some(wd), input })
574
14.1k
    }
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
9.20k
    fn parse_day<'i>(
585
9.20k
        &self,
586
9.20k
        input: &'i [u8],
587
9.20k
    ) -> Result<Parsed<'i, t::Day>, Error> {
588
9.20k
        if input.is_empty() {
589
139
            return Err(err!("expected day, but found end of input"));
590
9.06k
        }
591
9.06k
        let mut digits = 1;
592
9.06k
        if input.len() >= 2 && matches!(input[1], b'0'..=b'9') {
593
2.68k
            digits = 2;
594
6.38k
        }
595
9.06k
        let (day, input) = input.split_at(digits);
596
9.06k
        let day = parse::i64(day).with_context(|| {
597
68
            err!("failed to parse {day:?} as day", day = escape::Bytes(day))
598
68
        })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_day::{closure#0}
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_day::{closure#0}
<jiff::fmt::rfc2822::DateTimeParser>::parse_day::{closure#0}
Line
Count
Source
596
47
        let day = parse::i64(day).with_context(|| {
597
47
            err!("failed to parse {day:?} as day", day = escape::Bytes(day))
598
47
        })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_day::{closure#0}
<jiff::fmt::rfc2822::DateTimeParser>::parse_day::{closure#0}
Line
Count
Source
596
21
        let day = parse::i64(day).with_context(|| {
597
21
            err!("failed to parse {day:?} as day", day = escape::Bytes(day))
598
21
        })?;
599
8.99k
        let day = t::Day::try_new("day", day).context("day is not valid")?;
600
4.00k
        let Parsed { input, .. } =
601
6.61k
            self.parse_whitespace(input).with_context(|| {
602
2.60k
                err!("expected whitespace after parsing day {day}")
603
2.60k
            })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_day::{closure#1}
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_day::{closure#1}
<jiff::fmt::rfc2822::DateTimeParser>::parse_day::{closure#1}
Line
Count
Source
601
1.66k
            self.parse_whitespace(input).with_context(|| {
602
1.66k
                err!("expected whitespace after parsing day {day}")
603
1.66k
            })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_day::{closure#1}
<jiff::fmt::rfc2822::DateTimeParser>::parse_day::{closure#1}
Line
Count
Source
601
949
            self.parse_whitespace(input).with_context(|| {
602
949
                err!("expected whitespace after parsing day {day}")
603
949
            })?;
604
4.00k
        Ok(Parsed { value: day, input })
605
9.20k
    }
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
4.00k
    fn parse_month<'i>(
616
4.00k
        &self,
617
4.00k
        input: &'i [u8],
618
4.00k
    ) -> Result<Parsed<'i, t::Month>, Error> {
619
4.00k
        if input.is_empty() {
620
166
            return Err(err!(
621
166
                "expected abbreviated month name, but found end of input"
622
166
            ));
623
3.84k
        }
624
3.84k
        if input.len() < 3 {
625
18
            return Err(err!(
626
18
                "expected abbreviated month name, but remaining input \
627
18
                 is too short (remaining bytes is {length})",
628
18
                length = input.len(),
629
18
            ));
630
3.82k
        }
631
3.82k
        let b1 = input[0].to_ascii_lowercase();
632
3.82k
        let b2 = input[1].to_ascii_lowercase();
633
3.82k
        let b3 = input[2].to_ascii_lowercase();
634
3.82k
        let month = match &[b1, b2, b3] {
635
360
            b"jan" => 1,
636
243
            b"feb" => 2,
637
447
            b"mar" => 3,
638
55
            b"apr" => 4,
639
643
            b"may" => 5,
640
226
            b"jun" => 6,
641
29
            b"jul" => 7,
642
133
            b"aug" => 8,
643
94
            b"sep" => 9,
644
12
            b"oct" => 10,
645
340
            b"nov" => 11,
646
329
            b"dec" => 12,
647
            _ => {
648
912
                return Err(err!(
649
912
                    "expected abbreviated month name, \
650
912
                     but did not recognize {got:?} \
651
912
                     as a valid month",
652
912
                    got = escape::Bytes(&input[..3]),
653
912
                ));
654
            }
655
        };
656
        // OK because we just assigned a numeric value ourselves
657
        // above, and all values are valid months.
658
2.91k
        let month = t::Month::new(month).unwrap();
659
2.83k
        let Parsed { input, .. } =
660
2.91k
            self.parse_whitespace(&input[3..]).with_context(|| {
661
75
                err!("expected whitespace after parsing month name")
662
75
            })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_month::{closure#0}
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_month::{closure#0}
<jiff::fmt::rfc2822::DateTimeParser>::parse_month::{closure#0}
Line
Count
Source
660
57
            self.parse_whitespace(&input[3..]).with_context(|| {
661
57
                err!("expected whitespace after parsing month name")
662
57
            })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_month::{closure#0}
<jiff::fmt::rfc2822::DateTimeParser>::parse_month::{closure#0}
Line
Count
Source
660
18
            self.parse_whitespace(&input[3..]).with_context(|| {
661
18
                err!("expected whitespace after parsing month name")
662
18
            })?;
663
2.83k
        Ok(Parsed { value: month, input })
664
4.00k
    }
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
2.83k
    fn parse_year<'i>(
685
2.83k
        &self,
686
2.83k
        input: &'i [u8],
687
2.83k
    ) -> Result<Parsed<'i, t::Year>, Error> {
688
2.83k
        let mut digits = 0;
689
8.42k
        while digits <= 3
690
8.26k
            && !input[digits..].is_empty()
691
8.03k
            && matches!(input[digits], b'0'..=b'9')
692
5.58k
        {
693
5.58k
            digits += 1;
694
5.58k
        }
695
2.83k
        if digits <= 1 {
696
278
            return Err(err!(
697
278
                "expected at least two ASCII digits for parsing \
698
278
                 a year, but only found {digits}",
699
278
            ));
700
2.55k
        }
701
2.55k
        let (year, input) = input.split_at(digits);
702
2.55k
        let year = parse::i64(year).with_context(|| {
703
0
            err!(
704
                "failed to parse {year:?} as year \
705
                 (a two, three or four digit integer)",
706
0
                year = escape::Bytes(year),
707
            )
708
0
        })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_year::{closure#0}
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_year::{closure#0}
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_year::{closure#0}
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_year::{closure#0}
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_year::{closure#0}
709
2.55k
        let year = match digits {
710
2.28k
            2 if year <= 49 => year + 2000,
711
1.00k
            2 | 3 => year + 1900,
712
163
            4 => year,
713
0
            _ => unreachable!("digits={digits} must be 2, 3 or 4"),
714
        };
715
2.55k
        let year =
716
2.55k
            t::Year::try_new("year", year).context("year is not valid")?;
717
2.55k
        let Parsed { input, .. } = self
718
2.55k
            .parse_whitespace(input)
719
2.55k
            .with_context(|| err!("expected whitespace after parsing year"))?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_year::{closure#1}
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_year::{closure#1}
<jiff::fmt::rfc2822::DateTimeParser>::parse_year::{closure#1}
Line
Count
Source
719
117
            .with_context(|| err!("expected whitespace after parsing year"))?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_year::{closure#1}
<jiff::fmt::rfc2822::DateTimeParser>::parse_year::{closure#1}
Line
Count
Source
719
60
            .with_context(|| err!("expected whitespace after parsing year"))?;
720
2.38k
        Ok(Parsed { value: year, input })
721
2.83k
    }
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
2.38k
    fn parse_hour<'i>(
730
2.38k
        &self,
731
2.38k
        input: &'i [u8],
732
2.38k
    ) -> Result<Parsed<'i, t::Hour>, Error> {
733
2.38k
        let (hour, input) = parse::split(input, 2).ok_or_else(|| {
734
179
            err!("expected two digit hour, but found end of input")
735
179
        })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_hour::{closure#0}
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_hour::{closure#0}
<jiff::fmt::rfc2822::DateTimeParser>::parse_hour::{closure#0}
Line
Count
Source
733
126
        let (hour, input) = parse::split(input, 2).ok_or_else(|| {
734
126
            err!("expected two digit hour, but found end of input")
735
126
        })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_hour::{closure#0}
<jiff::fmt::rfc2822::DateTimeParser>::parse_hour::{closure#0}
Line
Count
Source
733
53
        let (hour, input) = parse::split(input, 2).ok_or_else(|| {
734
53
            err!("expected two digit hour, but found end of input")
735
53
        })?;
736
2.20k
        let hour = parse::i64(hour).with_context(|| {
737
97
            err!(
738
                "failed to parse {hour:?} as hour (a two digit integer)",
739
97
                hour = escape::Bytes(hour),
740
            )
741
97
        })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_hour::{closure#1}
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_hour::{closure#1}
<jiff::fmt::rfc2822::DateTimeParser>::parse_hour::{closure#1}
Line
Count
Source
736
75
        let hour = parse::i64(hour).with_context(|| {
737
75
            err!(
738
                "failed to parse {hour:?} as hour (a two digit integer)",
739
75
                hour = escape::Bytes(hour),
740
            )
741
75
        })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_hour::{closure#1}
<jiff::fmt::rfc2822::DateTimeParser>::parse_hour::{closure#1}
Line
Count
Source
736
22
        let hour = parse::i64(hour).with_context(|| {
737
22
            err!(
738
                "failed to parse {hour:?} as hour (a two digit integer)",
739
22
                hour = escape::Bytes(hour),
740
            )
741
22
        })?;
742
2.08k
        let hour =
743
2.10k
            t::Hour::try_new("hour", hour).context("hour is not valid")?;
744
2.08k
        Ok(Parsed { value: hour, input })
745
2.38k
    }
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
1.89k
    fn parse_minute<'i>(
751
1.89k
        &self,
752
1.89k
        input: &'i [u8],
753
1.89k
    ) -> Result<Parsed<'i, t::Minute>, Error> {
754
1.89k
        let (minute, input) = parse::split(input, 2).ok_or_else(|| {
755
102
            err!("expected two digit minute, but found end of input")
756
102
        })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_minute::{closure#0}
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_minute::{closure#0}
<jiff::fmt::rfc2822::DateTimeParser>::parse_minute::{closure#0}
Line
Count
Source
754
51
        let (minute, input) = parse::split(input, 2).ok_or_else(|| {
755
51
            err!("expected two digit minute, but found end of input")
756
51
        })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_minute::{closure#0}
<jiff::fmt::rfc2822::DateTimeParser>::parse_minute::{closure#0}
Line
Count
Source
754
51
        let (minute, input) = parse::split(input, 2).ok_or_else(|| {
755
51
            err!("expected two digit minute, but found end of input")
756
51
        })?;
757
1.79k
        let minute = parse::i64(minute).with_context(|| {
758
36
            err!(
759
                "failed to parse {minute:?} as minute (a two digit integer)",
760
36
                minute = escape::Bytes(minute),
761
            )
762
36
        })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_minute::{closure#1}
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_minute::{closure#1}
<jiff::fmt::rfc2822::DateTimeParser>::parse_minute::{closure#1}
Line
Count
Source
757
16
        let minute = parse::i64(minute).with_context(|| {
758
16
            err!(
759
                "failed to parse {minute:?} as minute (a two digit integer)",
760
16
                minute = escape::Bytes(minute),
761
            )
762
16
        })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_minute::{closure#1}
<jiff::fmt::rfc2822::DateTimeParser>::parse_minute::{closure#1}
Line
Count
Source
757
20
        let minute = parse::i64(minute).with_context(|| {
758
20
            err!(
759
                "failed to parse {minute:?} as minute (a two digit integer)",
760
20
                minute = escape::Bytes(minute),
761
            )
762
20
        })?;
763
1.75k
        let minute = t::Minute::try_new("minute", minute)
764
1.75k
            .context("minute is not valid")?;
765
1.75k
        Ok(Parsed { value: minute, input })
766
1.89k
    }
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
401
    fn parse_second<'i>(
772
401
        &self,
773
401
        input: &'i [u8],
774
401
    ) -> Result<Parsed<'i, t::Second>, Error> {
775
401
        let (second, input) = parse::split(input, 2).ok_or_else(|| {
776
107
            err!("expected two digit second, but found end of input")
777
107
        })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_second::{closure#0}
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_second::{closure#0}
<jiff::fmt::rfc2822::DateTimeParser>::parse_second::{closure#0}
Line
Count
Source
775
58
        let (second, input) = parse::split(input, 2).ok_or_else(|| {
776
58
            err!("expected two digit second, but found end of input")
777
58
        })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_second::{closure#0}
<jiff::fmt::rfc2822::DateTimeParser>::parse_second::{closure#0}
Line
Count
Source
775
49
        let (second, input) = parse::split(input, 2).ok_or_else(|| {
776
49
            err!("expected two digit second, but found end of input")
777
49
        })?;
778
294
        let mut second = parse::i64(second).with_context(|| {
779
30
            err!(
780
                "failed to parse {second:?} as second (a two digit integer)",
781
30
                second = escape::Bytes(second),
782
            )
783
30
        })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_second::{closure#1}
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_second::{closure#1}
<jiff::fmt::rfc2822::DateTimeParser>::parse_second::{closure#1}
Line
Count
Source
778
18
        let mut second = parse::i64(second).with_context(|| {
779
18
            err!(
780
                "failed to parse {second:?} as second (a two digit integer)",
781
18
                second = escape::Bytes(second),
782
            )
783
18
        })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_second::{closure#1}
<jiff::fmt::rfc2822::DateTimeParser>::parse_second::{closure#1}
Line
Count
Source
778
12
        let mut second = parse::i64(second).with_context(|| {
779
12
            err!(
780
                "failed to parse {second:?} as second (a two digit integer)",
781
12
                second = escape::Bytes(second),
782
            )
783
12
        })?;
784
264
        if second == 60 {
785
1
            second = 59;
786
263
        }
787
264
        let second = t::Second::try_new("second", second)
788
264
            .context("second is not valid")?;
789
255
        Ok(Parsed { value: second, input })
790
401
    }
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
1.57k
    fn parse_offset<'i>(
798
1.57k
        &self,
799
1.57k
        input: &'i [u8],
800
1.57k
    ) -> Result<Parsed<'i, Offset>, Error> {
801
        type ParsedOffsetHours = ri8<0, { t::SpanZoneOffsetHours::MAX }>;
802
        type ParsedOffsetMinutes = ri8<0, { t::SpanZoneOffsetMinutes::MAX }>;
803
804
1.57k
        let sign = input.get(0).copied().ok_or_else(|| {
805
210
            err!(
806
                "expected sign for time zone offset, \
807
                 (or a legacy time zone name abbreviation), \
808
                 but found end of input",
809
            )
810
210
        })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_offset::{closure#0}
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_offset::{closure#0}
<jiff::fmt::rfc2822::DateTimeParser>::parse_offset::{closure#0}
Line
Count
Source
804
109
        let sign = input.get(0).copied().ok_or_else(|| {
805
109
            err!(
806
                "expected sign for time zone offset, \
807
                 (or a legacy time zone name abbreviation), \
808
                 but found end of input",
809
            )
810
109
        })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_offset::{closure#0}
<jiff::fmt::rfc2822::DateTimeParser>::parse_offset::{closure#0}
Line
Count
Source
804
101
        let sign = input.get(0).copied().ok_or_else(|| {
805
101
            err!(
806
                "expected sign for time zone offset, \
807
                 (or a legacy time zone name abbreviation), \
808
                 but found end of input",
809
            )
810
101
        })?;
811
1.36k
        let sign = if sign == b'+' {
812
100
            t::Sign::N::<1>()
813
1.26k
        } else if sign == b'-' {
814
127
            t::Sign::N::<-1>()
815
        } else {
816
1.13k
            return self.parse_offset_obsolete(input);
817
        };
818
227
        let input = &input[1..];
819
227
        let (hhmm, input) = parse::split(input, 4).ok_or_else(|| {
820
5
            err!(
821
                "expected at least 4 digits for time zone offset \
822
                 after sign, but found only {len} bytes remaining",
823
5
                len = input.len(),
824
            )
825
5
        })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_offset::{closure#1}
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_offset::{closure#1}
<jiff::fmt::rfc2822::DateTimeParser>::parse_offset::{closure#1}
Line
Count
Source
819
3
        let (hhmm, input) = parse::split(input, 4).ok_or_else(|| {
820
3
            err!(
821
                "expected at least 4 digits for time zone offset \
822
                 after sign, but found only {len} bytes remaining",
823
3
                len = input.len(),
824
            )
825
3
        })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_offset::{closure#1}
<jiff::fmt::rfc2822::DateTimeParser>::parse_offset::{closure#1}
Line
Count
Source
819
2
        let (hhmm, input) = parse::split(input, 4).ok_or_else(|| {
820
2
            err!(
821
                "expected at least 4 digits for time zone offset \
822
                 after sign, but found only {len} bytes remaining",
823
2
                len = input.len(),
824
            )
825
2
        })?;
826
827
222
        let hh = parse::i64(&hhmm[0..2]).with_context(|| {
828
36
            err!(
829
                "failed to parse hours from time zone offset {hhmm}",
830
36
                hhmm = escape::Bytes(hhmm)
831
            )
832
36
        })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_offset::{closure#2}
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_offset::{closure#2}
<jiff::fmt::rfc2822::DateTimeParser>::parse_offset::{closure#2}
Line
Count
Source
827
13
        let hh = parse::i64(&hhmm[0..2]).with_context(|| {
828
13
            err!(
829
                "failed to parse hours from time zone offset {hhmm}",
830
13
                hhmm = escape::Bytes(hhmm)
831
            )
832
13
        })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_offset::{closure#2}
<jiff::fmt::rfc2822::DateTimeParser>::parse_offset::{closure#2}
Line
Count
Source
827
23
        let hh = parse::i64(&hhmm[0..2]).with_context(|| {
828
23
            err!(
829
                "failed to parse hours from time zone offset {hhmm}",
830
23
                hhmm = escape::Bytes(hhmm)
831
            )
832
23
        })?;
833
186
        let hh = ParsedOffsetHours::try_new("zone-offset-hours", hh)
834
186
            .context("time zone offset hours are not valid")?;
835
181
        let hh = t::SpanZoneOffset::rfrom(hh);
836
837
181
        let mm = parse::i64(&hhmm[2..4]).with_context(|| {
838
26
            err!(
839
                "failed to parse minutes from time zone offset {hhmm}",
840
26
                hhmm = escape::Bytes(hhmm)
841
            )
842
26
        })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_offset::{closure#3}
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_offset::{closure#3}
<jiff::fmt::rfc2822::DateTimeParser>::parse_offset::{closure#3}
Line
Count
Source
837
12
        let mm = parse::i64(&hhmm[2..4]).with_context(|| {
838
12
            err!(
839
                "failed to parse minutes from time zone offset {hhmm}",
840
12
                hhmm = escape::Bytes(hhmm)
841
            )
842
12
        })?;
Unexecuted instantiation: <jiff::fmt::rfc2822::DateTimeParser>::parse_offset::{closure#3}
<jiff::fmt::rfc2822::DateTimeParser>::parse_offset::{closure#3}
Line
Count
Source
837
14
        let mm = parse::i64(&hhmm[2..4]).with_context(|| {
838
14
            err!(
839
                "failed to parse minutes from time zone offset {hhmm}",
840
14
                hhmm = escape::Bytes(hhmm)
841
            )
842
14
        })?;
843
155
        let mm = ParsedOffsetMinutes::try_new("zone-offset-minutes", mm)
844
155
            .context("time zone offset minutes are not valid")?;
845
152
        let mm = t::SpanZoneOffset::rfrom(mm);
846
847
152
        let seconds = hh * C(3_600) + mm * C(60);
848
152
        let offset = Offset::from_seconds_ranged(seconds * sign);
849
152
        Ok(Parsed { value: offset, input })
850
1.57k
    }
851
852
    /// Parses an obsolete time zone offset.
853
    #[inline(never)]
854
1.13k
    fn parse_offset_obsolete<'i>(
855
1.13k
        &self,
856
1.13k
        input: &'i [u8],
857
1.13k
    ) -> Result<Parsed<'i, Offset>, Error> {
858
1.13k
        let mut letters = [0; 5];
859
1.13k
        let mut len = 0;
860
3.34k
        while len <= 4
861
3.22k
            && !input[len..].is_empty()
862
2.87k
            && !is_whitespace(input[len])
863
2.20k
        {
864
2.20k
            letters[len] = input[len].to_ascii_lowercase();
865
2.20k
            len += 1;
866
2.20k
        }
867
1.13k
        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
1.13k
        }
874
1.13k
        let offset = match &letters[..len] {
875
1.13k
            b"ut" | b"gmt" | b"z" => Offset::UTC,
876
5
            b"est" => Offset::constant(-5),
877
7
            b"edt" => Offset::constant(-4),
878
5
            b"cst" => Offset::constant(-6),
879
5
            b"cdt" => Offset::constant(-5),
880
5
            b"mst" => Offset::constant(-7),
881
5
            b"mdt" => Offset::constant(-6),
882
6
            b"pst" => Offset::constant(-8),
883
5
            b"pdt" => Offset::constant(-7),
884
1.04k
            name => {
885
1.04k
                if name.len() == 1
886
687
                    && 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
639
                    Offset::UTC
905
401
                } else if name.len() >= 3
906
1.07k
                    && 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
182
                    Offset::UTC
918
                } else {
919
                    // But anything else we throw our hands up I guess.
920
219
                    return Err(err!(
921
219
                        "expected obsolete RFC 2822 time zone abbreviation, \
922
219
                         but found {found:?}",
923
219
                        found = escape::Bytes(&input[..len]),
924
219
                    ));
925
                }
926
            }
927
        };
928
918
        Ok(Parsed { value: offset, input: &input[len..] })
929
1.13k
    }
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
2.48k
    fn parse_time_separator<'i>(
935
2.48k
        &self,
936
2.48k
        input: &'i [u8],
937
2.48k
    ) -> Result<Parsed<'i, ()>, Error> {
938
2.48k
        if input.is_empty() {
939
151
            return Err(err!(
940
151
                "expected time separator of ':', but found end of input",
941
151
            ));
942
2.33k
        }
943
2.33k
        if input[0] != b':' {
944
40
            return Err(err!(
945
40
                "expected time separator of ':', but found {got}",
946
40
                got = escape::Byte(input[0]),
947
40
            ));
948
2.29k
        }
949
2.29k
        Ok(Parsed { value: (), input: &input[1..] })
950
2.48k
    }
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
12.3k
    fn parse_whitespace<'i>(
956
12.3k
        &self,
957
12.3k
        input: &'i [u8],
958
12.3k
    ) -> Result<Parsed<'i, ()>, Error> {
959
12.3k
        let Parsed { input, value: had_whitespace } =
960
12.3k
            self.skip_whitespace(input);
961
12.3k
        if !had_whitespace {
962
2.87k
            return Err(err!(
963
2.87k
                "expected at least one whitespace character (space or tab), \
964
2.87k
                 but found none",
965
2.87k
            ));
966
9.46k
        }
967
9.46k
        Ok(Parsed { value: (), input })
968
12.3k
    }
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
35.2k
    fn skip_whitespace<'i>(&self, mut input: &'i [u8]) -> Parsed<'i, bool> {
977
35.2k
        let mut found_whitespace = false;
978
158k
        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
978
35.1k
        while input.first().map_or(false, |&b| is_whitespace(b)) {
<jiff::fmt::rfc2822::DateTimeParser>::skip_whitespace::{closure#0}
Line
Count
Source
978
82.6k
        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
978
38.1k
        while input.first().map_or(false, |&b| is_whitespace(b)) {
979
122k
            input = &input[1..];
980
122k
            found_whitespace = true;
981
122k
        }
982
35.2k
        Parsed { value: found_whitespace, input }
983
35.2k
    }
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
665
    fn skip_comment<'i>(
996
665
        &self,
997
665
        mut input: &'i [u8],
998
665
    ) -> Result<Parsed<'i, ()>, Error> {
999
665
        if !input.starts_with(b"(") {
1000
353
            return Ok(Parsed { value: (), input });
1001
312
        }
1002
312
        input = &input[1..];
1003
312
        let mut depth: u8 = 1;
1004
312
        let mut escape = false;
1005
524k
        for byte in input.iter().copied() {
1006
524k
            input = &input[1..];
1007
524k
            if escape {
1008
719
                escape = false;
1009
523k
            } else if byte == b'\\' {
1010
729
                escape = true;
1011
522k
            } 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
1.31k
                depth = depth.checked_sub(1).ok_or_else(|| {
1016
0
                    err!(
1017
                        "found closing parenthesis in comment with \
1018
                         no matching opening parenthesis"
1019
                    )
1020
0
                })?;
1021
1.31k
                if depth == 0 {
1022
193
                    break;
1023
1.11k
                }
1024
521k
            } else if byte == b'(' {
1025
3.92k
                depth = depth.checked_add(1).ok_or_else(|| {
1026
6
                    err!("found too many nested parenthesis in comment")
1027
6
                })?;
1028
517k
            }
1029
        }
1030
306
        if depth > 0 {
1031
113
            return Err(err!(
1032
113
                "found opening parenthesis in comment with \
1033
113
                 no matching closing parenthesis"
1034
113
            ));
1035
193
        }
1036
193
        let Parsed { input, .. } = self.skip_whitespace(input);
1037
193
        Ok(Parsed { value: (), input })
1038
665
    }
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
158k
fn is_whitespace(byte: u8) -> bool {
1560
158k
    byte.is_ascii_whitespace()
1561
158k
}
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
}