From f5aec79668a1c37bd75575eda5755e049f4e6f1b Mon Sep 17 00:00:00 2001 From: Josh Kaplan Date: Wed, 8 Oct 2025 17:53:52 -0400 Subject: [PATCH 1/3] Implement round-ties-to-even for Duration Debug for consistency with f64 --- library/core/src/time.rs | 28 +++++++++++++++++++++++++++- library/coretests/tests/time.rs | 10 +++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/library/core/src/time.rs b/library/core/src/time.rs index 940129d68816..0f2160338db5 100644 --- a/library/core/src/time.rs +++ b/library/core/src/time.rs @@ -1316,7 +1316,33 @@ fn fmt_decimal( // normal floating point numbers. However, we only need to do work // when rounding up. This happens if the first digit of the // remaining ones is >= 5. - let integer_part = if fractional_part > 0 && fractional_part >= divisor * 5 { + let integer_part = if fractional_part > 0 && fractional_part == divisor * 5 { + let last_digit_is_odd = if pos > 0 { + (buf[pos - 1] - b'0') % 2 == 1 + } else { + // No fractional digits - check the integer part + (integer_part % 2) == 1 + }; + + if last_digit_is_odd { + let mut rev_pos = pos; + let mut carry = true; + while carry && rev_pos > 0 { + rev_pos -= 1; + + if buf[rev_pos] < b'9' { + buf[rev_pos] += 1; + carry = false; + } else { + buf[rev_pos] = b'0'; + } + } + + if carry { integer_part.checked_add(1) } else { Some(integer_part) } + } else { + Some(integer_part) + } + } else if fractional_part > 0 && fractional_part > divisor * 5 { // Round up the number contained in the buffer. We go through // the buffer backwards and keep track of the carry. let mut rev_pos = pos; diff --git a/library/coretests/tests/time.rs b/library/coretests/tests/time.rs index fb3c50f9bde9..ff80ff680943 100644 --- a/library/coretests/tests/time.rs +++ b/library/coretests/tests/time.rs @@ -439,7 +439,6 @@ fn debug_formatting_precision_two() { assert_eq!(format!("{:.2?}", Duration::new(4, 001_000_000)), "4.00s"); assert_eq!(format!("{:.2?}", Duration::new(2, 100_000_000)), "2.10s"); assert_eq!(format!("{:.2?}", Duration::new(2, 104_990_000)), "2.10s"); - assert_eq!(format!("{:.2?}", Duration::new(2, 105_000_000)), "2.11s"); assert_eq!(format!("{:.2?}", Duration::new(8, 999_999_999)), "9.00s"); } @@ -480,6 +479,15 @@ fn debug_formatting_precision_high() { assert_eq!(format!("{:.20?}", Duration::new(4, 001_000_000)), "4.00100000000000000000s"); } +#[test] +fn debug_formatting_round_to_even() { + assert_eq!(format!("{:.0?}", Duration::new(1, 500_000_000)), "2s"); + assert_eq!(format!("{:.0?}", Duration::new(2, 500_000_000)), "2s"); + assert_eq!(format!("{:.0?}", Duration::new(0, 1_500_000)), "2ms"); + assert_eq!(format!("{:.0?}", Duration::new(0, 2_500_000)), "2ms"); + assert_eq!(format!("{:.2?}", Duration::new(2, 105_000_000)), "2.10s"); +} + #[test] fn duration_const() { // test that the methods of `Duration` are usable in a const context From 95302b25377bf678d8db964eff20f7acd2044991 Mon Sep 17 00:00:00 2001 From: Josh Kaplan Date: Sun, 12 Oct 2025 19:09:32 -0400 Subject: [PATCH 2/3] Update comment on duration rounding behavior --- library/core/src/time.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/core/src/time.rs b/library/core/src/time.rs index 0f2160338db5..8266a2a63bc0 100644 --- a/library/core/src/time.rs +++ b/library/core/src/time.rs @@ -1315,7 +1315,8 @@ fn fmt_decimal( // need to perform rounding to match the semantics of printing // normal floating point numbers. However, we only need to do work // when rounding up. This happens if the first digit of the - // remaining ones is >= 5. + // remaining ones is > 5. When the first digit is exactly 5, rounding + // follows IEEE-754 round-ties-to-even semantics. let integer_part = if fractional_part > 0 && fractional_part == divisor * 5 { let last_digit_is_odd = if pos > 0 { (buf[pos - 1] - b'0') % 2 == 1 From c74aa910ca9ab0f4b6662600a031d57689f11fae Mon Sep 17 00:00:00 2001 From: Josh Kaplan Date: Mon, 22 Dec 2025 09:20:56 -0500 Subject: [PATCH 3/3] Merged Duration Debug tie rounding with existing rounding logic --- library/core/src/time.rs | 64 ++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 38 deletions(-) diff --git a/library/core/src/time.rs b/library/core/src/time.rs index 8266a2a63bc0..18792659d974 100644 --- a/library/core/src/time.rs +++ b/library/core/src/time.rs @@ -1315,9 +1315,12 @@ fn fmt_decimal( // need to perform rounding to match the semantics of printing // normal floating point numbers. However, we only need to do work // when rounding up. This happens if the first digit of the - // remaining ones is > 5. When the first digit is exactly 5, rounding - // follows IEEE-754 round-ties-to-even semantics. - let integer_part = if fractional_part > 0 && fractional_part == divisor * 5 { + // remaining ones is >= 5. When the first digit is exactly 5, rounding + // follows IEEE-754 round-ties-to-even semantics: we only round up + // if the last written digit is odd. + let integer_part = if fractional_part > 0 && fractional_part >= divisor * 5 { + // For ties (fractional_part == divisor * 5), only round up if last digit is odd + let is_tie = fractional_part == divisor * 5; let last_digit_is_odd = if pos > 0 { (buf[pos - 1] - b'0') % 2 == 1 } else { @@ -1325,12 +1328,20 @@ fn fmt_decimal( (integer_part % 2) == 1 }; - if last_digit_is_odd { + if is_tie && !last_digit_is_odd { + Some(integer_part) + } else { + // Round up the number contained in the buffer. We go through + // the buffer backwards and keep track of the carry. let mut rev_pos = pos; let mut carry = true; while carry && rev_pos > 0 { rev_pos -= 1; + // If the digit in the buffer is not '9', we just need to + // increment it and can stop then (since we don't have a + // carry anymore). Otherwise, we set it to '0' (overflow) + // and continue. if buf[rev_pos] < b'9' { buf[rev_pos] += 1; carry = false; @@ -1339,43 +1350,20 @@ fn fmt_decimal( } } - if carry { integer_part.checked_add(1) } else { Some(integer_part) } - } else { - Some(integer_part) - } - } else if fractional_part > 0 && fractional_part > divisor * 5 { - // Round up the number contained in the buffer. We go through - // the buffer backwards and keep track of the carry. - let mut rev_pos = pos; - let mut carry = true; - while carry && rev_pos > 0 { - rev_pos -= 1; - - // If the digit in the buffer is not '9', we just need to - // increment it and can stop then (since we don't have a - // carry anymore). Otherwise, we set it to '0' (overflow) - // and continue. - if buf[rev_pos] < b'9' { - buf[rev_pos] += 1; - carry = false; + // If we still have the carry bit set, that means that we set + // the whole buffer to '0's and need to increment the integer + // part. + if carry { + // If `integer_part == u64::MAX` and precision < 9, any + // carry of the overflow during rounding of the + // `fractional_part` into the `integer_part` will cause the + // `integer_part` itself to overflow. Avoid this by using an + // `Option`, with `None` representing `u64::MAX + 1`. + integer_part.checked_add(1) } else { - buf[rev_pos] = b'0'; + Some(integer_part) } } - - // If we still have the carry bit set, that means that we set - // the whole buffer to '0's and need to increment the integer - // part. - if carry { - // If `integer_part == u64::MAX` and precision < 9, any - // carry of the overflow during rounding of the - // `fractional_part` into the `integer_part` will cause the - // `integer_part` itself to overflow. Avoid this by using an - // `Option`, with `None` representing `u64::MAX + 1`. - integer_part.checked_add(1) - } else { - Some(integer_part) - } } else { Some(integer_part) };