Skip to content

Add float to Ratio conversion #9838

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

Closed
wants to merge 2 commits into from
Closed
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
101 changes: 100 additions & 1 deletion src/libextra/num/rational.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
use std::cmp;
use std::from_str::FromStr;
use std::num::{Zero,One,ToStrRadix,FromStrRadix,Round};
use super::bigint::BigInt;
use super::bigint::{BigInt, BigUint, Sign, Plus, Minus};

/// Represents the ratio between 2 numbers.
#[deriving(Clone)]
Expand Down Expand Up @@ -107,6 +107,66 @@ impl<T: Clone + Integer + Ord>
}
}

impl Ratio<BigInt> {
// Ported from
// http://www.ginac.de/CLN/cln.git/?p=cln.git;a=blob;f=src/real/misc/cl_R_rationalize.cc;hb=HEAD
// http://sourceforge.net/p/sbcl/sbcl/ci/master/tree/src/code/float.lisp#l850
// (2013-11-17)
/// Converts a float into a rational number
fn rationalize<T: Float>(f: T) -> Option<BigRational> {
if !f.is_finite() {
return None;
}
let (mantissa, exponent, sign) = f.integer_decode();
if mantissa == 0 || exponent >= 0 {
let mut numer: BigUint = FromPrimitive::from_u64(mantissa).unwrap();
numer = numer << (exponent as uint);
let bigint_sign: Sign = if sign == 1 { Plus } else { Minus };
return Some(Ratio::from_integer(BigInt::from_biguint(bigint_sign, numer)));
}

let one: BigInt = One::one();
let mut a: BigRational = Ratio::new(
FromPrimitive::from_u64(2 * mantissa - 1).unwrap(),
one << ((1 - exponent) as uint));
let mut b: BigRational = Ratio::new(
FromPrimitive::from_u64(2 * mantissa + 1).unwrap(),
one << ((1 - exponent) as uint));

let mut p0: BigInt = Zero::zero();
let mut p1: BigInt = One::one();
let mut q0: BigInt = One::one();
let mut q1: BigInt = Zero::zero();
let mut c;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this just be let c = a.ceil(); in the loop?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Oh, no, I missed that it's used below.)


loop {
c = a.ceil();
if c >= b {
let k = c.to_integer() - One::one();

let p2 = k * p1 + p0;
p0 = p1;
p1 = p2;
let q2 = k * q1 + q0;
q0 = q1;
q1 = q2;

let tmp = (b - Ratio::from_integer(k.clone())).recip();
b = (a - Ratio::from_integer(k)).recip();
a = tmp;
} else {
break;
}
};

let p_last: BigInt = c.to_integer() * p1 + p0;
let q_last: BigInt = c.to_integer() * q1 + q0;
let p = if sign == 1 { p_last } else { -p_last };
Some(Ratio::new(p, q_last))
}
}


/* Comparisons */

// comparing a/b and c/d is the same as comparing a*d and b*c, so we
Expand Down Expand Up @@ -621,4 +681,43 @@ mod test {
test(s);
}
}

#[test]
fn test_rationalize() {
fn test<T: Float>(given: T, expected: (&str, &str)) {
let ratio: BigRational = Ratio::rationalize(given).unwrap();
let (numer, denom) = expected;
assert_eq!(ratio, Ratio::new(
FromStr::from_str(numer).unwrap(),
FromStr::from_str(denom).unwrap()));
}

// f32
test(3.14159265359f32, ("93343", "29712"));
test(2f32.pow(&100.), ("1267650600228229401496703205376", "1"));
test(-2f32.pow(&100.), ("-1267650600228229401496703205376", "1"));
test(1.0 / 2f32.pow(&100.), ("1", "1267650524670370179181738721296"));
test(684729.48391f32, ("1369459", "2"));
test(-8573.5918555f32, ("-420106", "49"));

// f64
test(3.14159265359f64, ("226883371", "72219220"));
test(2f64.pow(&100.), ("1267650600228229401496703205376", "1"));
test(-2f64.pow(&100.), ("-1267650600228229401496703205376", "1"));
test(1.0 / 2f64.pow(&100.), ("1", "1267650600228229260759214850049"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A power of two should definitely be an even number, and 2**-100 is representable exactly as a float, so there shouldn't be any loses. (Specifically, this should be an exact flip of the 2f64.pow(&100.) case.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, also, I get different numbers in Haskell (and Python agrees from a incomplete check):

> -- f32
> let nums = [3.14159265359, 2.0^100, -2.0 ^ 100, 1.0 / (2.0^100), 684729.48391, -8573.5918555] :: [Float]
> mapM_ (\n -> putStrLn $ show n ++ "  \t" ++ show (toRational n)) nums
3.1415927       13176795 % 4194304
1.2676506e30    1267650600228229401496703205376 % 1
-1.2676506e30   (-1267650600228229401496703205376) % 1
7.888609e-31    1 % 1267650600228229401496703205376
684729.5        1369459 % 2
-8573.592       (-4389679) % 512

> -- f64
> let nums = [3.14159265359, 2.0^100, -2.0 ^ 100, 1.0 / (2.0^100), 684729.48391, -8573.5918555] :: [Double]
> mapM_ (\n -> putStrLn $ show n ++ "\t" ++ show (toRational n)) nums
3.14159265359           3537118876014453 % 1125899906842624
1.2676506002282294e30   1267650600228229401496703205376 % 1
-1.2676506002282294e30  (-1267650600228229401496703205376) % 1
7.888609052210118e-31   1 % 1267650600228229401496703205376
684729.48391            367611342500051 % 536870912
-8573.5918555           (-4713381968463931) % 549755813888

(The format is <float> <numerator> % <denominator> .)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python and Haskell have a different implementation. They do the simple way. The algorithm I use tries to find the smallest possible value that still matches the float. Try Lisp (Clisp or SBCL), that's what I've used to verify my numbers. Let me write a small script to show the output.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see (sorry for my clisp inexperience):

> (map nil #'(lambda (n) (format t "~S ~S~%" n (rationalize n))) `(3.14159265359d0  684729.48391d0 -8573.5918555d0 ,(expt 2.0d0 -100)))
3.14159265359d0 226883371/72219220
684729.48391d0 68472948391/100000
-8573.5918555d0 -13743853556/1603045
7.888609052210118d-31 1/1267650600228229260759214850049
NIL

I think it's a little peculiar that the exact 2.0**-100 isn't exact for f64 (it appears to be exactly one epsilon off); I guess we don't have much choice?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's one epsilon off. When you look at the bit representation:

>>> import struct
>>> [struct.unpack('<Q', struct.pack('<d', f))[0] for f in (2 ** -100, 1.0/1267650600228229260759214850049, 1.0/1267650600228229401496703205376)]
[4156822456062967808, 4156822456062967809, 4156822456062967808]

We have a choice in fixing the algorithm. Not sure how easy that would be as I've only ported it and not really thought through it. I'll give it a try though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming can you get the license stuff OK-d, I'm happy to r+ as is, and we can investigate a perfectly precise algorithm later, if necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've sent a mail on Monday, still waiting for a reply :) Though I'll take the chance to have a closer look at the algorithm. I'm not sure if it makes sense to have it go in with a known bug.

test(684729.48391f64, ("68472948391", "100000"));
test(-8573.5918555f64, ("-13743853556", "1603045"));
}

#[test]
fn test_rationalize_fail() {
use std::{f32, f64};

assert_eq!(Ratio::rationalize(f32::NAN), None);
assert_eq!(Ratio::rationalize(f32::INFINITY), None);
assert_eq!(Ratio::rationalize(f32::NEG_INFINITY), None);
assert_eq!(Ratio::rationalize(f64::NAN), None);
assert_eq!(Ratio::rationalize(f64::INFINITY), None);
assert_eq!(Ratio::rationalize(f64::NEG_INFINITY), None);
}
}