diff --git a/ci/big_quickcheck/src/lib.rs b/ci/big_quickcheck/src/lib.rs index 4db72f8f..47ba2e14 100644 --- a/ci/big_quickcheck/src/lib.rs +++ b/ci/big_quickcheck/src/lib.rs @@ -8,7 +8,7 @@ use num_bigint::{BigInt, BigUint}; use num_integer::Integer; -use num_traits::{Num, One, Signed, Zero}; +use num_traits::{Num, One, Signed, ToPrimitive, Zero}; use quickcheck::{Gen, QuickCheck, TestResult}; use quickcheck_macros::quickcheck; @@ -357,3 +357,20 @@ fn quickcheck_modpow() { qc.quickcheck(test_modpow as fn(i128, u128, i128) -> TestResult); } + +#[test] +fn quickcheck_to_float_equals_i128_cast() { + let gen = Gen::new(usize::max_value()); + let mut qc = QuickCheck::new().gen(gen).tests(1_000_000); + + fn to_f32_equals_i128_cast(value: i128) -> bool { + BigInt::from(value).to_f32() == Some(value as f32) + } + + fn to_f64_equals_i128_cast(value: i128) -> bool { + BigInt::from(value).to_f64() == Some(value as f64) + } + + qc.quickcheck(to_f32_equals_i128_cast as fn(i128) -> bool); + qc.quickcheck(to_f64_equals_i128_cast as fn(i128) -> bool); +} diff --git a/src/biguint/convert.rs b/src/biguint/convert.rs index de407fa2..e4f7f091 100644 --- a/src/biguint/convert.rs +++ b/src/biguint/convert.rs @@ -287,19 +287,31 @@ fn high_bits_to_u64(v: &BigUint) -> u64 { let digit_bits = (bits - 1) % u64::from(big_digit::BITS) + 1; let bits_want = Ord::min(64 - ret_bits, digit_bits); - if bits_want != 64 { - ret <<= bits_want; + if bits_want != 0 { + if bits_want != 64 { + ret <<= bits_want; + } + // XXX Conversion is useless if already 64-bit. + #[allow(clippy::useless_conversion)] + let d0 = u64::from(*d) >> (digit_bits - bits_want); + ret |= d0; } - // XXX Conversion is useless if already 64-bit. - #[allow(clippy::useless_conversion)] - let d0 = u64::from(*d) >> (digit_bits - bits_want); - ret |= d0; - ret_bits += bits_want; - bits -= bits_want; - if ret_bits == 64 { - break; + // Implement round-to-odd: If any lower bits are 1, set LSB to 1 + // so that rounding again to floating point value using + // nearest-ties-to-even is correct. + // + // See: https://en.wikipedia.org/wiki/Rounding#Rounding_to_prepare_for_shorter_precision + + if digit_bits - bits_want != 0 { + // XXX Conversion is useless if already 64-bit. + #[allow(clippy::useless_conversion)] + let masked = u64::from(*d) << (64 - (digit_bits - bits_want) as u32); + ret |= (masked != 0) as u64; } + + ret_bits += bits_want; + bits -= bits_want; } ret diff --git a/tests/bigint.rs b/tests/bigint.rs index b071c6d8..3144367f 100644 --- a/tests/bigint.rs +++ b/tests/bigint.rs @@ -415,6 +415,13 @@ fn test_convert_f32() { b <<= 1; } + // test correct ties-to-even rounding + let weird: i128 = (1i128 << 100) + (1i128 << (100 - f32::MANTISSA_DIGITS)); + assert_ne!(weird as f32, (weird + 1) as f32); + + assert_eq!(BigInt::from(weird).to_f32(), Some(weird as f32)); + assert_eq!(BigInt::from(weird + 1).to_f32(), Some((weird + 1) as f32)); + // rounding assert_eq!( BigInt::from_f32(-f32::consts::PI), @@ -505,6 +512,13 @@ fn test_convert_f64() { b <<= 1; } + // test correct ties-to-even rounding + let weird: i128 = (1i128 << 100) + (1i128 << (100 - f64::MANTISSA_DIGITS)); + assert_ne!(weird as f64, (weird + 1) as f64); + + assert_eq!(BigInt::from(weird).to_f64(), Some(weird as f64)); + assert_eq!(BigInt::from(weird + 1).to_f64(), Some((weird + 1) as f64)); + // rounding assert_eq!( BigInt::from_f64(-f64::consts::PI), diff --git a/tests/biguint.rs b/tests/biguint.rs index a3c107e7..c027771b 100644 --- a/tests/biguint.rs +++ b/tests/biguint.rs @@ -646,6 +646,13 @@ fn test_convert_f32() { b <<= 1; } + // test correct ties-to-even rounding + let weird: i128 = (1i128 << 100) + (1i128 << (100 - f32::MANTISSA_DIGITS)); + assert_ne!(weird as f32, (weird + 1) as f32); + + assert_eq!(BigInt::from(weird).to_f32(), Some(weird as f32)); + assert_eq!(BigInt::from(weird + 1).to_f32(), Some((weird + 1) as f32)); + // rounding assert_eq!(BigUint::from_f32(-1.0), None); assert_eq!(BigUint::from_f32(-0.99999), Some(BigUint::zero())); @@ -722,6 +729,13 @@ fn test_convert_f64() { b <<= 1; } + // test correct ties-to-even rounding + let weird: i128 = (1i128 << 100) + (1i128 << (100 - f64::MANTISSA_DIGITS)); + assert_ne!(weird as f64, (weird + 1) as f64); + + assert_eq!(BigInt::from(weird).to_f64(), Some(weird as f64)); + assert_eq!(BigInt::from(weird + 1).to_f64(), Some((weird + 1) as f64)); + // rounding assert_eq!(BigUint::from_f64(-1.0), None); assert_eq!(BigUint::from_f64(-0.99999), Some(BigUint::zero()));