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