diff --git a/CHANGELOG.md b/CHANGELOG.md index 1aa4211..f3ac0d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +0.1.9 (2024-08-23) +================== +This release introduces new options for controlling the precision +of fractional seconds when printing `Zoned`, `Timestamp`, +`civil::DateTime` or `civil::Time` values. This is principally exposed +via `jiff::fmt::temporal::DateTimePrinter::precision`, but it's also +available via the standard library's formatting machinery. For example, +if `zdt` is a `jiff::Zoned`, then `format!("{zdt:.6}")` will format +it into a string with microsecond precision, even if its fractional +component is zero. + +Enhancements: + +* [#92](https://github.com/BurntSushi/jiff/issues/92): +Support setting the precision of fractional seconds when printing datetimes. + + 0.1.8 (2024-08-19) ================== This releases fixes a build error in Jiff's `alloc`-only configuration. This diff --git a/src/civil/datetime.rs b/src/civil/datetime.rs index 07931f1..252fe9d 100644 --- a/src/civil/datetime.rs +++ b/src/civil/datetime.rs @@ -6,7 +6,7 @@ use crate::{ error::{err, Error, ErrorContext}, fmt::{ self, - temporal::{DEFAULT_DATETIME_PARSER, DEFAULT_DATETIME_PRINTER}, + temporal::{self, DEFAULT_DATETIME_PARSER}, }, tz::TimeZone, util::{ @@ -2282,6 +2282,31 @@ impl Default for DateTime { } } +/// Converts a `DateTime` into a human readable datetime string. +/// +/// (This `Debug` representation currently emits the same string as the +/// `Display` representation, but this is not a guarantee.) +/// +/// Options currently supported: +/// +/// * [`std::fmt::Formatter::precision`] can be set to control the precision +/// of the fractional second component. +/// +/// # Example +/// +/// ``` +/// use jiff::civil::date; +/// +/// let dt = date(2024, 6, 15).at(7, 0, 0, 123_000_000); +/// assert_eq!(format!("{dt:.6?}"), "2024-06-15T07:00:00.123000"); +/// // Precision values greater than 9 are clamped to 9. +/// assert_eq!(format!("{dt:.300?}"), "2024-06-15T07:00:00.123000000"); +/// // A precision of 0 implies the entire fractional +/// // component is always truncated. +/// assert_eq!(format!("{dt:.0?}"), "2024-06-15T07:00:00"); +/// +/// # Ok::<(), Box>(()) +/// ``` impl core::fmt::Debug for DateTime { #[inline] fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { @@ -2289,12 +2314,37 @@ impl core::fmt::Debug for DateTime { } } +/// Converts a `DateTime` into an ISO 8601 compliant string. +/// +/// Options currently supported: +/// +/// * [`std::fmt::Formatter::precision`] can be set to control the precision +/// of the fractional second component. +/// +/// # Example +/// +/// ``` +/// use jiff::civil::date; +/// +/// let dt = date(2024, 6, 15).at(7, 0, 0, 123_000_000); +/// assert_eq!(format!("{dt:.6}"), "2024-06-15T07:00:00.123000"); +/// // Precision values greater than 9 are clamped to 9. +/// assert_eq!(format!("{dt:.300}"), "2024-06-15T07:00:00.123000000"); +/// // A precision of 0 implies the entire fractional +/// // component is always truncated. +/// assert_eq!(format!("{dt:.0}"), "2024-06-15T07:00:00"); +/// +/// # Ok::<(), Box>(()) +/// ``` impl core::fmt::Display for DateTime { #[inline] fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { use crate::fmt::StdFmtWrite; - DEFAULT_DATETIME_PRINTER + let precision = + f.precision().map(|p| u8::try_from(p).unwrap_or(u8::MAX)); + temporal::DateTimePrinter::new() + .precision(precision) .print_datetime(self, StdFmtWrite(f)) .map_err(|_| core::fmt::Error) } diff --git a/src/civil/time.rs b/src/civil/time.rs index 95d7b6f..3cceff9 100644 --- a/src/civil/time.rs +++ b/src/civil/time.rs @@ -6,7 +6,7 @@ use crate::{ error::{err, Error, ErrorContext}, fmt::{ self, - temporal::{DEFAULT_DATETIME_PARSER, DEFAULT_DATETIME_PRINTER}, + temporal::{self, DEFAULT_DATETIME_PARSER}, }, util::{ rangeint::{RFrom, RInto, TryRFrom}, @@ -1757,6 +1757,31 @@ impl Default for Time { } } +/// Converts a `Time` into a human readable time string. +/// +/// (This `Debug` representation currently emits the same string as the +/// `Display` representation, but this is not a guarantee.) +/// +/// Options currently supported: +/// +/// * [`std::fmt::Formatter::precision`] can be set to control the precision +/// of the fractional second component. +/// +/// # Example +/// +/// ``` +/// use jiff::civil::time; +/// +/// let t = time(7, 0, 0, 123_000_000); +/// assert_eq!(format!("{t:.6?}"), "07:00:00.123000"); +/// // Precision values greater than 9 are clamped to 9. +/// assert_eq!(format!("{t:.300?}"), "07:00:00.123000000"); +/// // A precision of 0 implies the entire fractional +/// // component is always truncated. +/// assert_eq!(format!("{t:.0?}"), "07:00:00"); +/// +/// # Ok::<(), Box>(()) +/// ``` impl core::fmt::Debug for Time { #[inline] fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { @@ -1764,12 +1789,37 @@ impl core::fmt::Debug for Time { } } +/// Converts a `Time` into an ISO 8601 compliant string. +/// +/// Options currently supported: +/// +/// * [`std::fmt::Formatter::precision`] can be set to control the precision +/// of the fractional second component. +/// +/// # Example +/// +/// ``` +/// use jiff::civil::time; +/// +/// let t = time(7, 0, 0, 123_000_000); +/// assert_eq!(format!("{t:.6}"), "07:00:00.123000"); +/// // Precision values greater than 9 are clamped to 9. +/// assert_eq!(format!("{t:.300}"), "07:00:00.123000000"); +/// // A precision of 0 implies the entire fractional +/// // component is always truncated. +/// assert_eq!(format!("{t:.0}"), "07:00:00"); +/// +/// # Ok::<(), Box>(()) +/// ``` impl core::fmt::Display for Time { #[inline] fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { use crate::fmt::StdFmtWrite; - DEFAULT_DATETIME_PRINTER + let precision = + f.precision().map(|p| u8::try_from(p).unwrap_or(u8::MAX)); + temporal::DateTimePrinter::new() + .precision(precision) .print_time(self, StdFmtWrite(f)) .map_err(|_| core::fmt::Error) } diff --git a/src/fmt/temporal/mod.rs b/src/fmt/temporal/mod.rs index b30d2d3..59552dd 100644 --- a/src/fmt/temporal/mod.rs +++ b/src/fmt/temporal/mod.rs @@ -885,6 +885,70 @@ impl DateTimePrinter { self } + /// Set the precision to use for formatting the fractional second component + /// of a time. + /// + /// The default is `None`, which will automatically set the precision based + /// on the value. + /// + /// When the precision is set to `N`, you'll always get precisely `N` + /// digits after a decimal point (unless `N==0`, then no fractional + /// component is printed), even if they are `0`. + /// + /// # Example + /// + /// ``` + /// use jiff::{civil::date, fmt::temporal::DateTimePrinter}; + /// + /// const PRINTER: DateTimePrinter = + /// DateTimePrinter::new().precision(Some(3)); + /// + /// let zdt = date(2024, 6, 15).at(7, 0, 0, 123_456_789).intz("US/Eastern")?; + /// + /// let mut buf = String::new(); + /// // Printing to a `String` can never fail. + /// PRINTER.print_zoned(&zdt, &mut buf).unwrap(); + /// assert_eq!(buf, "2024-06-15T07:00:00.123-04:00[US/Eastern]"); + /// + /// # Ok::<(), Box>(()) + /// ``` + /// + /// # Example: available via formatting machinery + /// + /// When formatting datetime types that may contain a fractional second + /// component, this can be set via Rust's formatting DSL. Specifically, + /// it corresponds to the [`std::fmt::Formatter::precision`] setting. + /// + /// ``` + /// use jiff::civil::date; + /// + /// let zdt = date(2024, 6, 15).at(7, 0, 0, 123_000_000).intz("US/Eastern")?; + /// assert_eq!( + /// format!("{zdt:.6}"), + /// "2024-06-15T07:00:00.123000-04:00[US/Eastern]", + /// ); + /// // Precision values greater than 9 are clamped to 9. + /// assert_eq!( + /// format!("{zdt:.300}"), + /// "2024-06-15T07:00:00.123000000-04:00[US/Eastern]", + /// ); + /// // A precision of 0 implies the entire fractional + /// // component is always truncated. + /// assert_eq!( + /// format!("{zdt:.0}"), + /// "2024-06-15T07:00:00-04:00[US/Eastern]", + /// ); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub const fn precision( + mut self, + precision: Option, + ) -> DateTimePrinter { + self.p = self.p.precision(precision); + self + } + /// Print a `Zoned` datetime to the given writer. /// /// # Errors diff --git a/src/fmt/temporal/printer.rs b/src/fmt/temporal/printer.rs index 76fa0ca..0822256 100644 --- a/src/fmt/temporal/printer.rs +++ b/src/fmt/temporal/printer.rs @@ -16,11 +16,17 @@ pub(super) struct DateTimePrinter { lowercase: bool, separator: u8, rfc9557: bool, + precision: Option, } impl DateTimePrinter { pub(super) const fn new() -> DateTimePrinter { - DateTimePrinter { lowercase: false, separator: b'T', rfc9557: true } + DateTimePrinter { + lowercase: false, + separator: b'T', + rfc9557: true, + precision: None, + } } pub(super) const fn lowercase(self, yes: bool) -> DateTimePrinter { @@ -32,6 +38,13 @@ impl DateTimePrinter { DateTimePrinter { separator: ascii_char, ..self } } + pub(super) const fn precision( + self, + precision: Option, + ) -> DateTimePrinter { + DateTimePrinter { precision, ..self } + } + pub(super) fn print_zoned( &self, zdt: &Zoned, @@ -113,7 +126,15 @@ impl DateTimePrinter { wtr.write_str(":")?; wtr.write_int(&FMT_TWO, time.second())?; let fractional_nanosecond = time.subsec_nanosecond(); - if fractional_nanosecond != 0 { + if let Some(precision) = self.precision { + if precision > 0 { + wtr.write_str(".")?; + wtr.write_fraction( + &FMT_FRACTION.precision(precision), + fractional_nanosecond, + )?; + } + } else if fractional_nanosecond != 0 { wtr.write_str(".")?; wtr.write_fraction(&FMT_FRACTION, fractional_nanosecond)?; } diff --git a/src/timestamp.rs b/src/timestamp.rs index adf8308..922a8d0 100644 --- a/src/timestamp.rs +++ b/src/timestamp.rs @@ -5,7 +5,7 @@ use crate::{ error::{err, Error, ErrorContext}, fmt::{ self, - temporal::{DEFAULT_DATETIME_PARSER, DEFAULT_DATETIME_PRINTER}, + temporal::{self, DEFAULT_DATETIME_PARSER}, }, tz::TimeZone, util::{ @@ -2292,6 +2292,40 @@ impl Default for Timestamp { } } +/// Converts a `Timestamp` datetime into a human readable datetime string. +/// +/// (This `Debug` representation currently emits the same string as the +/// `Display` representation, but this is not a guarantee.) +/// +/// Options currently supported: +/// +/// * [`std::fmt::Formatter::precision`] can be set to control the precision +/// of the fractional second component. +/// +/// # Example +/// +/// ``` +/// use jiff::Timestamp; +/// +/// let ts = Timestamp::new(1_123_456_789, 123_000_000)?; +/// assert_eq!( +/// format!("{ts:.6?}"), +/// "2005-08-07T23:19:49.123000Z", +/// ); +/// // Precision values greater than 9 are clamped to 9. +/// assert_eq!( +/// format!("{ts:.300?}"), +/// "2005-08-07T23:19:49.123000000Z", +/// ); +/// // A precision of 0 implies the entire fractional +/// // component is always truncated. +/// assert_eq!( +/// format!("{ts:.0?}"), +/// "2005-08-07T23:19:49Z", +/// ); +/// +/// # Ok::<(), Box>(()) +/// ``` impl core::fmt::Debug for Timestamp { #[inline] fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { @@ -2299,12 +2333,46 @@ impl core::fmt::Debug for Timestamp { } } +/// Converts a `Timestamp` datetime into a RFC 3339 compliant string. +/// +/// Options currently supported: +/// +/// * [`std::fmt::Formatter::precision`] can be set to control the precision +/// of the fractional second component. +/// +/// # Example +/// +/// ``` +/// use jiff::Timestamp; +/// +/// let ts = Timestamp::new(1_123_456_789, 123_000_000)?; +/// assert_eq!( +/// format!("{ts:.6}"), +/// "2005-08-07T23:19:49.123000Z", +/// ); +/// // Precision values greater than 9 are clamped to 9. +/// assert_eq!( +/// format!("{ts:.300}"), +/// "2005-08-07T23:19:49.123000000Z", +/// ); +/// // A precision of 0 implies the entire fractional +/// // component is always truncated. +/// assert_eq!( +/// format!("{ts:.0}"), +/// "2005-08-07T23:19:49Z", +/// ); +/// +/// # Ok::<(), Box>(()) +/// ``` impl core::fmt::Display for Timestamp { #[inline] fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { use crate::fmt::StdFmtWrite; - DEFAULT_DATETIME_PRINTER + let precision = + f.precision().map(|p| u8::try_from(p).unwrap_or(u8::MAX)); + temporal::DateTimePrinter::new() + .precision(precision) .print_timestamp(self, StdFmtWrite(f)) .map_err(|_| core::fmt::Error) } diff --git a/src/zoned.rs b/src/zoned.rs index a2db4e8..c907a12 100644 --- a/src/zoned.rs +++ b/src/zoned.rs @@ -6,7 +6,7 @@ use crate::{ error::{err, Error, ErrorContext}, fmt::{ self, - temporal::{DEFAULT_DATETIME_PARSER, DEFAULT_DATETIME_PRINTER}, + temporal::{self, DEFAULT_DATETIME_PARSER}, }, tz::{AmbiguousOffset, Disambiguation, Offset, OffsetConflict, TimeZone}, util::{ @@ -2979,17 +2979,85 @@ impl Default for Zoned { } } +/// Converts a `Zoned` datetime into a human readable datetime string. +/// +/// (This `Debug` representation currently emits the same string as the +/// `Display` representation, but this is not a guarantee.) +/// +/// Options currently supported: +/// +/// * [`std::fmt::Formatter::precision`] can be set to control the precision +/// of the fractional second component. +/// +/// # Example +/// +/// ``` +/// use jiff::civil::date; +/// +/// let zdt = date(2024, 6, 15).at(7, 0, 0, 123_000_000).intz("US/Eastern")?; +/// assert_eq!( +/// format!("{zdt:.6?}"), +/// "2024-06-15T07:00:00.123000-04:00[US/Eastern]", +/// ); +/// // Precision values greater than 9 are clamped to 9. +/// assert_eq!( +/// format!("{zdt:.300?}"), +/// "2024-06-15T07:00:00.123000000-04:00[US/Eastern]", +/// ); +/// // A precision of 0 implies the entire fractional +/// // component is always truncated. +/// assert_eq!( +/// format!("{zdt:.0?}"), +/// "2024-06-15T07:00:00-04:00[US/Eastern]", +/// ); +/// +/// # Ok::<(), Box>(()) +/// ``` impl core::fmt::Debug for Zoned { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { core::fmt::Display::fmt(self, f) } } +/// Converts a `Zoned` datetime into a RFC 9557 compliant string. +/// +/// Options currently supported: +/// +/// * [`std::fmt::Formatter::precision`] can be set to control the precision +/// of the fractional second component. +/// +/// # Example +/// +/// ``` +/// use jiff::civil::date; +/// +/// let zdt = date(2024, 6, 15).at(7, 0, 0, 123_000_000).intz("US/Eastern")?; +/// assert_eq!( +/// format!("{zdt:.6}"), +/// "2024-06-15T07:00:00.123000-04:00[US/Eastern]", +/// ); +/// // Precision values greater than 9 are clamped to 9. +/// assert_eq!( +/// format!("{zdt:.300}"), +/// "2024-06-15T07:00:00.123000000-04:00[US/Eastern]", +/// ); +/// // A precision of 0 implies the entire fractional +/// // component is always truncated. +/// assert_eq!( +/// format!("{zdt:.0}"), +/// "2024-06-15T07:00:00-04:00[US/Eastern]", +/// ); +/// +/// # Ok::<(), Box>(()) +/// ``` impl core::fmt::Display for Zoned { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { use crate::fmt::StdFmtWrite; - DEFAULT_DATETIME_PRINTER + let precision = + f.precision().map(|p| u8::try_from(p).unwrap_or(u8::MAX)); + temporal::DateTimePrinter::new() + .precision(precision) .print_zoned(self, StdFmtWrite(f)) .map_err(|_| core::fmt::Error) }