Skip to content

Avoid overflows in Ratio's Ord::cmp #169

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 25, 2016
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 95 additions & 24 deletions src/rational.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,33 +224,67 @@ impl Ratio<BigInt> {

/* Comparisons */

// comparing a/b and c/d is the same as comparing a*d and b*c, so we
// abstract that pattern. The following macro takes a trait and either
// a comma-separated list of "method name -> return value" or just
// "method name" (return value is bool in that case)
macro_rules! cmp_impl {
(impl $imp:ident, $($method:ident),+) => {
cmp_impl!(impl $imp, $($method -> bool),+);
};
// return something other than a Ratio<T>
(impl $imp:ident, $($method:ident -> $res:ty),*) => {
impl<T> $imp for Ratio<T> where
T: Clone + Mul<T, Output = T> + $imp
{
$(
#[inline]
fn $method(&self, other: &Ratio<T>) -> $res {
(self.numer.clone() * other.denom.clone()). $method (&(self.denom.clone()*other.numer.clone()))
// Mathematically, comparing a/b and c/d is the same as comparing a*d and b*c, but it's very easy
// for those multiplications to overflow fixed-size integers, so we need to take care.

impl<T: Clone + Integer> Ord for Ratio<T> {
#[inline]
fn cmp(&self, other: &Self) -> cmp::Ordering {
// With equal denominators, the numerators can be directly compared
if self.denom == other.denom {
let ord = self.numer.cmp(&other.numer);
return if self.denom < T::zero() { ord.reverse() } else { ord };
}

// With equal numerators, the denominators can be inversely compared
if self.numer == other.numer {
let ord = self.denom.cmp(&other.denom);
return if self.numer < T::zero() { ord } else { ord.reverse() };
}

// Unfortunately, we don't have CheckedMul to try. That could sometimes avoid all the
// division below, or even always avoid it for BigInt and BigUint.
// FIXME- future breaking change to add Checked* to Integer?

// Compare as floored integers and remainders
let (self_int, self_rem) = self.numer.div_mod_floor(&self.denom);
let (other_int, other_rem) = other.numer.div_mod_floor(&other.denom);
match self_int.cmp(&other_int) {
cmp::Ordering::Greater => cmp::Ordering::Greater,
cmp::Ordering::Less => cmp::Ordering::Less,
cmp::Ordering::Equal => {
match (self_rem.is_zero(), other_rem.is_zero()) {
(true, true) => cmp::Ordering::Equal,
(true, false) => cmp::Ordering::Less,
(false, true) => cmp::Ordering::Greater,
(false, false) => {
// Compare the reciprocals of the remaining fractions in reverse
let self_recip = Ratio::new_raw(self.denom.clone(), self_rem);
let other_recip = Ratio::new_raw(other.denom.clone(), other_rem);
self_recip.cmp(&other_recip).reverse()
}
}
)*
},
}
};
}
}

impl<T: Clone + Integer> PartialOrd for Ratio<T> {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
Some(self.cmp(other))
}
}
cmp_impl!(impl PartialEq, eq, ne);
cmp_impl!(impl PartialOrd, lt -> bool, gt -> bool, le -> bool, ge -> bool,
partial_cmp -> Option<cmp::Ordering>);
cmp_impl!(impl Eq, );
cmp_impl!(impl Ord, cmp -> cmp::Ordering);

impl<T: Clone + Integer> PartialEq for Ratio<T> {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == cmp::Ordering::Equal
}
}

impl<T: Clone + Integer> Eq for Ratio<T> {}


macro_rules! forward_val_val_binop {
(impl $imp:ident, $method:ident) => {
Expand Down Expand Up @@ -597,6 +631,43 @@ mod test {
assert!(_1 >= _0 && !(_0 >= _1));
}

#[test]
fn test_cmp_overflow() {
use std::cmp::Ordering;

// issue #7 example:
let big = Ratio::new(128u8, 1);
let small = big.recip();
assert!(big > small);

// try a few that are closer together
// (some matching numer, some matching denom, some neither)
let ratios = vec![
Ratio::new(125_i8, 127_i8),
Ratio::new(63_i8, 64_i8),
Ratio::new(124_i8, 125_i8),
Ratio::new(125_i8, 126_i8),
Ratio::new(126_i8, 127_i8),
Ratio::new(127_i8, 126_i8),
];

fn check_cmp(a: Ratio<i8>, b: Ratio<i8>, ord: Ordering) {
println!("comparing {} and {}", a, b);
assert_eq!(a.cmp(&b), ord);
assert_eq!(b.cmp(&a), ord.reverse());
}

for (i, &a) in ratios.iter().enumerate() {
check_cmp(a, a, Ordering::Equal);
check_cmp(-a, a, Ordering::Less);
for &b in &ratios[i+1..] {
check_cmp(a, b, Ordering::Less);
check_cmp(-a, -b, Ordering::Greater);
check_cmp(a.recip(), b.recip(), Ordering::Greater);
check_cmp(-a.recip(), -b.recip(), Ordering::Less);
}
}
}

#[test]
fn test_to_integer() {
Expand Down