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