I use the following code to find the lowest denominator Rational that is within a certain delta from a double
.
The rationale is that the I am pulling float numbers from a database and in many cases summing them. All of the numbers are calculated using simple maths such as +, -, * and /. No transcendental numbers are involved, nor is there any trigonometry. In most cases finding the nearest Rational to the float gets what the original figure is supposed to be rather than the results of adding mashed-up numbers together.
// Create a good rational for the value within the delta supplied.
public static Rational valueOf(double dbl, double delta) {
// Primary checks.
if (delta <= 0.0) {
throw new IllegalArgumentException("Delta must be > 0.0");
}
// Remove the sign and integral part.
long integral = (long) Math.floor(dbl);
dbl -= integral;
// The value we are looking for.
final Rational d = new Rational((long) ((dbl) / delta), (long) (1 / delta));
// Min value = d - delta.
final Rational min = new Rational((long) ((dbl - delta) / delta), (long) (1 / delta));
// Max value = d + delta.
final Rational max = new Rational((long) ((dbl + delta) / delta), (long) (1 / delta));
// Start the fairey sequence.
Rational l = ZERO;
Rational h = ONE;
Rational found = null;
// Keep slicing until we arrive within the delta range.
do {
// Either between min and max -> found it.
if (found == null && min.compareTo(l) <= 0 && max.compareTo(l) >= 0) {
found = l;
}
if (found == null && min.compareTo(h) <= 0 && max.compareTo(h) >= 0) {
found = h;
}
if (found == null) {
// Make the mediant.
Rational m = mediant(l, h);
// Replace either l or h with mediant.
if (m.compareTo(d) < 0) {
l = m;
} else {
h = m;
}
}
} while (found == null);
// Bring back the sign and the integral.
if (integral != 0) {
found = found.plus(new Rational(integral, 1));
}
// That's me.
return found;
}
In a recent test using 0.000001 as my delta this code took 75% of the CPU. Dropping it to 0.0001 reduced that dramatically but it is still a significant bottleneck.
Is there a quicker way of doing this?
My implementation of Rational forces the numerator and denominator to be fully reduced at all times. I accept that that is likely the biggest overhead but as mentioned in Wikipedia the rationals must be fully reduced for the mediant function to work correctly.
Here is the full class - borrowed from the mentioned site and enhanced:
/**
* ***********************************************************************
* Immutable ADT for Rational numbers.
*
* Invariants
* -----------
* - gcd(num, den) = 1, i.e, the rational number is in reduced form
* - den >= 1, the denominator is always a positive integer
* - 0/1 is the unique representation of 0
*
* We employ some tricks to stave of overflow, but if you
* need arbitrary precision rationals, use BigRational.java.
*
* Borrowed from http://introcs.cs.princeton.edu/java/92symbolic/Rational.java.html
* because it has a mediant method.
*
************************************************************************
*/
public class Rational extends Number implements Comparable<Rational> {
public static final Rational ZERO = new Rational(0, 1);
public static final Rational ONE = new Rational(1, 1);
private long num; // the numerator
private long den; // the denominator
// create and initialize a new Rational object
public Rational(long numerator, long denominator) {
// deal with x/0
if (denominator == 0) {
throw new IllegalArgumentException("Denominator cannot be 0.");
}
// reduce fraction
long g = gcd(numerator, denominator);
num = numerator / g;
den = denominator / g;
// only needed for negative numbers
if (den < 0) {
den = -den;
num = -num;
}
}
public Rational(Rational from) {
num = from.num;
den = from.den;
}
// return the numerator and denominator of (this)
public long numerator() {
return num;
}
public long denominator() {
return den;
}
// return double precision representation of (this)
public double toDouble() {
return (double) num / den;
}
public BigDecimal toBigDecimal() {
// Do it to just 4 decimal places.
return toBigDecimal(4);
}
public BigDecimal toBigDecimal(int digits) {
// Do it to n decimal places.
return new BigDecimal(num).divide(new BigDecimal(den), digits, RoundingMode.DOWN).stripTrailingZeros();
}
// return string representation of (this)
@Override
public String toString() {
if (den == 1) {
return num + "";
} else {
return num + "/" + den;
}
}
public int compareTo(Rational b) {
// return { -1, 0, +1 } if a < b, a = b, or a > b
Rational a = this;
long lhs = a.num * b.den;
long rhs = a.den * b.num;
if (lhs < rhs) {
return -1;
}
if (lhs > rhs) {
return +1;
}
return 0;
}
@Override
public boolean equals(Object y) {
// is this Rational object equal to y?
if (y == null) {
return false;
}
if (y.getClass() != this.getClass()) {
return false;
}
Rational b = (Rational) y;
return compareTo(b) == 0;
}
@Override
public int hashCode() {
int hash = 5;
hash = 97 * hash + (int) (this.num ^ (this.num >>> 32));
hash = 97 * hash + (int) (this.den ^ (this.den >>> 32));
return hash;
}
// create and return a new rational (r.num + s.num) / (r.den + s.den)
public static Rational mediant(Rational r, Rational s) {
return new Rational(r.num + s.num, r.den + s.den);
}
// return gcd(|m|, |n|)
private static long gcd(long m, long n) {
if (m < 0) {
m = -m;
}
if (n < 0) {
n = -n;
}
if (0 == n) {
return m;
} else {
return gcd(n, m % n);
}
}
// return lcm(|m|, |n|)
private static long lcm(long m, long n) {
if (m < 0) {
m = -m;
}
if (n < 0) {
n = -n;
}
return m * (n / gcd(m, n)); // parentheses important to avoid overflow
}
// return a * b, staving off overflow as much as possible by cross-cancellation
public Rational times(Rational b) {
Rational a = this;
// reduce p1/q2 and p2/q1, then multiply, where a = p1/q1 and b = p2/q2
Rational c = new Rational(a.num, b.den);
Rational d = new Rational(b.num, a.den);
return new Rational(c.num * d.num, c.den * d.den);
}
// return a + b, staving off overflow
public Rational plus(Rational b) {
Rational a = this;
// special cases
if (a.compareTo(ZERO) == 0) {
return b;
}
if (b.compareTo(ZERO) == 0) {
return a;
}
// Find gcd of numerators and denominators
long f = gcd(a.num, b.num);
long g = gcd(a.den, b.den);
// add cross-product terms for numerator
Rational s = new Rational((a.num / f) * (b.den / g) + (b.num / f) * (a.den / g),
lcm(a.den, b.den));
// multiply back in
s.num *= f;
return s;
}
// return -a
public Rational negate() {
return new Rational(-num, den);
}
// return a - b
public Rational minus(Rational b) {
return plus(b.negate());
}
public Rational reciprocal() {
return new Rational(den, num);
}
// return a / b
public Rational divides(Rational b) {
Rational a = this;
return a.times(b.reciprocal());
}
// Default delta to apply.
public static final double DELTA = 0.0001;
public static Rational valueOf(double dbl) {
return valueOf(dbl, DELTA);
}
public static Rational valueOf(BigDecimal dbl) {
return valueOf(dbl.doubleValue(), DELTA);
}
public static Rational valueOf(double dbl, int digits) {
return valueOf(dbl, Math.pow(10, -digits));
}
public static Rational valueOf(BigDecimal dbl, int digits) {
return valueOf(dbl.doubleValue(), Math.pow(10, -digits));
}
// Create a good rational for the value within the delta supplied.
public static Rational valueOf(double dbl, double delta) {
// Primary checks.
if (delta <= 0.0) {
throw new IllegalArgumentException("Delta must be > 0.0");
}
// Remove the sign and integral part.
long integral = (long) Math.floor(dbl);
dbl -= integral;
// The value we are looking for.
final Rational d = new Rational((long) ((dbl) / delta), (long) (1 / delta));
// Min value = d - delta.
final Rational min = new Rational((long) ((dbl - delta) / delta), (long) (1 / delta));
// Max value = d + delta.
final Rational max = new Rational((long) ((dbl + delta) / delta), (long) (1 / delta));
// Start the fairey sequence.
Rational l = ZERO;
Rational h = ONE;
Rational found = null;
// Keep slicing until we arrive within the delta range.
do {
// Either between min and max -> found it.
if (found == null && min.compareTo(l) <= 0 && max.compareTo(l) >= 0) {
found = l;
}
if (found == null && min.compareTo(h) <= 0 && max.compareTo(h) >= 0) {
found = h;
}
if (found == null) {
// Make the mediant.
Rational m = mediant(l, h);
// Replace either l or h with mediant.
if (m.compareTo(d) < 0) {
l = m;
} else {
h = m;
}
}
} while (found == null);
// Bring back the sign and the integral.
if (integral != 0) {
found = found.plus(new Rational(integral, 1));
}
// That's me.
return found;
}
private static void print(String name, Rational r) {
System.out.println(name + "=" + r + "(" + r.toDouble() + ")");
}
private enum TestNumber {
OneTenth(0.100000001490116119384765625),
Pi(Math.PI),
E(Math.E),
OneThird(0.3333333333333),
MinusOneThird(-0.3333333333333),
ABig1(1.87344227533222758141533568138280569154340745619495504034120344898213260187710089517712780269958755185722145694193999220);
final double v;
TestNumber(double v) {
this.v = v;
}
}
private static void test2() {
for (TestNumber n : TestNumber.values()) {
print(n.name(), Rational.valueOf(n.v));
}
}
private static void test1() {
Rational x, y, z;
// 1/2 + 1/3 = 5/6
x = new Rational(1, 2);
y = new Rational(1, 3);
z = x.plus(y);
System.out.println(z);
// 8/9 + 1/9 = 1
x = new Rational(8, 9);
y = new Rational(1, 9);
z = x.plus(y);
System.out.println(z);
// 1/200000000 + 1/300000000 = 1/120000000
x = new Rational(1, 200000000);
y = new Rational(1, 300000000);
z = x.plus(y);
System.out.println(z);
// 1073741789/20 + 1073741789/30 = 1073741789/12
x = new Rational(1073741789, 20);
y = new Rational(1073741789, 30);
z = x.plus(y);
System.out.println(z);
// 4/17 * 17/4 = 1
x = new Rational(4, 17);
y = new Rational(17, 4);
z = x.times(y);
System.out.println(z);
// 3037141/3247033 * 3037547/3246599 = 841/961
x = new Rational(3037141, 3247033);
y = new Rational(3037547, 3246599);
z = x.times(y);
System.out.println(z);
// 1/6 - -4/-8 = -1/3
x = new Rational(1, 6);
y = new Rational(-4, -8);
z = x.minus(y);
System.out.println(z);
}
// test client
public static void main(String[] args) {
//test1();
test2();
}
// Implement Number.
@Override
public int intValue() {
return (int) doubleValue();
}
@Override
public long longValue() {
return (long) doubleValue();
}
@Override
public float floatValue() {
return (float) doubleValue();
}
@Override
public double doubleValue() {
return toDouble();
}
}