Skip to content

Commit 9079b88

Browse files
committed
Auto merge of #169 - cuviper:rational-overflow, r=hauleth
Avoid overflows in Ratio's Ord::cmp Fixes #7
2 parents 8be7e7b + 4e66bbe commit 9079b88

File tree

1 file changed

+95
-24
lines changed

1 file changed

+95
-24
lines changed

src/rational.rs

Lines changed: 95 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -224,33 +224,67 @@ impl Ratio<BigInt> {
224224

225225
/* Comparisons */
226226

227-
// comparing a/b and c/d is the same as comparing a*d and b*c, so we
228-
// abstract that pattern. The following macro takes a trait and either
229-
// a comma-separated list of "method name -> return value" or just
230-
// "method name" (return value is bool in that case)
231-
macro_rules! cmp_impl {
232-
(impl $imp:ident, $($method:ident),+) => {
233-
cmp_impl!(impl $imp, $($method -> bool),+);
234-
};
235-
// return something other than a Ratio<T>
236-
(impl $imp:ident, $($method:ident -> $res:ty),*) => {
237-
impl<T> $imp for Ratio<T> where
238-
T: Clone + Mul<T, Output = T> + $imp
239-
{
240-
$(
241-
#[inline]
242-
fn $method(&self, other: &Ratio<T>) -> $res {
243-
(self.numer.clone() * other.denom.clone()). $method (&(self.denom.clone()*other.numer.clone()))
227+
// Mathematically, comparing a/b and c/d is the same as comparing a*d and b*c, but it's very easy
228+
// for those multiplications to overflow fixed-size integers, so we need to take care.
229+
230+
impl<T: Clone + Integer> Ord for Ratio<T> {
231+
#[inline]
232+
fn cmp(&self, other: &Self) -> cmp::Ordering {
233+
// With equal denominators, the numerators can be directly compared
234+
if self.denom == other.denom {
235+
let ord = self.numer.cmp(&other.numer);
236+
return if self.denom < T::zero() { ord.reverse() } else { ord };
237+
}
238+
239+
// With equal numerators, the denominators can be inversely compared
240+
if self.numer == other.numer {
241+
let ord = self.denom.cmp(&other.denom);
242+
return if self.numer < T::zero() { ord } else { ord.reverse() };
243+
}
244+
245+
// Unfortunately, we don't have CheckedMul to try. That could sometimes avoid all the
246+
// division below, or even always avoid it for BigInt and BigUint.
247+
// FIXME- future breaking change to add Checked* to Integer?
248+
249+
// Compare as floored integers and remainders
250+
let (self_int, self_rem) = self.numer.div_mod_floor(&self.denom);
251+
let (other_int, other_rem) = other.numer.div_mod_floor(&other.denom);
252+
match self_int.cmp(&other_int) {
253+
cmp::Ordering::Greater => cmp::Ordering::Greater,
254+
cmp::Ordering::Less => cmp::Ordering::Less,
255+
cmp::Ordering::Equal => {
256+
match (self_rem.is_zero(), other_rem.is_zero()) {
257+
(true, true) => cmp::Ordering::Equal,
258+
(true, false) => cmp::Ordering::Less,
259+
(false, true) => cmp::Ordering::Greater,
260+
(false, false) => {
261+
// Compare the reciprocals of the remaining fractions in reverse
262+
let self_recip = Ratio::new_raw(self.denom.clone(), self_rem);
263+
let other_recip = Ratio::new_raw(other.denom.clone(), other_rem);
264+
self_recip.cmp(&other_recip).reverse()
265+
}
244266
}
245-
)*
267+
},
246268
}
247-
};
269+
}
270+
}
271+
272+
impl<T: Clone + Integer> PartialOrd for Ratio<T> {
273+
#[inline]
274+
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
275+
Some(self.cmp(other))
276+
}
248277
}
249-
cmp_impl!(impl PartialEq, eq, ne);
250-
cmp_impl!(impl PartialOrd, lt -> bool, gt -> bool, le -> bool, ge -> bool,
251-
partial_cmp -> Option<cmp::Ordering>);
252-
cmp_impl!(impl Eq, );
253-
cmp_impl!(impl Ord, cmp -> cmp::Ordering);
278+
279+
impl<T: Clone + Integer> PartialEq for Ratio<T> {
280+
#[inline]
281+
fn eq(&self, other: &Self) -> bool {
282+
self.cmp(other) == cmp::Ordering::Equal
283+
}
284+
}
285+
286+
impl<T: Clone + Integer> Eq for Ratio<T> {}
287+
254288

255289
macro_rules! forward_val_val_binop {
256290
(impl $imp:ident, $method:ident) => {
@@ -597,6 +631,43 @@ mod test {
597631
assert!(_1 >= _0 && !(_0 >= _1));
598632
}
599633

634+
#[test]
635+
fn test_cmp_overflow() {
636+
use std::cmp::Ordering;
637+
638+
// issue #7 example:
639+
let big = Ratio::new(128u8, 1);
640+
let small = big.recip();
641+
assert!(big > small);
642+
643+
// try a few that are closer together
644+
// (some matching numer, some matching denom, some neither)
645+
let ratios = vec![
646+
Ratio::new(125_i8, 127_i8),
647+
Ratio::new(63_i8, 64_i8),
648+
Ratio::new(124_i8, 125_i8),
649+
Ratio::new(125_i8, 126_i8),
650+
Ratio::new(126_i8, 127_i8),
651+
Ratio::new(127_i8, 126_i8),
652+
];
653+
654+
fn check_cmp(a: Ratio<i8>, b: Ratio<i8>, ord: Ordering) {
655+
println!("comparing {} and {}", a, b);
656+
assert_eq!(a.cmp(&b), ord);
657+
assert_eq!(b.cmp(&a), ord.reverse());
658+
}
659+
660+
for (i, &a) in ratios.iter().enumerate() {
661+
check_cmp(a, a, Ordering::Equal);
662+
check_cmp(-a, a, Ordering::Less);
663+
for &b in &ratios[i+1..] {
664+
check_cmp(a, b, Ordering::Less);
665+
check_cmp(-a, -b, Ordering::Greater);
666+
check_cmp(a.recip(), b.recip(), Ordering::Greater);
667+
check_cmp(-a.recip(), -b.recip(), Ordering::Less);
668+
}
669+
}
670+
}
600671

601672
#[test]
602673
fn test_to_integer() {

0 commit comments

Comments
 (0)