Skip to content

Commit e61cf2e

Browse files
eamonnmcmanusGoogle Java Core Libraries
authored andcommitted
Fix a rounding bug in UnsignedLong.doubleValue() and .floatValue().
See the bug report for a detailed analysis. Fixes #5375. Thanks to @harpocrates (Alex Theriault) for the bug report and suggested fix. RELNOTES=Fixed a rounding bug in `UnsignedLong.doubleValue()`. PiperOrigin-RevId: 368332108
1 parent 1054847 commit e61cf2e

File tree

4 files changed

+70
-26
lines changed

4 files changed

+70
-26
lines changed

android/guava-tests/test/com/google/common/primitives/UnsignedLongTest.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,23 @@ public class UnsignedLongTest extends TestCase {
3636
static {
3737
ImmutableSet.Builder<Long> testLongsBuilder = ImmutableSet.builder();
3838
ImmutableSet.Builder<BigInteger> testBigIntegersBuilder = ImmutableSet.builder();
39+
40+
// The values here look like 111...11101...010 in binary, where the initial 111...1110 takes
41+
// up exactly as many bits as can be represented in the significand (24 for float, 53 for
42+
// double). That final 0 should be rounded up to 1 because the remaining bits make that number
43+
// slightly nearer.
44+
long floatConversionTest = 0xfffffe8000000002L;
45+
long doubleConversionTest = 0xfffffffffffff402L;
46+
3947
for (long i = -3; i <= 3; i++) {
4048
testLongsBuilder
4149
.add(i)
4250
.add(Long.MAX_VALUE + i)
4351
.add(Long.MIN_VALUE + i)
4452
.add(Integer.MIN_VALUE + i)
45-
.add(Integer.MAX_VALUE + i);
53+
.add(Integer.MAX_VALUE + i)
54+
.add(floatConversionTest + i)
55+
.add(doubleConversionTest + i);
4656
BigInteger bigI = BigInteger.valueOf(i);
4757
testBigIntegersBuilder
4858
.add(bigI)
@@ -130,17 +140,26 @@ public void testToStringRadixQuick() {
130140
}
131141
}
132142

143+
@AndroidIncompatible // b/28251030, re-enable when the fix is everywhere we run this test
133144
public void testFloatValue() {
134145
for (long value : TEST_LONGS) {
135146
UnsignedLong unsignedValue = UnsignedLong.fromLongBits(value);
136-
assertEquals(unsignedValue.bigIntegerValue().floatValue(), unsignedValue.floatValue());
147+
assertEquals(
148+
"Float value of " + unsignedValue,
149+
unsignedValue.bigIntegerValue().floatValue(),
150+
unsignedValue.floatValue(),
151+
0.0f);
137152
}
138153
}
139154

140155
public void testDoubleValue() {
141156
for (long value : TEST_LONGS) {
142157
UnsignedLong unsignedValue = UnsignedLong.fromLongBits(value);
143-
assertEquals(unsignedValue.bigIntegerValue().doubleValue(), unsignedValue.doubleValue());
158+
assertEquals(
159+
"Double value of " + unsignedValue,
160+
unsignedValue.bigIntegerValue().doubleValue(),
161+
unsignedValue.doubleValue(),
162+
0.0);
144163
}
145164
}
146165

android/guava/src/com/google/common/primitives/UnsignedLong.java

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -195,12 +195,12 @@ public long longValue() {
195195
*/
196196
@Override
197197
public float floatValue() {
198-
@SuppressWarnings("cast")
199-
float fValue = (float) (value & UNSIGNED_MASK);
200-
if (value < 0) {
201-
fValue += 0x1.0p63f;
198+
if (value >= 0) {
199+
return (float) value;
202200
}
203-
return fValue;
201+
// The top bit is set, which means that the float value is going to come from the top 24 bits.
202+
// So we can ignore the bottom 8, except for rounding. See doubleValue() for more.
203+
return (float) ((value >>> 1) | (value & 1)) * 2f;
204204
}
205205

206206
/**
@@ -209,12 +209,15 @@ public float floatValue() {
209209
*/
210210
@Override
211211
public double doubleValue() {
212-
@SuppressWarnings("cast")
213-
double dValue = (double) (value & UNSIGNED_MASK);
214-
if (value < 0) {
215-
dValue += 0x1.0p63;
212+
if (value >= 0) {
213+
return (double) value;
216214
}
217-
return dValue;
215+
// The top bit is set, which means that the double value is going to come from the top 53 bits.
216+
// So we can ignore the bottom 11, except for rounding. We can unsigned-shift right 1, aka
217+
// unsigned-divide by 2, and convert that. Then we'll get exactly half of the desired double
218+
// value. But in the specific case where the bottom two bits of the original number are 01, we
219+
// want to replace that with 1 in the shifted value for correct rounding.
220+
return (double) ((value >>> 1) | (value & 1)) * 2.0;
218221
}
219222

220223
/** Returns the value of this {@code UnsignedLong} as a {@link BigInteger}. */

guava-tests/test/com/google/common/primitives/UnsignedLongTest.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,23 @@ public class UnsignedLongTest extends TestCase {
3636
static {
3737
ImmutableSet.Builder<Long> testLongsBuilder = ImmutableSet.builder();
3838
ImmutableSet.Builder<BigInteger> testBigIntegersBuilder = ImmutableSet.builder();
39+
40+
// The values here look like 111...11101...010 in binary, where the initial 111...1110 takes
41+
// up exactly as many bits as can be represented in the significand (24 for float, 53 for
42+
// double). That final 0 should be rounded up to 1 because the remaining bits make that number
43+
// slightly nearer.
44+
long floatConversionTest = 0xfffffe8000000002L;
45+
long doubleConversionTest = 0xfffffffffffff402L;
46+
3947
for (long i = -3; i <= 3; i++) {
4048
testLongsBuilder
4149
.add(i)
4250
.add(Long.MAX_VALUE + i)
4351
.add(Long.MIN_VALUE + i)
4452
.add(Integer.MIN_VALUE + i)
45-
.add(Integer.MAX_VALUE + i);
53+
.add(Integer.MAX_VALUE + i)
54+
.add(floatConversionTest + i)
55+
.add(doubleConversionTest + i);
4656
BigInteger bigI = BigInteger.valueOf(i);
4757
testBigIntegersBuilder
4858
.add(bigI)
@@ -130,17 +140,26 @@ public void testToStringRadixQuick() {
130140
}
131141
}
132142

143+
@AndroidIncompatible // b/28251030, re-enable when the fix is everywhere we run this test
133144
public void testFloatValue() {
134145
for (long value : TEST_LONGS) {
135146
UnsignedLong unsignedValue = UnsignedLong.fromLongBits(value);
136-
assertEquals(unsignedValue.bigIntegerValue().floatValue(), unsignedValue.floatValue());
147+
assertEquals(
148+
"Float value of " + unsignedValue,
149+
unsignedValue.bigIntegerValue().floatValue(),
150+
unsignedValue.floatValue(),
151+
0.0f);
137152
}
138153
}
139154

140155
public void testDoubleValue() {
141156
for (long value : TEST_LONGS) {
142157
UnsignedLong unsignedValue = UnsignedLong.fromLongBits(value);
143-
assertEquals(unsignedValue.bigIntegerValue().doubleValue(), unsignedValue.doubleValue());
158+
assertEquals(
159+
"Double value of " + unsignedValue,
160+
unsignedValue.bigIntegerValue().doubleValue(),
161+
unsignedValue.doubleValue(),
162+
0.0);
144163
}
145164
}
146165

guava/src/com/google/common/primitives/UnsignedLong.java

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -195,12 +195,12 @@ public long longValue() {
195195
*/
196196
@Override
197197
public float floatValue() {
198-
@SuppressWarnings("cast")
199-
float fValue = (float) (value & UNSIGNED_MASK);
200-
if (value < 0) {
201-
fValue += 0x1.0p63f;
198+
if (value >= 0) {
199+
return (float) value;
202200
}
203-
return fValue;
201+
// The top bit is set, which means that the float value is going to come from the top 24 bits.
202+
// So we can ignore the bottom 8, except for rounding. See doubleValue() for more.
203+
return (float) ((value >>> 1) | (value & 1)) * 2f;
204204
}
205205

206206
/**
@@ -209,12 +209,15 @@ public float floatValue() {
209209
*/
210210
@Override
211211
public double doubleValue() {
212-
@SuppressWarnings("cast")
213-
double dValue = (double) (value & UNSIGNED_MASK);
214-
if (value < 0) {
215-
dValue += 0x1.0p63;
212+
if (value >= 0) {
213+
return (double) value;
216214
}
217-
return dValue;
215+
// The top bit is set, which means that the double value is going to come from the top 53 bits.
216+
// So we can ignore the bottom 11, except for rounding. We can unsigned-shift right 1, aka
217+
// unsigned-divide by 2, and convert that. Then we'll get exactly half of the desired double
218+
// value. But in the specific case where the bottom two bits of the original number are 01, we
219+
// want to replace that with 1 in the shifted value for correct rounding.
220+
return (double) ((value >>> 1) | (value & 1)) * 2.0;
218221
}
219222

220223
/** Returns the value of this {@code UnsignedLong} as a {@link BigInteger}. */

0 commit comments

Comments
 (0)