Skip to content

Commit 9dbf8d6

Browse files
Merge #271
271: Implement ties-to-even for Big[U]Int::to_{f32,f64} r=cuviper a=dramforever This makes the rounding consistent with primitive types, which, per IEEE-754, uses nearest-ties-to-even rounding. See: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=a6ab9b18a4982f881cd594061f096f8c ```rust #[test] fn test() { use num::{BigInt, ToPrimitive}; let a: i128 = (1 << 120) + (1 << (120 - 53)); let b: i128 = 1 << (120 - 60); assert_ne!(a as f64, (a + 1) as f64); assert_eq!((a + b) as f64, (a + 1) as f64); assert_eq!(BigInt::from(a).to_f64().unwrap(), a as f64); // This works because the b is among the high 64 bits assert_eq!(BigInt::from(a + b).to_f64().unwrap(), (a + b) as f64); // This fails because 1 is just thrown away assert_eq!(BigInt::from(a + 1).to_f64().unwrap(), (a + 1) as f64); } ``` Co-authored-by: dramforever <[email protected]>
2 parents 27e4acf + 907fc19 commit 9dbf8d6

File tree

4 files changed

+68
-11
lines changed

4 files changed

+68
-11
lines changed

ci/big_quickcheck/src/lib.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
use num_bigint::{BigInt, BigUint};
1010
use num_integer::Integer;
11-
use num_traits::{Num, One, Signed, Zero};
11+
use num_traits::{Num, One, Signed, ToPrimitive, Zero};
1212
use quickcheck::{Gen, QuickCheck, TestResult};
1313
use quickcheck_macros::quickcheck;
1414

@@ -357,3 +357,20 @@ fn quickcheck_modpow() {
357357

358358
qc.quickcheck(test_modpow as fn(i128, u128, i128) -> TestResult);
359359
}
360+
361+
#[test]
362+
fn quickcheck_to_float_equals_i128_cast() {
363+
let gen = Gen::new(usize::max_value());
364+
let mut qc = QuickCheck::new().gen(gen).tests(1_000_000);
365+
366+
fn to_f32_equals_i128_cast(value: i128) -> bool {
367+
BigInt::from(value).to_f32() == Some(value as f32)
368+
}
369+
370+
fn to_f64_equals_i128_cast(value: i128) -> bool {
371+
BigInt::from(value).to_f64() == Some(value as f64)
372+
}
373+
374+
qc.quickcheck(to_f32_equals_i128_cast as fn(i128) -> bool);
375+
qc.quickcheck(to_f64_equals_i128_cast as fn(i128) -> bool);
376+
}

src/biguint/convert.rs

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -287,19 +287,31 @@ fn high_bits_to_u64(v: &BigUint) -> u64 {
287287
let digit_bits = (bits - 1) % u64::from(big_digit::BITS) + 1;
288288
let bits_want = Ord::min(64 - ret_bits, digit_bits);
289289

290-
if bits_want != 64 {
291-
ret <<= bits_want;
290+
if bits_want != 0 {
291+
if bits_want != 64 {
292+
ret <<= bits_want;
293+
}
294+
// XXX Conversion is useless if already 64-bit.
295+
#[allow(clippy::useless_conversion)]
296+
let d0 = u64::from(*d) >> (digit_bits - bits_want);
297+
ret |= d0;
292298
}
293-
// XXX Conversion is useless if already 64-bit.
294-
#[allow(clippy::useless_conversion)]
295-
let d0 = u64::from(*d) >> (digit_bits - bits_want);
296-
ret |= d0;
297-
ret_bits += bits_want;
298-
bits -= bits_want;
299299

300-
if ret_bits == 64 {
301-
break;
300+
// Implement round-to-odd: If any lower bits are 1, set LSB to 1
301+
// so that rounding again to floating point value using
302+
// nearest-ties-to-even is correct.
303+
//
304+
// See: https://en.wikipedia.org/wiki/Rounding#Rounding_to_prepare_for_shorter_precision
305+
306+
if digit_bits - bits_want != 0 {
307+
// XXX Conversion is useless if already 64-bit.
308+
#[allow(clippy::useless_conversion)]
309+
let masked = u64::from(*d) << (64 - (digit_bits - bits_want) as u32);
310+
ret |= (masked != 0) as u64;
302311
}
312+
313+
ret_bits += bits_want;
314+
bits -= bits_want;
303315
}
304316

305317
ret

tests/bigint.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,13 @@ fn test_convert_f32() {
415415
b <<= 1;
416416
}
417417

418+
// test correct ties-to-even rounding
419+
let weird: i128 = (1i128 << 100) + (1i128 << (100 - f32::MANTISSA_DIGITS));
420+
assert_ne!(weird as f32, (weird + 1) as f32);
421+
422+
assert_eq!(BigInt::from(weird).to_f32(), Some(weird as f32));
423+
assert_eq!(BigInt::from(weird + 1).to_f32(), Some((weird + 1) as f32));
424+
418425
// rounding
419426
assert_eq!(
420427
BigInt::from_f32(-f32::consts::PI),
@@ -505,6 +512,13 @@ fn test_convert_f64() {
505512
b <<= 1;
506513
}
507514

515+
// test correct ties-to-even rounding
516+
let weird: i128 = (1i128 << 100) + (1i128 << (100 - f64::MANTISSA_DIGITS));
517+
assert_ne!(weird as f64, (weird + 1) as f64);
518+
519+
assert_eq!(BigInt::from(weird).to_f64(), Some(weird as f64));
520+
assert_eq!(BigInt::from(weird + 1).to_f64(), Some((weird + 1) as f64));
521+
508522
// rounding
509523
assert_eq!(
510524
BigInt::from_f64(-f64::consts::PI),

tests/biguint.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,13 @@ fn test_convert_f32() {
646646
b <<= 1;
647647
}
648648

649+
// test correct ties-to-even rounding
650+
let weird: i128 = (1i128 << 100) + (1i128 << (100 - f32::MANTISSA_DIGITS));
651+
assert_ne!(weird as f32, (weird + 1) as f32);
652+
653+
assert_eq!(BigInt::from(weird).to_f32(), Some(weird as f32));
654+
assert_eq!(BigInt::from(weird + 1).to_f32(), Some((weird + 1) as f32));
655+
649656
// rounding
650657
assert_eq!(BigUint::from_f32(-1.0), None);
651658
assert_eq!(BigUint::from_f32(-0.99999), Some(BigUint::zero()));
@@ -722,6 +729,13 @@ fn test_convert_f64() {
722729
b <<= 1;
723730
}
724731

732+
// test correct ties-to-even rounding
733+
let weird: i128 = (1i128 << 100) + (1i128 << (100 - f64::MANTISSA_DIGITS));
734+
assert_ne!(weird as f64, (weird + 1) as f64);
735+
736+
assert_eq!(BigInt::from(weird).to_f64(), Some(weird as f64));
737+
assert_eq!(BigInt::from(weird + 1).to_f64(), Some((weird + 1) as f64));
738+
725739
// rounding
726740
assert_eq!(BigUint::from_f64(-1.0), None);
727741
assert_eq!(BigUint::from_f64(-0.99999), Some(BigUint::zero()));

0 commit comments

Comments
 (0)