Skip to content

Commit

Permalink
fmt/strtime: fix float formatting with precision setting
Browse files Browse the repository at this point in the history
Previously, formatting 1.123456789 seconds with %S%.3f would
result in 1.123456789, but it should be 1.123. This commit fixes
that by switching to the new fractional formatter in the previous
commit.

Fixes #73
  • Loading branch information
BurntSushi committed Aug 3, 2024
1 parent a23300a commit 185c531
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 7 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
0.1.5 (TBD)
==================
This releases fixes a bug with fixed precision fractional formatting.

Bug fixes:

* [#73](https://github.com/BurntSushi/jiff/issues/73):
Make it so `%.Nf` only formats to `N` decimal places.


0.1.4 (2024-08-01)
==================
This release includes a small improvement for `strptime` that permits
`%Y%m%d` to parse `20240730` correctly.

Enhancements:

* [#62](https://github.com/BurntSushi/jiff/issues/62):
Tweak `strptime` so that things like `%Y` aren't unceremoniously greedy.

Expand Down
45 changes: 40 additions & 5 deletions src/fmt/strtime/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
month_name_abbrev, month_name_full, weekday_name_abbrev,
weekday_name_full, BrokenDownTime, Extension, Flag,
},
util::DecimalFormatter,
util::{DecimalFormatter, FractionalFormatter},
Write, WriteExt,
},
util::{escape, parse},
Expand Down Expand Up @@ -375,14 +375,27 @@ impl<'f, 't, 'w, W: Write> Formatter<'f, 't, 'w, W> {
let subsec = self.tm.subsec.ok_or_else(|| {
err!("requires time to format subsecond nanoseconds")
})?;
// For %f, we always want to emit at least one digit. The only way we
// wouldn't is if our fractional component is zero. One exception to
// this is when the width is `0` (which looks like `%00f`), in which
// case, we emit an error. We could allow it to emit an empty string,
// but this seems very odd. And an empty string cannot be parsed by
// `%f`.
if ext.width == Some(0) {
return Err(err!("zero precision with %f is not allowed"));
}
if subsec == 0 && ext.width.is_none() {
self.wtr.write_str("0")?;
return Ok(());
}
ext.write_fractional_seconds(subsec, self.wtr)?;
Ok(())
}

/// %.f
fn fmt_dot_fractional(&mut self, ext: Extension) -> Result<(), Error> {
let Some(subsec) = self.tm.subsec else { return Ok(()) };
if subsec == 0 && ext.width.is_none() {
if subsec == 0 && ext.width.is_none() || ext.width == Some(0) {
return Ok(());
}
ext.write_str(Case::AsIs, ".", self.wtr)?;
Expand Down Expand Up @@ -519,9 +532,11 @@ impl Extension {
) -> Result<(), Error> {
let number = number.into();

let mut formatter =
DecimalFormatter::new().fractional(self.width.unwrap_or(1), 9);
wtr.write_int(&formatter, number)
let mut formatter = FractionalFormatter::new();
if let Some(precision) = self.width {
formatter = formatter.precision(precision);
}
wtr.write_fraction(&formatter, number)
}
}

Expand Down Expand Up @@ -715,6 +730,15 @@ mod tests {
insta::assert_snapshot!(f("%.6f", mk(123_000_000)), @".123000");
insta::assert_snapshot!(f("%.9f", mk(123_000_000)), @".123000000");
insta::assert_snapshot!(f("%.255f", mk(123_000_000)), @".123000000");

insta::assert_snapshot!(f("%3f", mk(123_456_789)), @"123");
insta::assert_snapshot!(f("%6f", mk(123_456_789)), @"123456");
insta::assert_snapshot!(f("%9f", mk(123_456_789)), @"123456789");

insta::assert_snapshot!(f("%.0f", mk(123_456_789)), @"");
insta::assert_snapshot!(f("%.3f", mk(123_456_789)), @".123");
insta::assert_snapshot!(f("%.6f", mk(123_456_789)), @".123456");
insta::assert_snapshot!(f("%.9f", mk(123_456_789)), @".123456789");
}

#[test]
Expand Down Expand Up @@ -770,4 +794,15 @@ mod tests {
insta::assert_snapshot!(f("%_y", date(2001, 7, 14)), @" 1");
insta::assert_snapshot!(f("%_5y", date(2001, 7, 14)), @" 1");
}

#[test]
fn err_format_subsec_nanosecond() {
let f = |fmt: &str, time: Time| format(fmt, time).unwrap_err();
let mk = |subsec| time(0, 0, 0, subsec);

insta::assert_snapshot!(
f("%00f", mk(123_456_789)),
@"strftime formatting failed: %f failed: zero precision with %f is not allowed",
);
}
}
6 changes: 4 additions & 2 deletions src/fmt/strtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,13 +196,15 @@ default, `%d` will only try to consume at most 2 digits.
The `%f` and `%.f` flags also support specifying the precision, up to
nanoseconds. For example, `%3f` and `%.3f` will both always print a fractional
second component to at least 3 decimal places. When no precision is specified,
second component to exactly 3 decimal places. When no precision is specified,
then `%f` will always emit at least one digit, even if it's zero. But `%.f`
will emit the empty string when the fractional component is zero. Otherwise, it
will include the leading `.`. For parsing, `%f` does not include the leading
dot, but `%.f` does. Note that all of the options above are still parsed for
`%f` and `%.f`, but they are all no-ops (except for the padding for `%f`, which
is instead interpreted as a precision setting).
is instead interpreted as a precision setting). When using a precision setting,
truncation is used. If you need a different rounding mode, you should use
higher level APIs like [`Timestamp::round`] or [`Zoned::round`].
# Unsupported
Expand Down

0 comments on commit 185c531

Please sign in to comment.