From f9b47795aaaa388aedfdf7ef6a146c67c8e7cc0f Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Sat, 31 Aug 2024 10:16:23 -0400 Subject: [PATCH 1/8] fmt/temporal: add DateTimePrinter::print_timestamp_with_offset This makes it possible to format a `Timestamp` with a specific offset. This was already technically possible with the `strftime` API, but adding explicit support for it to the `jiff::fmt::temporal` module seems like good sense. Note that no dedicated parsing routine is added for this because it is already supported automatically. --- src/fmt/temporal/mod.rs | 91 ++++++++++++++++++++++++++++++++++++- src/fmt/temporal/printer.rs | 15 ++++-- 2 files changed, 100 insertions(+), 6 deletions(-) diff --git a/src/fmt/temporal/mod.rs b/src/fmt/temporal/mod.rs index 59552dd..3877eb5 100644 --- a/src/fmt/temporal/mod.rs +++ b/src/fmt/temporal/mod.rs @@ -168,7 +168,7 @@ use crate::{ error::Error, fmt::Write, span::Span, - tz::{Disambiguation, OffsetConflict, TimeZoneDatabase}, + tz::{Disambiguation, Offset, OffsetConflict, TimeZoneDatabase}, SignedDuration, Timestamp, Zoned, }; @@ -984,6 +984,25 @@ impl DateTimePrinter { /// Print a `Timestamp` datetime to the given writer. /// + /// This will always write an RFC 3339 compatible string with a `Z` or + /// Zulu offset. Zulu is chosen in accordance with RFC 9557's update to + /// RFC 3339 that establishes the `-00:00` offset as equivalent to Zulu: + /// + /// > If the time in UTC is known, but the offset to local time is + /// > unknown, this can be represented with an offset of "Z". (The + /// > original version of this specification provided -00:00 for this + /// > purpose, which is not allowed by ISO8601:2000 and therefore is + /// > less interoperable; Section 3.3 of RFC5322 describes a related + /// > convention for email, which does not have this problem). This + /// > differs semantically from an offset of +00:00, which implies that + /// > UTC is the preferred reference point for the specified time. + /// + /// In other words, both Zulu time and `-00:00` mean "the time in UTC is + /// known, but the offset to local time is unknown." + /// + /// If you need to write an RFC 3339 timestamp with a specific offset, + /// use [`DateTimePrinter::print_timestamp_with_offset`]. + /// /// # Errors /// /// This only returns an error when writing to the given [`Write`] @@ -1011,7 +1030,75 @@ impl DateTimePrinter { timestamp: &Timestamp, wtr: W, ) -> Result<(), Error> { - self.p.print_timestamp(timestamp, wtr) + self.p.print_timestamp(timestamp, None, wtr) + } + + /// Print a `Timestamp` datetime to the given writer with the given offset. + /// + /// This will always write an RFC 3339 compatible string with an offset. + /// + /// This will never write either `Z` (for Zulu time) or `-00:00` as an + /// offset. This is because Zulu time (and `-00:00`) mean "the time in UTC + /// is known, but the offset to local time is unknown." Since this routine + /// accepts an explicit offset, the offset is known. For example, + /// `Offset::UTC` will be formatted as `+00:00`. + /// + /// To write an RFC 3339 string in Zulu time, use + /// [`DateTimePrinter::print_timestamp`]. + /// + /// # Errors + /// + /// This only returns an error when writing to the given [`Write`] + /// implementation would fail. Some such implementations, like for `String` + /// and `Vec`, never fail (unless memory allocation fails). In such + /// cases, it would be appropriate to call `unwrap()` on the result. + /// + /// # Example + /// + /// ``` + /// use jiff::{fmt::temporal::DateTimePrinter, tz, Timestamp}; + /// + /// let timestamp = Timestamp::new(0, 1) + /// .expect("one nanosecond after Unix epoch is always valid"); + /// + /// let mut buf = String::new(); + /// // Printing to a `String` can never fail. + /// DateTimePrinter::new().print_timestamp_with_offset( + /// ×tamp, + /// tz::offset(-5), + /// &mut buf, + /// ).unwrap(); + /// assert_eq!(buf, "1969-12-31T19:00:00.000000001-05:00"); + /// + /// # Ok::<(), Box>(()) + /// ``` + /// + /// # Example: `Offset::UTC` formats as `+00:00` + /// + /// ``` + /// use jiff::{fmt::temporal::DateTimePrinter, tz::Offset, Timestamp}; + /// + /// let timestamp = Timestamp::new(0, 1) + /// .expect("one nanosecond after Unix epoch is always valid"); + /// + /// let mut buf = String::new(); + /// // Printing to a `String` can never fail. + /// DateTimePrinter::new().print_timestamp_with_offset( + /// ×tamp, + /// Offset::UTC, // equivalent to `Offset::from_hours(0)` + /// &mut buf, + /// ).unwrap(); + /// assert_eq!(buf, "1970-01-01T00:00:00.000000001+00:00"); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn print_timestamp_with_offset( + &self, + timestamp: &Timestamp, + offset: Offset, + wtr: W, + ) -> Result<(), Error> { + self.p.print_timestamp(timestamp, Some(offset), wtr) } /// Print a `civil::DateTime` to the given writer. diff --git a/src/fmt/temporal/printer.rs b/src/fmt/temporal/printer.rs index 0822256..a8f17b4 100644 --- a/src/fmt/temporal/printer.rs +++ b/src/fmt/temporal/printer.rs @@ -63,11 +63,18 @@ impl DateTimePrinter { pub(super) fn print_timestamp( &self, timestamp: &Timestamp, + offset: Option, mut wtr: W, ) -> Result<(), Error> { - let dt = TimeZone::UTC.to_datetime(*timestamp); + let Some(offset) = offset else { + let dt = TimeZone::UTC.to_datetime(*timestamp); + self.print_datetime(&dt, &mut wtr)?; + self.print_zulu(&mut wtr)?; + return Ok(()); + }; + let dt = offset.to_datetime(*timestamp); self.print_datetime(&dt, &mut wtr)?; - self.print_zulu(&mut wtr)?; + self.print_offset(&offset, &mut wtr)?; Ok(()) } @@ -427,7 +434,7 @@ mod tests { let zoned: Zoned = dt.intz("America/New_York").unwrap(); let mut buf = String::new(); DateTimePrinter::new() - .print_timestamp(&zoned.timestamp(), &mut buf) + .print_timestamp(&zoned.timestamp(), None, &mut buf) .unwrap(); assert_eq!(buf, "2024-03-10T09:34:45Z"); @@ -435,7 +442,7 @@ mod tests { let zoned: Zoned = dt.intz("America/New_York").unwrap(); let mut buf = String::new(); DateTimePrinter::new() - .print_timestamp(&zoned.timestamp(), &mut buf) + .print_timestamp(&zoned.timestamp(), None, &mut buf) .unwrap(); assert_eq!(buf, "-002024-03-10T10:30:47Z"); } From 00ca0131ecfa552229ca722d2cee1e337b6b94b3 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Sat, 31 Aug 2024 10:25:14 -0400 Subject: [PATCH 2/8] fmt: add some #[inline] annotations These are just on the config settings. In newer versions of Rust, I'd expect these to get cross-crate inlined since they are trivial. And of course, when used in a const context, it doesn't matter if they get inlined. But these really should always be inlined, so add the annotation to allow it in contexts where they might otherwise not. I was tempted to add inline annotations to basically every other parse/print routine as well, but I didn't want to overdo it without a more compelling motivation. --- src/fmt/rfc2822.rs | 5 +++++ src/fmt/temporal/mod.rs | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/src/fmt/rfc2822.rs b/src/fmt/rfc2822.rs index 43402d8..b38535d 100644 --- a/src/fmt/rfc2822.rs +++ b/src/fmt/rfc2822.rs @@ -105,6 +105,7 @@ pub(crate) static DEFAULT_DATETIME_PRINTER: DateTimePrinter = /// /// # Ok::<(), Box>(()) /// ``` +#[inline] pub fn to_string(zdt: &Zoned) -> Result { let mut buf = String::new(); DEFAULT_DATETIME_PRINTER.print_zoned(zdt, &mut buf)?; @@ -168,6 +169,7 @@ pub fn to_string(zdt: &Zoned) -> Result { /// /// # Ok::<(), Box>(()) /// ``` +#[inline] pub fn parse(string: &str) -> Result { DEFAULT_DATETIME_PARSER.parse_zoned(string) } @@ -222,6 +224,7 @@ pub struct DateTimeParser { impl DateTimeParser { /// Create a new RFC 2822 datetime parser with the default configuration. + #[inline] pub const fn new() -> DateTimeParser { DateTimeParser { relaxed_weekday: false } } @@ -259,6 +262,7 @@ impl DateTimeParser { /// /// # Ok::<(), Box>(()) /// ``` + #[inline] pub const fn relaxed_weekday(self, yes: bool) -> DateTimeParser { DateTimeParser { relaxed_weekday: yes, ..self } } @@ -1090,6 +1094,7 @@ pub struct DateTimePrinter { impl DateTimePrinter { /// Create a new RFC 2822 datetime printer with the default configuration. + #[inline] pub const fn new() -> DateTimePrinter { DateTimePrinter { _private: () } } diff --git a/src/fmt/temporal/mod.rs b/src/fmt/temporal/mod.rs index 3877eb5..aca4c1d 100644 --- a/src/fmt/temporal/mod.rs +++ b/src/fmt/temporal/mod.rs @@ -244,6 +244,7 @@ pub struct DateTimeParser { impl DateTimeParser { /// Create a new Temporal datetime parser with the default configuration. + #[inline] pub const fn new() -> DateTimeParser { DateTimeParser { p: parser::DateTimeParser::new(), @@ -283,6 +284,7 @@ impl DateTimeParser { /// /// # Ok::<(), Box>(()) /// ``` + #[inline] pub const fn offset_conflict( self, strategy: OffsetConflict, @@ -363,6 +365,7 @@ impl DateTimeParser { /// /// # Ok::<(), Box>(()) /// ``` + #[inline] pub const fn disambiguation( self, strategy: Disambiguation, @@ -849,6 +852,7 @@ impl DateTimePrinter { /// /// # Ok::<(), Box>(()) /// ``` + #[inline] pub const fn lowercase(mut self, yes: bool) -> DateTimePrinter { self.p = self.p.lowercase(yes); self @@ -880,6 +884,7 @@ impl DateTimePrinter { /// /// # Ok::<(), Box>(()) /// ``` + #[inline] pub const fn separator(mut self, ascii_char: u8) -> DateTimePrinter { self.p = self.p.separator(ascii_char); self @@ -941,6 +946,7 @@ impl DateTimePrinter { /// /// # Ok::<(), Box>(()) /// ``` + #[inline] pub const fn precision( mut self, precision: Option, @@ -1231,6 +1237,7 @@ pub struct SpanParser { impl SpanParser { /// Create a new Temporal datetime printer with the default configuration. + #[inline] pub const fn new() -> SpanParser { SpanParser { p: parser::SpanParser::new() } } @@ -1389,6 +1396,7 @@ pub struct SpanPrinter { impl SpanPrinter { /// Create a new Temporal span printer with the default configuration. + #[inline] pub const fn new() -> SpanPrinter { SpanPrinter { p: printer::SpanPrinter::new() } } From 421fbbd502135c2e1c38dc334bc2baeffdf466bc Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Sat, 31 Aug 2024 10:38:37 -0400 Subject: [PATCH 3/8] fmt/temporal: add 'to_string' convenience routines to Temporal printers I think this is a big ergonomic win for a very common case. It makes using the datetime and span printers much nicer, and they are necessary to use in some cases when the output format needs more configuration. --- src/fmt/temporal/mod.rs | 258 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 252 insertions(+), 6 deletions(-) diff --git a/src/fmt/temporal/mod.rs b/src/fmt/temporal/mod.rs index aca4c1d..c50a428 100644 --- a/src/fmt/temporal/mod.rs +++ b/src/fmt/temporal/mod.rs @@ -163,6 +163,8 @@ There is some more [background on Temporal's format] available. [background on Temporal's format]: https://github.com/tc39/proposal-temporal/issues/2843 */ +use alloc::string::String; + use crate::{ civil, error::Error, @@ -955,6 +957,198 @@ impl DateTimePrinter { self } + /// Format a `Zoned` datetime into a string. + /// + /// This is a convenience routine for [`DateTimePrinter::print_zoned`] with + /// a `String`. + /// + /// # Example + /// + /// ``` + /// use jiff::{civil::date, fmt::temporal::DateTimePrinter}; + /// + /// const PRINTER: DateTimePrinter = DateTimePrinter::new(); + /// + /// let zdt = date(2024, 6, 15).at(7, 0, 0, 0).intz("America/New_York")?; + /// assert_eq!( + /// PRINTER.zoned_to_string(&zdt), + /// "2024-06-15T07:00:00-04:00[America/New_York]", + /// ); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn zoned_to_string(&self, zdt: &Zoned) -> String { + let mut buf = String::with_capacity(4); + // OK because writing to `String` never fails. + self.print_zoned(zdt, &mut buf).unwrap(); + buf + } + + /// Format a `Timestamp` datetime into a string. + /// + /// This will always return an RFC 3339 compatible string with a `Z` or + /// Zulu offset. Zulu is chosen in accordance with RFC 9557's update to + /// RFC 3339 that establishes the `-00:00` offset as equivalent to Zulu: + /// + /// > If the time in UTC is known, but the offset to local time is + /// > unknown, this can be represented with an offset of "Z". (The + /// > original version of this specification provided -00:00 for this + /// > purpose, which is not allowed by ISO8601:2000 and therefore is + /// > less interoperable; Section 3.3 of RFC5322 describes a related + /// > convention for email, which does not have this problem). This + /// > differs semantically from an offset of +00:00, which implies that + /// > UTC is the preferred reference point for the specified time. + /// + /// In other words, both Zulu time and `-00:00` mean "the time in UTC is + /// known, but the offset to local time is unknown." + /// + /// If you need to format an RFC 3339 timestamp with a specific offset, + /// use [`DateTimePrinter::timestamp_with_offset_to_string`]. + /// + /// This is a convenience routine for [`DateTimePrinter::print_timestamp`] + /// with a `String`. + /// + /// # Example + /// + /// ``` + /// use jiff::{fmt::temporal::DateTimePrinter, Timestamp}; + /// + /// let timestamp = Timestamp::new(0, 1) + /// .expect("one nanosecond after Unix epoch is always valid"); + /// assert_eq!( + /// DateTimePrinter::new().timestamp_to_string(×tamp), + /// "1970-01-01T00:00:00.000000001Z", + /// ); + /// ``` + pub fn timestamp_to_string(&self, timestamp: &Timestamp) -> String { + let mut buf = String::with_capacity(4); + // OK because writing to `String` never fails. + self.print_timestamp(timestamp, &mut buf).unwrap(); + buf + } + + /// Format a `Timestamp` datetime into a string with the given offset. + /// + /// This will always return an RFC 3339 compatible string with an offset. + /// + /// This will never use either `Z` (for Zulu time) or `-00:00` as an + /// offset. This is because Zulu time (and `-00:00`) mean "the time in UTC + /// is known, but the offset to local time is unknown." Since this routine + /// accepts an explicit offset, the offset is known. For example, + /// `Offset::UTC` will be formatted as `+00:00`. + /// + /// To format an RFC 3339 string in Zulu time, use + /// [`DateTimePrinter::timestamp_to_string`]. + /// + /// This is a convenience routine for + /// [`DateTimePrinter::print_timestamp_with_offset`] with a `String`. + /// + /// # Example + /// + /// ``` + /// use jiff::{fmt::temporal::DateTimePrinter, tz, Timestamp}; + /// + /// const PRINTER: DateTimePrinter = DateTimePrinter::new(); + /// + /// let timestamp = Timestamp::new(0, 1) + /// .expect("one nanosecond after Unix epoch is always valid"); + /// assert_eq!( + /// PRINTER.timestamp_with_offset_to_string(×tamp, tz::offset(-5)), + /// "1969-12-31T19:00:00.000000001-05:00", + /// ); + /// ``` + /// + /// # Example: `Offset::UTC` formats as `+00:00` + /// + /// ``` + /// use jiff::{fmt::temporal::DateTimePrinter, tz::Offset, Timestamp}; + /// + /// const PRINTER: DateTimePrinter = DateTimePrinter::new(); + /// + /// let timestamp = Timestamp::new(0, 1) + /// .expect("one nanosecond after Unix epoch is always valid"); + /// assert_eq!( + /// PRINTER.timestamp_with_offset_to_string(×tamp, Offset::UTC), + /// "1970-01-01T00:00:00.000000001+00:00", + /// ); + /// ``` + pub fn timestamp_with_offset_to_string( + &self, + timestamp: &Timestamp, + offset: Offset, + ) -> String { + let mut buf = String::with_capacity(4); + // OK because writing to `String` never fails. + self.print_timestamp_with_offset(timestamp, offset, &mut buf).unwrap(); + buf + } + + /// Format a `civil::DateTime` into a string. + /// + /// This is a convenience routine for [`DateTimePrinter::print_datetime`] + /// with a `String`. + /// + /// # Example + /// + /// ``` + /// use jiff::{civil::date, fmt::temporal::DateTimePrinter}; + /// + /// const PRINTER: DateTimePrinter = DateTimePrinter::new(); + /// + /// let dt = date(2024, 6, 15).at(7, 0, 0, 0); + /// assert_eq!(PRINTER.datetime_to_string(&dt), "2024-06-15T07:00:00"); + /// ``` + pub fn datetime_to_string(&self, dt: &civil::DateTime) -> String { + let mut buf = String::with_capacity(4); + // OK because writing to `String` never fails. + self.print_datetime(dt, &mut buf).unwrap(); + buf + } + + /// Format a `civil::Date` into a string. + /// + /// This is a convenience routine for [`DateTimePrinter::print_date`] + /// with a `String`. + /// + /// # Example + /// + /// ``` + /// use jiff::{civil::date, fmt::temporal::DateTimePrinter}; + /// + /// const PRINTER: DateTimePrinter = DateTimePrinter::new(); + /// + /// let d = date(2024, 6, 15); + /// assert_eq!(PRINTER.date_to_string(&d), "2024-06-15"); + /// ``` + pub fn date_to_string(&self, date: &civil::Date) -> String { + let mut buf = String::with_capacity(4); + // OK because writing to `String` never fails. + self.print_date(date, &mut buf).unwrap(); + buf + } + + /// Format a `civil::Time` into a string. + /// + /// This is a convenience routine for [`DateTimePrinter::print_time`] + /// with a `String`. + /// + /// # Example + /// + /// ``` + /// use jiff::{civil::time, fmt::temporal::DateTimePrinter}; + /// + /// const PRINTER: DateTimePrinter = DateTimePrinter::new(); + /// + /// let t = time(7, 0, 0, 0); + /// assert_eq!(PRINTER.time_to_string(&t), "07:00:00"); + /// ``` + pub fn time_to_string(&self, time: &civil::Time) -> String { + let mut buf = String::with_capacity(4); + // OK because writing to `String` never fails. + self.print_time(time, &mut buf).unwrap(); + buf + } + /// Print a `Zoned` datetime to the given writer. /// /// # Errors @@ -1123,11 +1317,11 @@ impl DateTimePrinter { /// /// const PRINTER: DateTimePrinter = DateTimePrinter::new(); /// - /// let dt = date(2024, 6, 15).at(7, 0, 0, 0); + /// let d = date(2024, 6, 15).at(7, 0, 0, 0); /// /// let mut buf = String::new(); /// // Printing to a `String` can never fail. - /// PRINTER.print_datetime(&dt, &mut buf).unwrap(); + /// PRINTER.print_datetime(&d, &mut buf).unwrap(); /// assert_eq!(buf, "2024-06-15T07:00:00"); /// /// # Ok::<(), Box>(()) @@ -1156,11 +1350,11 @@ impl DateTimePrinter { /// /// const PRINTER: DateTimePrinter = DateTimePrinter::new(); /// - /// let dt = date(2024, 6, 15); + /// let d = date(2024, 6, 15); /// /// let mut buf = String::new(); /// // Printing to a `String` can never fail. - /// PRINTER.print_date(&dt, &mut buf).unwrap(); + /// PRINTER.print_date(&d, &mut buf).unwrap(); /// assert_eq!(buf, "2024-06-15"); /// /// # Ok::<(), Box>(()) @@ -1189,11 +1383,11 @@ impl DateTimePrinter { /// /// const PRINTER: DateTimePrinter = DateTimePrinter::new(); /// - /// let dt = time(7, 0, 0, 0); + /// let t = time(7, 0, 0, 0); /// /// let mut buf = String::new(); /// // Printing to a `String` can never fail. - /// PRINTER.print_time(&dt, &mut buf).unwrap(); + /// PRINTER.print_time(&t, &mut buf).unwrap(); /// assert_eq!(buf, "07:00:00"); /// /// # Ok::<(), Box>(()) @@ -1401,6 +1595,58 @@ impl SpanPrinter { SpanPrinter { p: printer::SpanPrinter::new() } } + /// Format a `Span` into a string. + /// + /// This is a convenience routine for [`SpanPrinter::print_span`] with + /// a `String`. + /// + /// # Example + /// + /// ``` + /// use jiff::{fmt::temporal::SpanPrinter, ToSpan}; + /// + /// const PRINTER: SpanPrinter = SpanPrinter::new(); + /// + /// let span = 3.years().months(5); + /// assert_eq!(PRINTER.span_to_string(&span), "P3y5m"); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn span_to_string(&self, span: &Span) -> String { + let mut buf = String::with_capacity(4); + // OK because writing to `String` never fails. + self.print_span(span, &mut buf).unwrap(); + buf + } + + /// Format a `SignedDuration` into a string. + /// + /// This balances the units of the duration up to at most hours + /// automatically. + /// + /// This is a convenience routine for [`SpanPrinter::print_duration`] with + /// a `String`. + /// + /// # Example + /// + /// ``` + /// use jiff::{fmt::temporal::SpanPrinter, SignedDuration}; + /// + /// const PRINTER: SpanPrinter = SpanPrinter::new(); + /// + /// let dur = SignedDuration::new(86_525, 123_000_789); + /// assert_eq!(PRINTER.duration_to_string(&dur), "PT24h2m5.123000789s"); + /// assert_eq!(PRINTER.duration_to_string(&-dur), "-PT24h2m5.123000789s"); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn duration_to_string(&self, duration: &SignedDuration) -> String { + let mut buf = String::with_capacity(4); + // OK because writing to `String` never fails. + self.print_duration(duration, &mut buf).unwrap(); + buf + } + /// Print a `Span` to the given writer. /// /// # Errors From 9aef2478a63846de9fc560ba6e5a4cf89f46ce9b Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Sat, 31 Aug 2024 13:31:47 -0400 Subject: [PATCH 4/8] fmt/rfc2822: add 'to_string' convenience routines This mimics the previous commit, but for the RFC 2822 datetime printer. --- src/fmt/rfc2822.rs | 95 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/src/fmt/rfc2822.rs b/src/fmt/rfc2822.rs index b38535d..1fc9de9 100644 --- a/src/fmt/rfc2822.rs +++ b/src/fmt/rfc2822.rs @@ -1099,6 +1099,101 @@ impl DateTimePrinter { DateTimePrinter { _private: () } } + /// Format a `Zoned` datetime into a string. + /// + /// This never emits `-0000` as the offset in the RFC 2822 format. If you + /// desire a `-0000` offset, use [`DateTimePrinter::print_timestamp`] via + /// [`Zoned::timestamp`]. + /// + /// Moreover, since RFC 2822 does not support fractional seconds, this + /// routine prints the zoned datetime as if truncating any fractional + /// seconds. + /// + /// This is a convenience routine for [`DateTimePrinter::print_zoned`] + /// with a `String`. + /// + /// # Warning + /// + /// The RFC 2822 format only supports writing a precise instant in time + /// expressed via a time zone offset. It does *not* support serializing + /// the time zone itself. This means that if you format a zoned datetime + /// in a time zone like `America/New_York` and then deserialize it, the + /// zoned datetime you get back will be a "fixed offset" zoned datetime. + /// This in turn means it will not perform daylight saving time safe + /// arithmetic. + /// + /// Basically, you should use the RFC 2822 format if it's required (for + /// example, when dealing with HTTP). But you should not choose it as a + /// general interchange format for new applications. + /// + /// # Errors + /// + /// This can return an error if the year corresponding to this timestamp + /// cannot be represented in the RFC 2822 format. For example, a negative + /// year. + /// + /// # Example + /// + /// ``` + /// use jiff::{civil::date, fmt::rfc2822::DateTimePrinter}; + /// + /// const PRINTER: DateTimePrinter = DateTimePrinter::new(); + /// + /// let zdt = date(2024, 6, 15).at(7, 0, 0, 0).intz("America/New_York")?; + /// assert_eq!( + /// PRINTER.zoned_to_string(&zdt)?, + /// "Sat, 15 Jun 2024 07:00:00 -0400", + /// ); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn zoned_to_string(&self, zdt: &Zoned) -> Result { + let mut buf = String::with_capacity(4); + self.print_zoned(zdt, &mut buf)?; + Ok(buf) + } + + /// Format a `Timestamp` datetime into a string. + /// + /// This always emits `-0000` as the offset in the RFC 2822 format. If you + /// desire a `+0000` offset, use [`DateTimePrinter::print_zoned`] with a + /// zoned datetime with [`TimeZone::UTC`]. + /// + /// Moreover, since RFC 2822 does not support fractional seconds, this + /// routine prints the timestamp as if truncating any fractional seconds. + /// + /// This is a convenience routine for [`DateTimePrinter::print_timestamp`] + /// with a `String`. + /// + /// # Errors + /// + /// This returns an error if the year corresponding to this + /// timestamp cannot be represented in the RFC 2822 format. For example, a + /// negative year. + /// + /// # Example + /// + /// ``` + /// use jiff::{fmt::rfc2822::DateTimePrinter, Timestamp}; + /// + /// let timestamp = Timestamp::from_second(1) + /// .expect("one second after Unix epoch is always valid"); + /// assert_eq!( + /// DateTimePrinter::new().timestamp_to_string(×tamp)?, + /// "Thu, 1 Jan 1970 00:00:01 -0000", + /// ); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn timestamp_to_string( + &self, + timestamp: &Timestamp, + ) -> Result { + let mut buf = String::with_capacity(4); + self.print_timestamp(timestamp, &mut buf)?; + Ok(buf) + } + /// Print a `Zoned` datetime to the given writer. /// /// This never emits `-0000` as the offset in the RFC 2822 format. If you From f9105bf6d06e548ad3a8da9b61087db3dcd4df7f Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Sat, 31 Aug 2024 14:08:09 -0400 Subject: [PATCH 5/8] timestamp: add Timestamp::display_with_offset This provides a more convenient and more easily discoverable interface to `jiff::fmt::temporal::DateTimePrinter::print_timestamp_with_offset`. --- src/lib.rs | 4 +- src/timestamp.rs | 110 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 109 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index a1d19b0..6493a4f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -688,8 +688,8 @@ pub use crate::{ SpanTotal, ToSpan, Unit, }, timestamp::{ - Timestamp, TimestampArithmetic, TimestampDifference, TimestampRound, - TimestampSeries, + Timestamp, TimestampArithmetic, TimestampDifference, + TimestampDisplayWithOffset, TimestampRound, TimestampSeries, }, util::round::mode::RoundMode, zoned::{Zoned, ZonedArithmetic, ZonedDifference, ZonedRound, ZonedWith}, diff --git a/src/timestamp.rs b/src/timestamp.rs index 922a8d0..5df3d15 100644 --- a/src/timestamp.rs +++ b/src/timestamp.rs @@ -7,7 +7,7 @@ use crate::{ self, temporal::{self, DEFAULT_DATETIME_PARSER}, }, - tz::TimeZone, + tz::{Offset, TimeZone}, util::{ rangeint::{RFrom, RInto}, round::increment, @@ -1874,7 +1874,7 @@ impl Timestamp { } } -/// Parsing and formatting using a "printf"-style API. +/// Parsing and formatting APIs. impl Timestamp { /// Parses a timestamp (expressed as broken down time) in `input` matching /// the given `format`. @@ -1952,6 +1952,40 @@ impl Timestamp { ) -> fmt::strtime::Display<'f> { fmt::strtime::Display { fmt: format.as_ref(), tm: (*self).into() } } + + /// Format a `Timestamp` datetime into a string with the given offset. + /// + /// This will format to an RFC 3339 compatible string with an offset. + /// + /// This will never use either `Z` (for Zulu time) or `-00:00` as an + /// offset. This is because Zulu time (and `-00:00`) mean "the time in UTC + /// is known, but the offset to local time is unknown." Since this routine + /// accepts an explicit offset, the offset is known. For example, + /// `Offset::UTC` will be formatted as `+00:00`. + /// + /// To format an RFC 3339 string in Zulu time, use the default + /// [`std::fmt::Display`] trait implementation on `Timestamp`. + /// + /// # Example + /// + /// ``` + /// use jiff::{tz, Timestamp}; + /// + /// let ts = Timestamp::from_second(1)?; + /// assert_eq!( + /// ts.display_with_offset(tz::offset(-5)).to_string(), + /// "1969-12-31T19:00:01-05:00", + /// ); + /// + /// # Ok::<(), Box>(()) + /// ``` + #[inline] + pub fn display_with_offset( + &self, + offset: Offset, + ) -> TimestampDisplayWithOffset { + TimestampDisplayWithOffset { timestamp: *self, offset } + } } /// Deprecated APIs on `Timestamp`. @@ -2335,7 +2369,14 @@ impl core::fmt::Debug for Timestamp { /// Converts a `Timestamp` datetime into a RFC 3339 compliant string. /// -/// Options currently supported: +/// Since a `Timestamp` never has an offset associated with it and is always +/// in UTC, the string emitted by this trait implementation uses `Z` for "Zulu" +/// time. The significance of Zulu time is prescribed by RFC 9557 and means +/// that "the time in UTC is known, but the offset to local time is unknown." +/// If you need to emit an RFC 3339 compliant string with a specific offset, +/// then use [`Timestamp::display_with_offset`]. +/// +/// # Forrmatting options supported /// /// * [`std::fmt::Formatter::precision`] can be set to control the precision /// of the fractional second component. @@ -2714,6 +2755,69 @@ impl quickcheck::Arbitrary for Timestamp { } } +/// A type for formatting a [`Timestamp`] with a specific offset. +/// +/// This type is created by the [`Timestamp::display_with_offset`] method. +/// +/// Like the [`std::fmt::Display`] trait implementation for `Timestamp`, this +/// always emits an RFC 3339 compliant string. Unlike `Timestamp`'s `Display` +/// trait implementation, which always uses `Z` or "Zulu" time, this always +/// uses an offfset. +/// +/// # Forrmatting options supported +/// +/// * [`std::fmt::Formatter::precision`] can be set to control the precision +/// of the fractional second component. +/// +/// # Example +/// +/// ``` +/// use jiff::{tz, Timestamp}; +/// +/// let offset = tz::offset(-5); +/// let ts = Timestamp::new(1_123_456_789, 123_000_000)?; +/// assert_eq!( +/// format!("{ts:.6}", ts = ts.display_with_offset(offset)), +/// "2005-08-07T18:19:49.123000-05:00", +/// ); +/// // Precision values greater than 9 are clamped to 9. +/// assert_eq!( +/// format!("{ts:.300}", ts = ts.display_with_offset(offset)), +/// "2005-08-07T18:19:49.123000000-05:00", +/// ); +/// // A precision of 0 implies the entire fractional +/// // component is always truncated. +/// assert_eq!( +/// format!("{ts:.0}", ts = ts.display_with_offset(tz::Offset::UTC)), +/// "2005-08-07T23:19:49+00:00", +/// ); +/// +/// # Ok::<(), Box>(()) +/// ``` +#[derive(Clone, Copy, Debug)] +pub struct TimestampDisplayWithOffset { + timestamp: Timestamp, + offset: Offset, +} + +impl core::fmt::Display for TimestampDisplayWithOffset { + #[inline] + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + use crate::fmt::StdFmtWrite; + + let precision = + f.precision().map(|p| u8::try_from(p).unwrap_or(u8::MAX)); + temporal::DateTimePrinter::new() + .precision(precision) + .print_timestamp_with_offset( + &self.timestamp, + self.offset, + StdFmtWrite(f), + ) + .map_err(|_| core::fmt::Error) + } +} + /// An iterator over periodic timestamps, created by [`Timestamp::series`]. /// /// It is exhausted when the next value would exceed a [`Span`] or From 43185db18d21152d173b20778f3f13d8ce04d31e Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Sat, 31 Aug 2024 14:20:47 -0400 Subject: [PATCH 6/8] changelog: 0.1.12 --- CHANGELOG.md | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d8e6c4..98f9a2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,68 @@ +0.1.12 (2024-08-31) +=================== +This release introduces some new minor APIs that support formatting +`Timestamp` values as RFC 3339 strings with a specific offset. + +Previously, using the standard formatting routines that Jiff provides, it was +only possible to format a `Timestamp` using Zulu time. For example: + +```rust +use jiff::Timestamp; + +assert_eq!( + Timestamp::UNIX_EPOCH.to_string(), + "1970-01-01T00:00:00Z", +); +``` + +This is fine most use cases, but it can be useful on occasion to format +a `Timestamp` with a specific offset. While this isn't as expressive +as formatting a datetime with a time zone (e.g., with an IANA time +zone identifier), it may be useful in contexts where you just want to +"hint" at what a user's local time is. To that end, there is a new +[`Timestamp::display_with_offset`] method that makes this possible: + +```rust +use jiff::{tz, Timestamp}; + +assert_eq!( + Timestamp::UNIX_EPOCH.display_with_offset(tz::offset(-5)).to_string(), + "1969-12-31T19:00:00-05:00", +); +``` + +A corresponding API was added to `jiff::fmt::temporal::DateTimePrinter` for +lower level use. + +Moreover, this release also includes new convenience APIs on the Temporal and +RFC 2822 printer types for returning strings. For example, previously, if you +were using the RFC 2822 printer to format a `Timestamp`, you had to do this: + +```rust +use jiff::{fmt::rfc2822::DateTimePrinter, Timestamp}; + +let mut buf = String::new(); +DateTimePrinter::new().print_timestamp(&Timestamp::UNIX_EPOCH, &mut buf)?; +assert_eq!(buf, "Thu, 1 Jan 1970 00:00:00 -0000"); +``` + +But now you can just do this: + +```rust +use jiff::{fmt::rfc2822::DateTimePrinter, Timestamp}; + +assert_eq!( + DateTimePrinter::new().timestamp_to_string(&Timestamp::UNIX_EPOCH)?; + "Thu, 1 Jan 1970 00:00:00 -0000", +); +``` + +Enhancements: + +* [#122](https://github.com/BurntSushi/jiff/issues/122): +Support formatting `Timestamp` to an RFC 3339 string with a specific offset. + + 0.1.11 (2024-08-28) =================== This release includes a few small enhancements that have been requested over From 0381d01631b4ed2b65c82a664bd0fb45b5b833e5 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Sat, 31 Aug 2024 14:24:47 -0400 Subject: [PATCH 7/8] doc: add CHANGELOG to rustdoc This lets us test Rust snippets in the CHANGELOG, which is nice. And it lets us include the CHANGELOG in the rustdoc output itself, thereby hopefully increasing visibility. --- CHANGELOG.md | 15 +++++++++------ src/lib.rs | 3 +++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98f9a2b..d28e1bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +# CHANGELOG + 0.1.12 (2024-08-31) =================== This release introduces some new minor APIs that support formatting @@ -20,7 +22,7 @@ a `Timestamp` with a specific offset. While this isn't as expressive as formatting a datetime with a time zone (e.g., with an IANA time zone identifier), it may be useful in contexts where you just want to "hint" at what a user's local time is. To that end, there is a new -[`Timestamp::display_with_offset`] method that makes this possible: +`Timestamp::display_with_offset` method that makes this possible: ```rust use jiff::{tz, Timestamp}; @@ -42,7 +44,7 @@ were using the RFC 2822 printer to format a `Timestamp`, you had to do this: use jiff::{fmt::rfc2822::DateTimePrinter, Timestamp}; let mut buf = String::new(); -DateTimePrinter::new().print_timestamp(&Timestamp::UNIX_EPOCH, &mut buf)?; +DateTimePrinter::new().print_timestamp(&Timestamp::UNIX_EPOCH, &mut buf).unwrap(); assert_eq!(buf, "Thu, 1 Jan 1970 00:00:00 -0000"); ``` @@ -52,7 +54,7 @@ But now you can just do this: use jiff::{fmt::rfc2822::DateTimePrinter, Timestamp}; assert_eq!( - DateTimePrinter::new().timestamp_to_string(&Timestamp::UNIX_EPOCH)?; + DateTimePrinter::new().timestamp_to_string(&Timestamp::UNIX_EPOCH).unwrap(), "Thu, 1 Jan 1970 00:00:00 -0000", ); ``` @@ -75,15 +77,16 @@ or nanoseconds. For example: ```rust use jiff::Timestamp; +#[derive(serde::Serialize, serde::Deserialize)] struct Record { #[serde(with = "jiff::fmt::serde::timestamp::second::required")] timestamp: Timestamp, } let json = r#"{"timestamp":1517644800}"#; -let got: Record = serde_json::from_str(&json)?; -assert_eq!(got.timestamp, Timestamp::from_second(1517644800)?); -assert_eq!(serde_json::to_string(&got)?, json); +let got: Record = serde_json::from_str(&json).unwrap(); +assert_eq!(got.timestamp, Timestamp::from_second(1517644800).unwrap()); +assert_eq!(serde_json::to_string(&got).unwrap(), json); ``` If you need to support optional timestamps via `Option`, then use diff --git a/src/lib.rs b/src/lib.rs index 6493a4f..b058186 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,6 +66,7 @@ documentation: * [Comparison with other Rust datetime crates](crate::_documentation::comparison) * [The API design rationale for Jiff](crate::_documentation::design) * [Platform support](crate::_documentation::platform) +* [CHANGELOG](crate::_documentation::changelog) # Features @@ -719,6 +720,8 @@ pub mod _documentation { pub mod design {} #[doc = include_str!("../PLATFORM.md")] pub mod platform {} + #[doc = include_str!("../CHANGELOG.md")] + pub mod changelog {} } #[cfg(test)] From 18f03f7ea258c628473cc6325364d99ca072979f Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Sat, 31 Aug 2024 14:44:27 -0400 Subject: [PATCH 8/8] doc: update docs to account for the existence of SignedDuration This was an oversight. Jiff now has two duration types, so this section basically needed to be entirely rewritten. --- DESIGN.md | 121 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 69 insertions(+), 52 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index a3fa86b..82ea2db 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -263,22 +263,50 @@ Here's a list. More may be added in the future: [alt1]: https://github.com/BurntSushi/jiff/issues/63 -## Why is there one duration type instead of two? - -In large part, this design decision came from [Temporal], which also [uses -only one duration type][temporal-one-duration]. In Temporal, the duration -type is aptly named `Duration`, where as in Jiff, the duration type is -called `Span`. This is to distinguish it from the standard library type, -`std::time::Duration`, which is similar but subtly different. - -The main alternative design, found in [`java.time`] and [NodaTime], is to -have _two_ duration types: one duration type for describing calendar units -and another duration type for describing "absolute" time. The key distinction -between these types is that of uniform units. An absolute time duration only -uses units that are uniform. That is, units that always correspond to the same -amount of elapsed time, regardless of context. Conversely, a duration type -with calendar units uses non-uniform units. For example, 1 month starting from -`April 1` is shorter (30 days) than 1 month starting from `May 1` (31 days). +## Why are there two duration types? + +The two duration types provided by Jiff are `Span` and `SignedDuration`. A +`SignedDuration` is effectively identical to a `std::time::Duration`, but it's +signed instead of unsigned. A `Span` is also a duration type, but is likely +different than most other duration types you've used before. + +While a `SignedDuration` can be thought of as a single integer corresponding +to the number of nanoseconds between two points in time, a `Span` is a +collection of individual unit values that combine to represent the difference +between two point in time. Stated more concretely, while the spans `2 +hours` and `120 minutes` both correspond to the same duration of time, when +represented as a Jiff `Span`, they correspond to two distinct values in +memory. This is something that is fundamentally not expressible by a type +like `SignedDuration`, where `2 hours` and `120 minutes` are completely +indistinguishable. + +One of the key advantages of a `Span` is that it can represent units of +non-uniform length. For example, not every month has the same number of days, +but a `Span` can still represent units of months because it tracks the values +of each unit independently. For example, Jiff is smart enough to know that the +difference between `2024-03-01` and `2024-04-01` is the same number of months +as `2024-04-01` and `2024-05-01`, even though the number of days is different: + +```rust +use jiff::{civil::date, ToSpan, Unit}; + +fn main() -> anyhow::Result<()> { + let date1 = date(2024, 3, 1); + let date2 = date(2024, 4, 1); + let date3 = date(2024, 5, 1); + + // When computing durations between `Date` values, + // the spans default to days. + assert_eq!(date1.until(date2)?, 31.days()); + assert_eq!(date2.until(date3)?, 30.days()); + + // But we can request bigger units! + assert_eq!(date1.until((Unit::Month, date2))?, 1.month()); + assert_eq!(date2.until((Unit::Month, date3))?, 1.month()); + + Ok(()) +} +``` While most folks are very in tune with the fact that years and months have non-uniform length, a less obvious truth is that days themselves also have @@ -287,47 +315,36 @@ non-uniform length in the presence of time zones. For example, `2024-03-10` in time, creating a gap in time), while `2024-11-03` was 25 hours long (the region left daylight saving time, creating a fold in time). Being unaware of this corner case leads to folks assuming that "1 day" and "24 hours" are _always_ -exactly equivalent. But they aren't. - -The idea then is not just to separate durations into two distinct types, but to -provide _distinct operations_ that clearly delineate absolute time (where, -typically, days, if expressible, are always 24 hours in length) from -calendar time (where days might be shorter or longer than 24 hours). - -While I do acknowledge that delineating calendar from absolute span operations -can make some cases clearer, my belief (before Jiff's initial release) is -that a single span type that combines all units will ultimately be simpler -and less error prone. In particular, one crucial thing to note here is the -existence of the `Zoned` data type. That is, whenever you use the units "days" -with a `Zoned` type, it should always do the right thing. And if you need to -do arithmetic that ignores DST, you can use `Zoned::datetime` to get civil -time and perform DST-ignorant arithmetic (where days are always 24 hours). In -a sense, the distinct calendar and absolute durations are still captured by -Jiff's API, but are done via the datetime types instead of the span types. - -As the [Temporal GitHub issue on this topic discusses][temporal-one-duration], -there are some significant advantages to a single span type. In my own words: +exactly equivalent. But they aren't. The design of Jiff leans into this and +ensures that so long as you're using `Span` to encode a concept of days and are +doing arithmetic with it on `Zoned` values, then you can never get it wrong. +Jiff will always take time zones into account when dealing with units of days +or bigger. + +The design of `Span` comes from [Temporal], which [uses only one duration +type][temporal-one-duration]. From that issue, there are some significant +advantages to using a `Span`. In my own words: * It more closely lines up with ISO 8601 durations, which themselves combine calendar and clock units. -* A single type makes it very easy to move between `5 years 2 months` and +* With a `Span`, it is very easy to move between `5 years 2 months` and the number of hours in that same span. * Jiff's `Span` type specifically represents each unit as distinct from the -others. In contrast, most absolute duration types (like `std::time::Duration`), -are "just" a 96-bit integer number of nanoseconds. This means that, for -example, `1 hour 30 minutes` is impossible to differentiate from `90 minutes`. -But depending on the use case, you might want one or the other. Jiff's -`Span` design (copied from Temporal) enables users to express durations in -whatever units they want. And this expression can be manipulated via APIs like -`Span::round` in intuitive ways. -* APIs like `Zoned::since` would not be as simple as they are today. If we -have multiple duration types, then we'd need multiple distinct operations to -compute those durations between datetimes. Or at least, one operation that -returns each of the different types of duration. - -Ultimately, I think it is likely that Jiff will end up growing support for -[absolute durations][github-issue-duration], but likely for reasons related -to performance and better integration with standard library types. +others. In contrast, most absolute duration types (like `std::time::Duration` +and Jiff's own `SignedDuration`), are "just" a 96-bit integer number of +nanoseconds. This means that, for example, `1 hour 30 minutes` is impossible to +differentiate from `90 minutes`. But depending on the use case, you might want +one or the other. Jiff's `Span` design (copied from Temporal) enables users +to express durations in whatever units they want. And this expression can be +manipulated via APIs like `Span::round` in intuitive ways. + +A `SignedDuration` is still useful in some respects. For example, when you +need tighter integration with the standard library's `std::time::Duration` +(since a `SignedDuration` is the same, but just signed), or when you need +better performance than what `Span` gives you. In particular, since a `Span` +keeps track of the values for each individual unit, it is a much heavier type +than a `SignedDuration`. It uses up more stack space and also required more +computation to do arithmetic with it. ## Why isn't there a `TimeZone` trait?