Skip to content

Commit

Permalink
api: add options for controlling precision
Browse files Browse the repository at this point in the history
This PR 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.

This is useful when one wants a datetime to use a "fixed width" format.
Or at least, as close to one as possible. For `Zoned` in particular, a
fixed width format is somewhat difficult to accomplish because of the
variable length IANA time zone identifier. But if the time zone
identifier is the same for all `Zoned` values in a particular context,
then setting the precision will provide fixed width. (Unless the years
are negative.)

Closes #92
  • Loading branch information
BurntSushi committed Aug 23, 2024
1 parent 8839f9c commit 1290ff3
Show file tree
Hide file tree
Showing 7 changed files with 348 additions and 10 deletions.
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
54 changes: 52 additions & 2 deletions src/civil/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -2282,19 +2282,69 @@ 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<dyn std::error::Error>>(())
/// ```
impl core::fmt::Debug for DateTime {
#[inline]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
core::fmt::Display::fmt(self, f)
}
}

/// 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<dyn std::error::Error>>(())
/// ```
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)
}
Expand Down
54 changes: 52 additions & 2 deletions src/civil/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -1757,19 +1757,69 @@ 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<dyn std::error::Error>>(())
/// ```
impl core::fmt::Debug for Time {
#[inline]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
core::fmt::Display::fmt(self, f)
}
}

/// 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<dyn std::error::Error>>(())
/// ```
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)
}
Expand Down
64 changes: 64 additions & 0 deletions src/fmt/temporal/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn std::error::Error>>(())
/// ```
///
/// # 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<dyn std::error::Error>>(())
/// ```
pub const fn precision(
mut self,
precision: Option<u8>,
) -> DateTimePrinter {
self.p = self.p.precision(precision);
self
}

/// Print a `Zoned` datetime to the given writer.
///
/// # Errors
Expand Down
25 changes: 23 additions & 2 deletions src/fmt/temporal/printer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,17 @@ pub(super) struct DateTimePrinter {
lowercase: bool,
separator: u8,
rfc9557: bool,
precision: Option<u8>,
}

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 {
Expand All @@ -32,6 +38,13 @@ impl DateTimePrinter {
DateTimePrinter { separator: ascii_char, ..self }
}

pub(super) const fn precision(
self,
precision: Option<u8>,
) -> DateTimePrinter {
DateTimePrinter { precision, ..self }
}

pub(super) fn print_zoned<W: Write>(
&self,
zdt: &Zoned,
Expand Down Expand Up @@ -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)?;
}
Expand Down
72 changes: 70 additions & 2 deletions src/timestamp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -2292,19 +2292,87 @@ 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<dyn std::error::Error>>(())
/// ```
impl core::fmt::Debug for Timestamp {
#[inline]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
core::fmt::Display::fmt(self, f)
}
}

/// 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<dyn std::error::Error>>(())
/// ```
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)
}
Expand Down
Loading

0 comments on commit 1290ff3

Please sign in to comment.