Skip to content

Commit

Permalink
Merge #271
Browse files Browse the repository at this point in the history
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 <dramforever@live.com>
  • Loading branch information
bors[bot] and dramforever committed Aug 22, 2023
2 parents 27e4acf + 907fc19 commit 9dbf8d6
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 11 deletions.
19 changes: 18 additions & 1 deletion ci/big_quickcheck/src/lib.rs
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}
32 changes: 22 additions & 10 deletions src/biguint/convert.rs
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions tests/bigint.rs
Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down
14 changes: 14 additions & 0 deletions tests/biguint.rs
Expand Up @@ -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()));
Expand Down Expand Up @@ -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()));
Expand Down

0 comments on commit 9dbf8d6

Please sign in to comment.