diff --git a/double-conversion/bignum-dtoa.cc b/double-conversion/bignum-dtoa.cc index 614a5186..dcbb33ff 100644 --- a/double-conversion/bignum-dtoa.cc +++ b/double-conversion/bignum-dtoa.cc @@ -76,18 +76,21 @@ static void GenerateShortestDigits(Bignum* numerator, Bignum* denominator, // Generates 'requested_digits' after the decimal point. static void BignumToFixed(int requested_digits, int* decimal_point, Bignum* numerator, Bignum* denominator, - Vector buffer, int* length); + Vector buffer, int* length, + bool round_half_to_even = false); // Generates 'count' digits of numerator/denominator. // Once 'count' digits have been produced rounds the result depending on the // remainder (remainders of exactly .5 round upwards). Might update the // decimal_point when rounding up (for example for 0.9999). static void GenerateCountedDigits(int count, int* decimal_point, Bignum* numerator, Bignum* denominator, - Vector buffer, int* length); + Vector buffer, int* length, + bool round_half_to_even = false); void BignumDtoa(double v, BignumDtoaMode mode, int requested_digits, - Vector buffer, int* length, int* decimal_point) { + Vector buffer, int* length, int* decimal_point, + bool round_half_to_even) { DOUBLE_CONVERSION_ASSERT(v > 0); DOUBLE_CONVERSION_ASSERT(!Double(v).IsSpecial()); uint64_t significand; @@ -155,12 +158,14 @@ void BignumDtoa(double v, BignumDtoaMode mode, int requested_digits, case BIGNUM_DTOA_FIXED: BignumToFixed(requested_digits, decimal_point, &numerator, &denominator, - buffer, length); + buffer, length, + round_half_to_even); break; case BIGNUM_DTOA_PRECISION: GenerateCountedDigits(requested_digits, decimal_point, &numerator, &denominator, - buffer, length); + buffer, length, + round_half_to_even); break; default: DOUBLE_CONVERSION_UNREACHABLE(); @@ -282,7 +287,8 @@ static void GenerateShortestDigits(Bignum* numerator, Bignum* denominator, // exponent (decimal_point), when rounding upwards. static void GenerateCountedDigits(int count, int* decimal_point, Bignum* numerator, Bignum* denominator, - Vector buffer, int* length) { + Vector buffer, int* length, + bool round_half_to_even) { DOUBLE_CONVERSION_ASSERT(count >= 0); if (count <= 0) { // No digits requested. The "last digit" store below would write buffer[count - 1]. @@ -302,7 +308,8 @@ static void GenerateCountedDigits(int count, int* decimal_point, // Generate the last digit. uint16_t digit; digit = numerator->DivideModuloIntBignum(*denominator); - if (Bignum::PlusCompare(*numerator, *numerator, *denominator) >= 0) { + int compare = Bignum::PlusCompare(*numerator, *numerator, *denominator); + if (compare > 0 || (compare == 0 && (round_half_to_even ? (digit % 2 != 0) : true))) { digit++; } DOUBLE_CONVERSION_ASSERT(digit <= 10); @@ -330,7 +337,8 @@ static void GenerateCountedDigits(int count, int* decimal_point, // Input verifies: 1 <= (numerator + delta) / denominator < 10. static void BignumToFixed(int requested_digits, int* decimal_point, Bignum* numerator, Bignum* denominator, - Vector buffer, int* length) { + Vector buffer, int* length, + bool round_half_to_even) { // Note that we have to look at more than just the requested_digits, since // a number could be rounded up. Example: v=0.5 with requested_digits=0. // Even though the power of v equals 0 we can't just stop here. @@ -350,9 +358,11 @@ static void BignumToFixed(int requested_digits, int* decimal_point, // Initially the fraction lies in range (1, 10]. Multiply the denominator // by 10 so that we can compare more easily. denominator->Times10(); - if (Bignum::PlusCompare(*numerator, *numerator, *denominator) >= 0) { + int compare = Bignum::PlusCompare(*numerator, *numerator, *denominator); + if (compare > 0 || (compare == 0 && !round_half_to_even)) { // If the fraction is >= 0.5 then we have to include the rounded - // digit. + // digit. In half-to-even, the preceding digit is 0 (which is even), + // so exact halfway cases round down to 0. buffer[0] = '1'; *length = 1; (*decimal_point)++; @@ -367,7 +377,8 @@ static void BignumToFixed(int requested_digits, int* decimal_point, int needed_digits = (*decimal_point) + requested_digits; GenerateCountedDigits(needed_digits, decimal_point, numerator, denominator, - buffer, length); + buffer, length, + round_half_to_even); } } diff --git a/double-conversion/bignum-dtoa.h b/double-conversion/bignum-dtoa.h index 34b96199..a8f8ab01 100644 --- a/double-conversion/bignum-dtoa.h +++ b/double-conversion/bignum-dtoa.h @@ -77,7 +77,8 @@ enum BignumDtoaMode { // 'BignumDtoa' expects the given buffer to be big enough to hold all digits // and a terminating null-character. void BignumDtoa(double v, BignumDtoaMode mode, int requested_digits, - Vector buffer, int* length, int* point); + Vector buffer, int* length, int* point, + bool round_half_to_even = false); } // namespace double_conversion diff --git a/double-conversion/double-to-string.cc b/double-conversion/double-to-string.cc index d0635432..87afbb01 100644 --- a/double-conversion/double-to-string.cc +++ b/double-conversion/double-to-string.cc @@ -187,7 +187,8 @@ bool DoubleToStringConverter::ToShortestIeeeNumber( int decimal_rep_length; DoubleToAscii(value, mode, 0, decimal_rep, kDecimalRepCapacity, - &sign, &decimal_rep_length, &decimal_point); + &sign, &decimal_rep_length, &decimal_point, + (flags_ & ROUND_HALF_TO_EVEN) != 0); bool unique_zero = (flags_ & UNIQUE_ZERO) != 0; if (sign && (value != 0.0 || !unique_zero)) { @@ -233,7 +234,8 @@ bool DoubleToStringConverter::ToFixed(double value, int decimal_rep_length; DoubleToAscii(value, FIXED, requested_digits, decimal_rep, kDecimalRepCapacity, - &sign, &decimal_rep_length, &decimal_point); + &sign, &decimal_rep_length, &decimal_point, + (flags_ & ROUND_HALF_TO_EVEN) != 0); bool unique_zero = ((flags_ & UNIQUE_ZERO) != 0); if (sign && (value != 0.0 || !unique_zero)) { @@ -274,11 +276,13 @@ bool DoubleToStringConverter::ToExponential( if (requested_digits == -1) { DoubleToAscii(value, SHORTEST, 0, decimal_rep, kDecimalRepCapacity, - &sign, &decimal_rep_length, &decimal_point); + &sign, &decimal_rep_length, &decimal_point, + (flags_ & ROUND_HALF_TO_EVEN) != 0); } else { DoubleToAscii(value, PRECISION, requested_digits + 1, decimal_rep, kDecimalRepCapacity, - &sign, &decimal_rep_length, &decimal_point); + &sign, &decimal_rep_length, &decimal_point, + (flags_ & ROUND_HALF_TO_EVEN) != 0); DOUBLE_CONVERSION_ASSERT(decimal_rep_length <= requested_digits + 1); for (int i = decimal_rep_length; i < requested_digits + 1; ++i) { @@ -322,7 +326,8 @@ bool DoubleToStringConverter::ToPrecision(double value, DoubleToAscii(value, PRECISION, precision, decimal_rep, kDecimalRepCapacity, - &sign, &decimal_rep_length, &decimal_point); + &sign, &decimal_rep_length, &decimal_point, + (flags_ & ROUND_HALF_TO_EVEN) != 0); DOUBLE_CONVERSION_ASSERT(decimal_rep_length <= precision); bool unique_zero = ((flags_ & UNIQUE_ZERO) != 0); @@ -391,7 +396,8 @@ void DoubleToStringConverter::DoubleToAscii(double v, int buffer_length, bool* sign, int* length, - int* point) { + int* point, + bool round_half_to_even) { Vector vector(buffer, buffer_length); DOUBLE_CONVERSION_ASSERT(!Double(v).IsSpecial()); DOUBLE_CONVERSION_ASSERT(mode == SHORTEST || mode == SHORTEST_SINGLE || requested_digits >= 0); @@ -423,31 +429,33 @@ void DoubleToStringConverter::DoubleToAscii(double v, return; } - bool fast_worked; - switch (mode) { - case SHORTEST: - fast_worked = FastDtoa(v, FAST_DTOA_SHORTEST, 0, vector, length, point); - break; - case SHORTEST_SINGLE: - fast_worked = FastDtoa(v, FAST_DTOA_SHORTEST_SINGLE, 0, - vector, length, point); - break; - case FIXED: - fast_worked = FastFixedDtoa(v, requested_digits, vector, length, point); - break; - case PRECISION: - fast_worked = FastDtoa(v, FAST_DTOA_PRECISION, requested_digits, - vector, length, point); - break; - default: - fast_worked = false; - DOUBLE_CONVERSION_UNREACHABLE(); + bool fast_worked = false; + if (!round_half_to_even) { + switch (mode) { + case SHORTEST: + fast_worked = FastDtoa(v, FAST_DTOA_SHORTEST, 0, vector, length, point); + break; + case SHORTEST_SINGLE: + fast_worked = FastDtoa(v, FAST_DTOA_SHORTEST_SINGLE, 0, + vector, length, point); + break; + case FIXED: + fast_worked = FastFixedDtoa(v, requested_digits, vector, length, point); + break; + case PRECISION: + fast_worked = FastDtoa(v, FAST_DTOA_PRECISION, requested_digits, + vector, length, point); + break; + default: + fast_worked = false; + DOUBLE_CONVERSION_UNREACHABLE(); + } + if (fast_worked) return; } - if (fast_worked) return; // If the fast dtoa didn't succeed use the slower bignum version. BignumDtoaMode bignum_mode = DtoaToBignumDtoaMode(mode); - BignumDtoa(v, bignum_mode, requested_digits, vector, length, point); + BignumDtoa(v, bignum_mode, requested_digits, vector, length, point, round_half_to_even); vector[*length] = '\0'; } diff --git a/double-conversion/double-to-string.h b/double-conversion/double-to-string.h index 8472c720..230b54e2 100644 --- a/double-conversion/double-to-string.h +++ b/double-conversion/double-to-string.h @@ -80,7 +80,8 @@ class DoubleToStringConverter { UNIQUE_ZERO = 8, NO_TRAILING_ZERO = 16, EMIT_TRAILING_DECIMAL_POINT_IN_EXPONENTIAL = 32, - EMIT_TRAILING_ZERO_AFTER_POINT_IN_EXPONENTIAL = 64 + EMIT_TRAILING_ZERO_AFTER_POINT_IN_EXPONENTIAL = 64, + ROUND_HALF_TO_EVEN = 128 }; // Flags should be a bit-or combination of the possible Flags-enum. @@ -106,6 +107,8 @@ class DoubleToStringConverter { // - EMIT_TRAILING_ZERO_AFTER_POINT_IN_EXPONENTIAL: in addition to a trailing // decimal point emits a trailing '0'-character. This flag requires the // EMIT_TRAILING_DECIMAL_POINT_IN_EXPONENTIAL flag. + // - ROUND_HALF_TO_EVEN: round halfway cases towards the nearest even number + // (banker's rounding) instead of away from zero. Matches printf, std::to_chars, etc. // // Infinity symbol and nan_symbol provide the string representation for these // special values. If the string is NULL and the special value is encountered @@ -453,7 +456,8 @@ class DoubleToStringConverter { int buffer_length, bool* sign, int* length, - int* point); + int* point, + bool round_half_to_even = false); private: // Implementation for ToShortest and ToShortestSingle. diff --git a/test/cctest/test-conversions.cc b/test/cctest/test-conversions.cc index 38739211..7e4e1094 100644 --- a/test/cctest/test-conversions.cc +++ b/test/cctest/test-conversions.cc @@ -6310,3 +6310,212 @@ TEST(StringToTemplate) { CHECK_EQ(processed1, processed2); } } + +TEST(RoundHalfToEven) { + const int kBufferSize = 128; + char buffer[kBufferSize]; + StringBuilder builder(buffer, kBufferSize); + + int default_flags = DoubleToStringConverter::NO_FLAGS; + DoubleToStringConverter dc_default(default_flags, "Infinity", "NaN", 'e', + -6, 21, 6, 0); + + int even_flags = DoubleToStringConverter::ROUND_HALF_TO_EVEN; + DoubleToStringConverter dc_even(even_flags, "Infinity", "NaN", 'e', + -6, 21, 6, 0); + + // ToFixed halfway cases at integer boundaries (requested_digits = 0) + // Default rounds half away from zero. + builder.Reset(); + CHECK(dc_default.ToFixed(0.5, 0, &builder)); + CHECK_EQ("1", builder.Finalize()); + + builder.Reset(); + CHECK(dc_default.ToFixed(1.5, 0, &builder)); + CHECK_EQ("2", builder.Finalize()); + + builder.Reset(); + CHECK(dc_default.ToFixed(2.5, 0, &builder)); + CHECK_EQ("3", builder.Finalize()); + + builder.Reset(); + CHECK(dc_default.ToFixed(3.5, 0, &builder)); + CHECK_EQ("4", builder.Finalize()); + + builder.Reset(); + CHECK(dc_default.ToFixed(4.5, 0, &builder)); + CHECK_EQ("5", builder.Finalize()); + + // ROUND_HALF_TO_EVEN rounds halfway cases to the nearest even number. + builder.Reset(); + CHECK(dc_even.ToFixed(0.5, 0, &builder)); + CHECK_EQ("0", builder.Finalize()); + + builder.Reset(); + CHECK(dc_even.ToFixed(1.5, 0, &builder)); + CHECK_EQ("2", builder.Finalize()); + + builder.Reset(); + CHECK(dc_even.ToFixed(2.5, 0, &builder)); + CHECK_EQ("2", builder.Finalize()); + + builder.Reset(); + CHECK(dc_even.ToFixed(3.5, 0, &builder)); + CHECK_EQ("4", builder.Finalize()); + + builder.Reset(); + CHECK(dc_even.ToFixed(4.5, 0, &builder)); + CHECK_EQ("4", builder.Finalize()); + + // Negative values + builder.Reset(); + CHECK(dc_even.ToFixed(-0.5, 0, &builder)); + CHECK_EQ("-0", builder.Finalize()); + + builder.Reset(); + CHECK(dc_even.ToFixed(-1.5, 0, &builder)); + CHECK_EQ("-2", builder.Finalize()); + + builder.Reset(); + CHECK(dc_even.ToFixed(-2.5, 0, &builder)); + CHECK_EQ("-2", builder.Finalize()); + + builder.Reset(); + CHECK(dc_even.ToFixed(-3.5, 0, &builder)); + CHECK_EQ("-4", builder.Finalize()); + + // Fractional halfway cases (dyadic fractions representable exactly in binary double) + // Issue #251 example: 0.25 -> "0.2" vs "0.3" + builder.Reset(); + CHECK(dc_default.ToFixed(0.25, 1, &builder)); + CHECK_EQ("0.3", builder.Finalize()); + + builder.Reset(); + CHECK(dc_even.ToFixed(0.25, 1, &builder)); + CHECK_EQ("0.2", builder.Finalize()); + + builder.Reset(); + CHECK(dc_default.ToFixed(0.75, 1, &builder)); + CHECK_EQ("0.8", builder.Finalize()); + + builder.Reset(); + CHECK(dc_even.ToFixed(0.75, 1, &builder)); + CHECK_EQ("0.8", builder.Finalize()); + + builder.Reset(); + CHECK(dc_default.ToFixed(1.25, 1, &builder)); + CHECK_EQ("1.3", builder.Finalize()); + + builder.Reset(); + CHECK(dc_even.ToFixed(1.25, 1, &builder)); + CHECK_EQ("1.2", builder.Finalize()); + + builder.Reset(); + CHECK(dc_default.ToFixed(1.75, 1, &builder)); + CHECK_EQ("1.8", builder.Finalize()); + + builder.Reset(); + CHECK(dc_even.ToFixed(1.75, 1, &builder)); + CHECK_EQ("1.8", builder.Finalize()); + + builder.Reset(); + CHECK(dc_default.ToFixed(2.25, 1, &builder)); + CHECK_EQ("2.3", builder.Finalize()); + + builder.Reset(); + CHECK(dc_even.ToFixed(2.25, 1, &builder)); + CHECK_EQ("2.2", builder.Finalize()); + + // 2 decimal places (multiples of 1/8 = 0.125) + builder.Reset(); + CHECK(dc_default.ToFixed(0.125, 2, &builder)); + CHECK_EQ("0.13", builder.Finalize()); + + builder.Reset(); + CHECK(dc_even.ToFixed(0.125, 2, &builder)); + CHECK_EQ("0.12", builder.Finalize()); + + builder.Reset(); + CHECK(dc_default.ToFixed(0.375, 2, &builder)); + CHECK_EQ("0.38", builder.Finalize()); + + builder.Reset(); + CHECK(dc_even.ToFixed(0.375, 2, &builder)); + CHECK_EQ("0.38", builder.Finalize()); + + builder.Reset(); + CHECK(dc_default.ToFixed(0.625, 2, &builder)); + CHECK_EQ("0.63", builder.Finalize()); + + builder.Reset(); + CHECK(dc_even.ToFixed(0.625, 2, &builder)); + CHECK_EQ("0.62", builder.Finalize()); + + builder.Reset(); + CHECK(dc_default.ToFixed(0.875, 2, &builder)); + CHECK_EQ("0.88", builder.Finalize()); + + builder.Reset(); + CHECK(dc_even.ToFixed(0.875, 2, &builder)); + CHECK_EQ("0.88", builder.Finalize()); + + // ToPrecision with ROUND_HALF_TO_EVEN + builder.Reset(); + CHECK(dc_default.ToPrecision(0.25, 1, &builder)); + CHECK_EQ("0.3", builder.Finalize()); + + builder.Reset(); + CHECK(dc_even.ToPrecision(0.25, 1, &builder)); + CHECK_EQ("0.2", builder.Finalize()); + + builder.Reset(); + CHECK(dc_default.ToPrecision(0.75, 1, &builder)); + CHECK_EQ("0.8", builder.Finalize()); + + builder.Reset(); + CHECK(dc_even.ToPrecision(0.75, 1, &builder)); + CHECK_EQ("0.8", builder.Finalize()); + + builder.Reset(); + CHECK(dc_default.ToPrecision(1.25, 2, &builder)); + CHECK_EQ("1.3", builder.Finalize()); + + builder.Reset(); + CHECK(dc_even.ToPrecision(1.25, 2, &builder)); + CHECK_EQ("1.2", builder.Finalize()); + + builder.Reset(); + CHECK(dc_default.ToPrecision(1.75, 2, &builder)); + CHECK_EQ("1.8", builder.Finalize()); + + builder.Reset(); + CHECK(dc_even.ToPrecision(1.75, 2, &builder)); + CHECK_EQ("1.8", builder.Finalize()); + + // Direct DoubleToAscii check + char ascii_buffer[32]; + bool sign; + int length; + int point; + + DoubleToStringConverter::DoubleToAscii(0.25, DoubleToStringConverter::FIXED, 1, + ascii_buffer, sizeof(ascii_buffer), + &sign, &length, &point, false); + CHECK_EQ("3", ascii_buffer); + + DoubleToStringConverter::DoubleToAscii(0.25, DoubleToStringConverter::FIXED, 1, + ascii_buffer, sizeof(ascii_buffer), + &sign, &length, &point, true); + CHECK_EQ("2", ascii_buffer); + + DoubleToStringConverter::DoubleToAscii(1.25, DoubleToStringConverter::FIXED, 1, + ascii_buffer, sizeof(ascii_buffer), + &sign, &length, &point, false); + CHECK_EQ("13", ascii_buffer); + + DoubleToStringConverter::DoubleToAscii(1.25, DoubleToStringConverter::FIXED, 1, + ascii_buffer, sizeof(ascii_buffer), + &sign, &length, &point, true); + CHECK_EQ("12", ascii_buffer); +} +