Skip to content

Commit

Permalink
Add support for approximating a Num with a Num32
Browse files Browse the repository at this point in the history
A fallible conversion as we introduced with the TryFrom implementation
is not always enough: sometimes we need to get a Num32 from a Num,
potentially applying a lossy approximation.
With this change we introduce the functionality for creating such an
approximation, with the newly added Num32::approximate constructor.
  • Loading branch information
d-e-s-o committed Mar 29, 2022
1 parent d1e62d2 commit 2d08d92
Show file tree
Hide file tree
Showing 2 changed files with 164 additions and 0 deletions.
70 changes: 70 additions & 0 deletions src/num.rs
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,76 @@ impl_num! {
pub struct Num32(Rational32), i32
}

impl Num32 {
/// Approximate a [`Num`] with a `Num32`.
///
/// This constructor provides a potentially lossy way of creating a
/// `Num32` that approximates the provided [`Num`].
///
/// If you want to make sure that no precision is lost (and fail if
/// this constraint cannot be upheld), then usage of
/// [`Num32::try_from`] is advised.
pub fn approximate(num: Num) -> Self {
// If the number can directly be represented as a `Num32` we are
// trivially done.
if let Ok(num32) = Num32::try_from(&num) {
return num32
}

let integer = num.to_integer();
// Now if the represented value exceeds the range that we can
// represent (either in the positive or the negative) then the best
// we can do is to "clamp" the value to the representable min/max.
if integer >= BigInt::from(i32::MAX) {
Num32::from(i32::MAX)
} else if integer <= BigInt::from(i32::MIN) {
Num32::from(i32::MIN)
} else {
// Otherwise use the continued fractions algorithm to calculate
// the closest representable approximation.
match Self::continued_fractions(num, integer) {
Ok(num) | Err(num) => num,
}
}
}

/// Approximate the provided number using the continued fractions
/// algorithm as outlined here:
/// https://web.archive.org/web/20120223164926/http://mathforum.org/dr.math/faq/faq.fractions.html#decfrac
fn continued_fractions(num: Num, integer: BigInt) -> Result<Num32, Num32> {
let mut q = num.0;
let mut a = integer;
let mut n0 = 0i32;
let mut d0 = 1i32;
let mut n1 = 1i32;
let mut d1 = 0i32;

loop {
if q.is_integer() {
break Ok(Num32::new(n1, d1))
}

let a32 = a.to_i32();
let n = a32
.and_then(|n| n.checked_mul(n1))
.and_then(|n| n.checked_add(n0))
.ok_or_else(|| Num32::new(n1, d1))?;
let d = a32
.and_then(|n| n.checked_mul(d1))
.and_then(|n| n.checked_add(d0))
.ok_or_else(|| Num32::new(n1, d1))?;

n0 = n1;
d0 = d1;
n1 = n;
d1 = d;

q = (q - a).recip();
a = q.to_integer();
}
}
}

impl<T> From<T> for Num32
where
i32: From<T>,
Expand Down
94 changes: 94 additions & 0 deletions tests/test_num.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,100 @@ fn num64_from_num_failure() {
let () = Num64::try_from(num).unwrap_err();
}

/// Check that we can approximate a [`Num`] with a [`Num32`] when the
/// number can't really be represented.
#[test]
fn num32_approximate_out_of_bounds_num() {
let num = Num::new(i128::MAX, 1);
let num32 = Num32::approximate(num);
assert_eq!(num32, Num32::new(i32::MAX, 1));

let num = Num::new(i128::MIN, 1);
let num32 = Num32::approximate(num);
assert_eq!(num32, Num32::new(i32::MIN, 1));

let num = Num::new(i128::MAX, 1u128 << 96);
let num32 = Num32::approximate(num);
assert_eq!(num32, Num32::new(i32::MAX, 1));

let num = Num::new(i128::MIN, 1i128 << 96);
let num32 = Num32::approximate(num);
assert_eq!(num32, Num32::new(i32::MIN, 1));

let num = Num::new(i128::MAX, i64::MAX);
let num32 = Num32::approximate(num);
assert_eq!(num32, Num32::new(i32::MAX, 1));

let num = Num::new(i128::MAX, i64::MIN);
let num32 = Num32::approximate(num);
assert_eq!(num32, Num32::new(i32::MIN, 1));

let num = Num::new(1u128 << 64, 1u64 << 33);
let num32 = Num32::approximate(num);
assert_eq!(num32, Num32::new(i32::MAX, 1));

let num = Num::new(1u128 << 64, -(1i64 << 33));
let num32 = Num32::approximate(num);
assert_eq!(num32, Num32::new(i32::MIN, 1));
}

/// Check that we can approximate a [`Num`] with a [`Num32`], when the
/// number can be represented without approximation.
#[test]
fn num32_approximate_representable_num() {
let num = Num::new(1u128 << 64, 1u64 << 34);
let num32 = Num32::approximate(num);
assert_eq!(num32, Num32::new(1i32 << 30, 1));

let num = Num::new(-(1i128 << 64), 1u64 << 34);
let num32 = Num32::approximate(num);
assert_eq!(num32, Num32::new(1i32 << 30, -1));

let num = Num::new(127659574, 1000000000);
let num32 = Num32::approximate(num);
assert_eq!(num32, Num32::new(63829787, 500000000));

let num = Num::new(-127659574, 1000000000);
let num32 = Num32::approximate(num);
assert_eq!(num32, Num32::new(-63829787, 500000000));

let num = Num::new(415, 93);
let num32 = Num32::approximate(num);
assert_eq!(num32, Num32::new(415, 93));

let num = Num::new(20, 3);
let num32 = Num32::approximate(num);
assert_eq!(num32, Num32::new(20, 3));

let num = Num::new(-20, 3);
let num32 = Num32::approximate(num);
assert_eq!(num32, Num32::new(-20, 3));
}

/// Check that we can approximate a [`Num`] with a [`Num32`].
#[test]
fn num32_approximate_num() {
let num = Num::new(i128::MAX, i128::MAX - 1);
let num32 = Num32::approximate(num);
assert_eq!(num32, Num32::new(1, 1));

let num = Num::new(i128::MIN, i128::MAX - 1);
let num32 = Num32::approximate(num);
assert_eq!(num32, Num32::new(-1, 1));

let num = Num::new(i128::MIN, i128::MIN + 1);
let num32 = Num32::approximate(num);
assert_eq!(num32, Num32::new(1, 1));

let num = Num::new(3 * (i32::MAX as i64), 13);
let num32 = Num32::approximate(num);
assert_eq!(num32, Num32::new(1486719448, 3));

let num = Num::new(-3 * (i32::MAX as i64), 13);
let num32 = Num32::approximate(num);
assert_eq!(num32, Num32::new(-1486719448, 3));
}

/// Check that we can convert a [`Num`] into its constituent numerator
/// and denominator.
#[test]
Expand Down

0 comments on commit 2d08d92

Please sign in to comment.