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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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() >= 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(test)]
1543mod tests {
1544 use alloc::string::{String, ToString};
1545
1546 use crate::civil::date;
1547
1548 use super::*;
1549
1550 #[test]
1551 fn ok_parse_basic() {
1552 let p = |input| DateTimeParser::new().parse_zoned(input).unwrap();
1553
1554 insta::assert_debug_snapshot!(
1555 p("Wed, 10 Jan 2024 05:34:45 -0500"),
1556 @"2024-01-10T05:34:45-05:00[-05:00]",
1557 );
1558 insta::assert_debug_snapshot!(
1559 p("Tue, 9 Jan 2024 05:34:45 -0500"),
1560 @"2024-01-09T05:34:45-05:00[-05:00]",
1561 );
1562 insta::assert_debug_snapshot!(
1563 p("Tue, 09 Jan 2024 05:34:45 -0500"),
1564 @"2024-01-09T05:34:45-05:00[-05:00]",
1565 );
1566 insta::assert_debug_snapshot!(
1567 p("10 Jan 2024 05:34:45 -0500"),
1568 @"2024-01-10T05:34:45-05:00[-05:00]",
1569 );
1570 insta::assert_debug_snapshot!(
1571 p("10 Jan 2024 05:34 -0500"),
1572 @"2024-01-10T05:34:00-05:00[-05:00]",
1573 );
1574 insta::assert_debug_snapshot!(
1575 p("10 Jan 2024 05:34:45 +0500"),
1576 @"2024-01-10T05:34:45+05:00[+05:00]",
1577 );
1578 insta::assert_debug_snapshot!(
1579 p("Thu, 29 Feb 2024 05:34 -0500"),
1580 @"2024-02-29T05:34:00-05:00[-05:00]",
1581 );
1582
1583 // leap second constraining
1584 insta::assert_debug_snapshot!(
1585 p("10 Jan 2024 05:34:60 -0500"),
1586 @"2024-01-10T05:34:59-05:00[-05:00]",
1587 );
1588 }
1589
1590 #[test]
1591 fn ok_parse_obsolete_zone() {
1592 let p = |input| DateTimeParser::new().parse_zoned(input).unwrap();
1593
1594 insta::assert_debug_snapshot!(
1595 p("Wed, 10 Jan 2024 05:34:45 EST"),
1596 @"2024-01-10T05:34:45-05:00[-05:00]",
1597 );
1598 insta::assert_debug_snapshot!(
1599 p("Wed, 10 Jan 2024 05:34:45 EDT"),
1600 @"2024-01-10T05:34:45-04:00[-04:00]",
1601 );
1602 insta::assert_debug_snapshot!(
1603 p("Wed, 10 Jan 2024 05:34:45 CST"),
1604 @"2024-01-10T05:34:45-06:00[-06:00]",
1605 );
1606 insta::assert_debug_snapshot!(
1607 p("Wed, 10 Jan 2024 05:34:45 CDT"),
1608 @"2024-01-10T05:34:45-05:00[-05:00]",
1609 );
1610 insta::assert_debug_snapshot!(
1611 p("Wed, 10 Jan 2024 05:34:45 mst"),
1612 @"2024-01-10T05:34:45-07:00[-07:00]",
1613 );
1614 insta::assert_debug_snapshot!(
1615 p("Wed, 10 Jan 2024 05:34:45 mdt"),
1616 @"2024-01-10T05:34:45-06:00[-06:00]",
1617 );
1618 insta::assert_debug_snapshot!(
1619 p("Wed, 10 Jan 2024 05:34:45 pst"),
1620 @"2024-01-10T05:34:45-08:00[-08:00]",
1621 );
1622 insta::assert_debug_snapshot!(
1623 p("Wed, 10 Jan 2024 05:34:45 pdt"),
1624 @"2024-01-10T05:34:45-07:00[-07:00]",
1625 );
1626
1627 // Various things that mean UTC.
1628 insta::assert_debug_snapshot!(
1629 p("Wed, 10 Jan 2024 05:34:45 UT"),
1630 @"2024-01-10T05:34:45+00:00[UTC]",
1631 );
1632 insta::assert_debug_snapshot!(
1633 p("Wed, 10 Jan 2024 05:34:45 Z"),
1634 @"2024-01-10T05:34:45+00:00[UTC]",
1635 );
1636 insta::assert_debug_snapshot!(
1637 p("Wed, 10 Jan 2024 05:34:45 gmt"),
1638 @"2024-01-10T05:34:45+00:00[UTC]",
1639 );
1640
1641 // Even things that are unrecognized just get treated as having
1642 // an offset of 0.
1643 insta::assert_debug_snapshot!(
1644 p("Wed, 10 Jan 2024 05:34:45 XXX"),
1645 @"2024-01-10T05:34:45+00:00[UTC]",
1646 );
1647 insta::assert_debug_snapshot!(
1648 p("Wed, 10 Jan 2024 05:34:45 ABCDE"),
1649 @"2024-01-10T05:34:45+00:00[UTC]",
1650 );
1651 insta::assert_debug_snapshot!(
1652 p("Wed, 10 Jan 2024 05:34:45 FUCK"),
1653 @"2024-01-10T05:34:45+00:00[UTC]",
1654 );
1655 }
1656
1657 // whyyyyyyyyyyyyy
1658 #[test]
1659 fn ok_parse_comment() {
1660 let p = |input| DateTimeParser::new().parse_zoned(input).unwrap();
1661
1662 insta::assert_debug_snapshot!(
1663 p("Wed, 10 Jan 2024 05:34:45 -0500 (wat)"),
1664 @"2024-01-10T05:34:45-05:00[-05:00]",
1665 );
1666 insta::assert_debug_snapshot!(
1667 p("Wed, 10 Jan 2024 05:34:45 -0500 (w(a)t)"),
1668 @"2024-01-10T05:34:45-05:00[-05:00]",
1669 );
1670 insta::assert_debug_snapshot!(
1671 p(r"Wed, 10 Jan 2024 05:34:45 -0500 (w\(a\)t)"),
1672 @"2024-01-10T05:34:45-05:00[-05:00]",
1673 );
1674 }
1675
1676 #[test]
1677 fn ok_parse_whitespace() {
1678 let p = |input| DateTimeParser::new().parse_zoned(input).unwrap();
1679
1680 insta::assert_debug_snapshot!(
1681 p("Wed, 10 \t Jan \n\r\n\n 2024 05:34:45 -0500"),
1682 @"2024-01-10T05:34:45-05:00[-05:00]",
1683 );
1684 insta::assert_debug_snapshot!(
1685 p("Wed, 10 Jan 2024 05:34:45 -0500 "),
1686 @"2024-01-10T05:34:45-05:00[-05:00]",
1687 );
1688 }
1689
1690 #[test]
1691 fn err_parse_invalid() {
1692 let p = |input| {
1693 DateTimeParser::new().parse_zoned(input).unwrap_err().to_string()
1694 };
1695
1696 insta::assert_snapshot!(
1697 p("Thu, 10 Jan 2024 05:34:45 -0500"),
1698 @"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",
1699 );
1700 insta::assert_snapshot!(
1701 p("Wed, 29 Feb 2023 05:34:45 -0500"),
1702 @"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",
1703 );
1704 insta::assert_snapshot!(
1705 p("Mon, 31 Jun 2024 05:34:45 -0500"),
1706 @"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",
1707 );
1708 insta::assert_snapshot!(
1709 p("Tue, 32 Jun 2024 05:34:45 -0500"),
1710 @"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",
1711 );
1712 insta::assert_snapshot!(
1713 p("Sun, 30 Jun 2024 24:00:00 -0500"),
1714 @"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",
1715 );
1716 }
1717
1718 #[test]
1719 fn err_parse_incomplete() {
1720 let p = |input| {
1721 DateTimeParser::new().parse_zoned(input).unwrap_err().to_string()
1722 };
1723
1724 insta::assert_snapshot!(
1725 p(""),
1726 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected RFC 2822 datetime, but got empty string",
1727 );
1728 insta::assert_snapshot!(
1729 p(" "),
1730 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected RFC 2822 datetime, but got empty string after trimming whitespace",
1731 );
1732 insta::assert_snapshot!(
1733 p("Wat"),
1734 @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)"###,
1735 );
1736 insta::assert_snapshot!(
1737 p("Wed"),
1738 @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)"###,
1739 );
1740 insta::assert_snapshot!(
1741 p("Wat, "),
1742 @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"###,
1743 );
1744 insta::assert_snapshot!(
1745 p("Wed, "),
1746 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day, but found end of input",
1747 );
1748 insta::assert_snapshot!(
1749 p("Wed, 1"),
1750 @"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",
1751 );
1752 insta::assert_snapshot!(
1753 p("Wed, 10"),
1754 @"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",
1755 );
1756 insta::assert_snapshot!(
1757 p("Wed, 10 J"),
1758 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected abbreviated month name, but remaining input is too short (remaining bytes is 1)",
1759 );
1760 insta::assert_snapshot!(
1761 p("Wed, 10 Wat"),
1762 @r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected abbreviated month name, but did not recognize "Wat" as a valid month"###,
1763 );
1764 insta::assert_snapshot!(
1765 p("Wed, 10 Jan"),
1766 @"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",
1767 );
1768 insta::assert_snapshot!(
1769 p("Wed, 10 Jan 2"),
1770 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected at least two ASCII digits for parsing a year, but only found 1",
1771 );
1772 insta::assert_snapshot!(
1773 p("Wed, 10 Jan 2024"),
1774 @"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",
1775 );
1776 insta::assert_snapshot!(
1777 p("Wed, 10 Jan 2024 05"),
1778 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected time separator of ':', but found end of input",
1779 );
1780 insta::assert_snapshot!(
1781 p("Wed, 10 Jan 2024 053"),
1782 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected time separator of ':', but found 3",
1783 );
1784 insta::assert_snapshot!(
1785 p("Wed, 10 Jan 2024 05:34"),
1786 @"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",
1787 );
1788 insta::assert_snapshot!(
1789 p("Wed, 10 Jan 2024 05:34:"),
1790 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected two digit second, but found end of input",
1791 );
1792 insta::assert_snapshot!(
1793 p("Wed, 10 Jan 2024 05:34:45"),
1794 @"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",
1795 );
1796 insta::assert_snapshot!(
1797 p("Wed, 10 Jan 2024 05:34:45 J"),
1798 @r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected obsolete RFC 2822 time zone abbreviation, but found "J""###,
1799 );
1800 }
1801
1802 #[test]
1803 fn err_parse_comment() {
1804 let p = |input| {
1805 DateTimeParser::new().parse_zoned(input).unwrap_err().to_string()
1806 };
1807
1808 insta::assert_snapshot!(
1809 p(r"Wed, 10 Jan 2024 05:34:45 -0500 (wa)t)"),
1810 @r###"parsed value '2024-01-10T05:34:45-05:00[-05:00]', but unparsed input "t)" remains (expected no unparsed input)"###,
1811 );
1812 insta::assert_snapshot!(
1813 p(r"Wed, 10 Jan 2024 05:34:45 -0500 (wa(t)"),
1814 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: found opening parenthesis in comment with no matching closing parenthesis",
1815 );
1816 insta::assert_snapshot!(
1817 p(r"Wed, 10 Jan 2024 05:34:45 -0500 (w"),
1818 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: found opening parenthesis in comment with no matching closing parenthesis",
1819 );
1820 insta::assert_snapshot!(
1821 p(r"Wed, 10 Jan 2024 05:34:45 -0500 ("),
1822 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: found opening parenthesis in comment with no matching closing parenthesis",
1823 );
1824 insta::assert_snapshot!(
1825 p(r"Wed, 10 Jan 2024 05:34:45 -0500 ( "),
1826 @"failed to parse RFC 2822 datetime into Jiff zoned datetime: found opening parenthesis in comment with no matching closing parenthesis",
1827 );
1828 }
1829
1830 #[test]
1831 fn ok_print_zoned() {
1832 if crate::tz::db().is_definitively_empty() {
1833 return;
1834 }
1835
1836 let p = |zdt: &Zoned| -> String {
1837 let mut buf = String::new();
1838 DateTimePrinter::new().print_zoned(&zdt, &mut buf).unwrap();
1839 buf
1840 };
1841
1842 let zdt = date(2024, 1, 10)
1843 .at(5, 34, 45, 0)
1844 .in_tz("America/New_York")
1845 .unwrap();
1846 insta::assert_snapshot!(p(&zdt), @"Wed, 10 Jan 2024 05:34:45 -0500");
1847
1848 let zdt = date(2024, 2, 5)
1849 .at(5, 34, 45, 0)
1850 .in_tz("America/New_York")
1851 .unwrap();
1852 insta::assert_snapshot!(p(&zdt), @"Mon, 5 Feb 2024 05:34:45 -0500");
1853
1854 let zdt = date(2024, 7, 31)
1855 .at(5, 34, 45, 0)
1856 .in_tz("America/New_York")
1857 .unwrap();
1858 insta::assert_snapshot!(p(&zdt), @"Wed, 31 Jul 2024 05:34:45 -0400");
1859
1860 let zdt = date(2024, 3, 5).at(5, 34, 45, 0).in_tz("UTC").unwrap();
1861 // Notice that this prints a +0000 offset.
1862 // But when printing a Timestamp, a -0000 offset is used.
1863 // This is because in the case of Timestamp, the "true"
1864 // offset is not known.
1865 insta::assert_snapshot!(p(&zdt), @"Tue, 5 Mar 2024 05:34:45 +0000");
1866 }
1867
1868 #[test]
1869 fn ok_print_timestamp() {
1870 if crate::tz::db().is_definitively_empty() {
1871 return;
1872 }
1873
1874 let p = |ts: Timestamp| -> String {
1875 let mut buf = String::new();
1876 DateTimePrinter::new().print_timestamp(&ts, &mut buf).unwrap();
1877 buf
1878 };
1879
1880 let ts = date(2024, 1, 10)
1881 .at(5, 34, 45, 0)
1882 .in_tz("America/New_York")
1883 .unwrap()
1884 .timestamp();
1885 insta::assert_snapshot!(p(ts), @"Wed, 10 Jan 2024 10:34:45 -0000");
1886
1887 let ts = date(2024, 2, 5)
1888 .at(5, 34, 45, 0)
1889 .in_tz("America/New_York")
1890 .unwrap()
1891 .timestamp();
1892 insta::assert_snapshot!(p(ts), @"Mon, 5 Feb 2024 10:34:45 -0000");
1893
1894 let ts = date(2024, 7, 31)
1895 .at(5, 34, 45, 0)
1896 .in_tz("America/New_York")
1897 .unwrap()
1898 .timestamp();
1899 insta::assert_snapshot!(p(ts), @"Wed, 31 Jul 2024 09:34:45 -0000");
1900
1901 let ts = date(2024, 3, 5)
1902 .at(5, 34, 45, 0)
1903 .in_tz("UTC")
1904 .unwrap()
1905 .timestamp();
1906 // Notice that this prints a +0000 offset.
1907 // But when printing a Timestamp, a -0000 offset is used.
1908 // This is because in the case of Timestamp, the "true"
1909 // offset is not known.
1910 insta::assert_snapshot!(p(ts), @"Tue, 5 Mar 2024 05:34:45 -0000");
1911 }
1912
1913 #[test]
1914 fn ok_print_rfc9110_timestamp() {
1915 if crate::tz::db().is_definitively_empty() {
1916 return;
1917 }
1918
1919 let p = |ts: Timestamp| -> String {
1920 let mut buf = String::new();
1921 DateTimePrinter::new()
1922 .print_timestamp_rfc9110(&ts, &mut buf)
1923 .unwrap();
1924 buf
1925 };
1926
1927 let ts = date(2024, 1, 10)
1928 .at(5, 34, 45, 0)
1929 .in_tz("America/New_York")
1930 .unwrap()
1931 .timestamp();
1932 insta::assert_snapshot!(p(ts), @"Wed, 10 Jan 2024 10:34:45 GMT");
1933
1934 let ts = date(2024, 2, 5)
1935 .at(5, 34, 45, 0)
1936 .in_tz("America/New_York")
1937 .unwrap()
1938 .timestamp();
1939 insta::assert_snapshot!(p(ts), @"Mon, 05 Feb 2024 10:34:45 GMT");
1940
1941 let ts = date(2024, 7, 31)
1942 .at(5, 34, 45, 0)
1943 .in_tz("America/New_York")
1944 .unwrap()
1945 .timestamp();
1946 insta::assert_snapshot!(p(ts), @"Wed, 31 Jul 2024 09:34:45 GMT");
1947
1948 let ts = date(2024, 3, 5)
1949 .at(5, 34, 45, 0)
1950 .in_tz("UTC")
1951 .unwrap()
1952 .timestamp();
1953 // Notice that this prints a +0000 offset.
1954 // But when printing a Timestamp, a -0000 offset is used.
1955 // This is because in the case of Timestamp, the "true"
1956 // offset is not known.
1957 insta::assert_snapshot!(p(ts), @"Tue, 05 Mar 2024 05:34:45 GMT");
1958 }
1959
1960 #[test]
1961 fn err_print_zoned() {
1962 if crate::tz::db().is_definitively_empty() {
1963 return;
1964 }
1965
1966 let p = |zdt: &Zoned| -> String {
1967 let mut buf = String::new();
1968 DateTimePrinter::new()
1969 .print_zoned(&zdt, &mut buf)
1970 .unwrap_err()
1971 .to_string()
1972 };
1973
1974 let zdt = date(-1, 1, 10)
1975 .at(5, 34, 45, 0)
1976 .in_tz("America/New_York")
1977 .unwrap();
1978 insta::assert_snapshot!(p(&zdt), @"datetime -000001-01-10T05:34:45 has negative year, which cannot be formatted with RFC 2822");
1979 }
1980
1981 #[test]
1982 fn err_print_timestamp() {
1983 if crate::tz::db().is_definitively_empty() {
1984 return;
1985 }
1986
1987 let p = |ts: Timestamp| -> String {
1988 let mut buf = String::new();
1989 DateTimePrinter::new()
1990 .print_timestamp(&ts, &mut buf)
1991 .unwrap_err()
1992 .to_string()
1993 };
1994
1995 let ts = date(-1, 1, 10)
1996 .at(5, 34, 45, 0)
1997 .in_tz("America/New_York")
1998 .unwrap()
1999 .timestamp();
2000 insta::assert_snapshot!(p(ts), @"datetime -000001-01-10T10:30:47 has negative year, which cannot be formatted with RFC 2822");
2001 }
2002}