Repository: time-rs/time (language: rust)

Issue:
Formatting bug on ISO8601
Hi @jhpratt continuing our discussion from #674. I have created a repo to reproduce this bug [here](https://github.com/dracarys18/reproduce_time_bug). You can try to run it and check it yourself.

```bash
cargo r | tee test.txt
cat test.txt | rg ":60."
```

This looks like this happens only when the TimePrecision is 3 here.

```rust
    const FORMAT_CONFIG: EncodedConfig = Config::DEFAULT
        .set_time_precision(TimePrecision::Second {
            decimal_digits: NonZeroU8::new(3),
        })
        .encode();
```

It looks like it's rounding off the time to 60 seconds and it's happening every minute. If I change the precision to 6 I can see it's adjusting to nano second


Discussion / hints:
Odd. I've confirmed that the code as-is does what you say, so I'll definitely look into this more.
> It looks like it's rounding off the time to 60 seconds and it's happening every minute.

Confirmed. This appears to be the behavior of `Display` for floats, and to be fair it is what 99.9% of people would expect (as it's the closest to the true value). I naïvely assumed that it would truncate, and a quick search just now reveals that the behavior isn't actually documented.

Given that I can't rely on the standard library's implementations here, I'm going to have to manually format floats. It's doable and I'll probably get to it this weekend.
> > It looks like it's rounding off the time to 60 seconds and it's happening every minute.
> 
> Confirmed. This appears to be the behavior of `Display` for floats, and to be fair it is what 99.9% of people would expect (as it's the closest to the true value). I naïvely assumed that it would truncate, and a quick search just now reveals that the behavior isn't actually documented.
> 
> Given that I can't rely on the standard library's implementations here, I'm going to have to manually format floats. It's doable and I'll probably get to it this weekend.

Sure @jhpratt, I can take this up if you just give me a place where I should start looking at.
The specific function is `format_float` in `src/formatting/mod.rs`. The `Some` arm is what is relevant.
> The specific function is `format_float` in `src/formatting/mod.rs`. The `Some` arm is what is relevant.

Sure I will take a look and create a PR for this. Thanks!
Hey @jhpratt, So this can be fixed by just truncating the float number to the precision point before we pass it to formatter. [Playground link for it](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=1bbf848d7e3c048b87dc1da92fa31c26). This would require just a one line change in `format_float` function where we would just truncate `value` before it we pass it in `write!`. Let me know if you have better suggestions, Otherwise I will create a PR for this.

Fix patch:
diff --git a/time/src/formatting/mod.rs b/time/src/formatting/mod.rs
index b57c15222..f5a6de58e 100644
--- a/time/src/formatting/mod.rs
+++ b/time/src/formatting/mod.rs
@@ -2,6 +2,7 @@
 
 pub(crate) mod formattable;
 mod iso8601;
+
 use core::num::NonZeroU8;
 use std::io;
 
@@ -73,6 +74,10 @@ pub(crate) fn format_float(
 ) -> io::Result<usize> {
     match digits_after_decimal {
         Some(digits_after_decimal) => {
+            // Truncate the decimal points up to the precision
+            let trunc_num = 10_f64.powi(digits_after_decimal.get().cast_signed().extend());
+            let value = f64::trunc(value * trunc_num) / trunc_num;
+
             let digits_after_decimal = digits_after_decimal.get().extend();
             let width = digits_before_decimal.extend::<usize>() + 1 + digits_after_decimal;
             write!(output, "{value:0>width$.digits_after_decimal$}")?;


Test patch:
diff --git a/tests/formatting.rs b/tests/formatting.rs
index e54a9f618..6cb403b74 100644
--- a/tests/formatting.rs
+++ b/tests/formatting.rs
@@ -1,4 +1,5 @@
 use std::io;
+use std::num::NonZeroU8;
 
 use time::format_description::well_known::iso8601::{DateKind, OffsetPrecision, TimePrecision};
 use time::format_description::well_known::{iso8601, Iso8601, Rfc2822, Rfc3339};
@@ -226,6 +227,26 @@ fn iso_8601() -> time::Result<()> {
     Ok(())
 }
 
+#[test]
+fn iso_8601_issue_678() -> time::Result<()> {
+    macro_rules! assert_format_config {
+        ($formatted:literal $(, $($config:tt)+)?) => {
+            assert_eq!(
+                datetime!(2021-01-02 03:04:05.999_999_999 UTC).format(
+                    &Iso8601::<{ iso8601::Config::DEFAULT$($($config)+)?.encode() }>
+                )?,
+                $formatted
+            );
+        };
+    }
+
+    assert_format_config!("2021-01-02T03:04:05.999999999Z", .set_time_precision(TimePrecision::Second { decimal_digits: NonZeroU8::new(9) }));
+    assert_format_config!("2021-01-02T03:04:05.999999Z", .set_time_precision(TimePrecision::Second { decimal_digits: NonZeroU8::new(6) }));
+    assert_format_config!("2021-01-02T03:04:05.999Z", .set_time_precision(TimePrecision::Second { decimal_digits: NonZeroU8::new(3) }));
+
+    Ok(())
+}
+
 #[test]
 fn format_time() -> time::Result<()> {
     let format_output = [