4

I need to turn an array of doubles to ints while keeping their ratios the same and being as simple as possible. For example [0.7, 0, -0.7] should become [1, 0, -1] and [24, 12, 0] should become [2, 1, 0]. I'm not certain if this would involve getting the least common multiple of the doubles or not, and how would this be done if so?

Youssef Moawad
  • 2,846
  • 5
  • 28
  • 50

1 Answers1

11

(The code has been updated for Swift 4 and later.)

First of all, there is no GCD or LCM for floating point numbers. You have to convert the input to rational numbers first.

This is not as easy as it sounds, because decimal fractions like 0.7 cannot be represented exactly as a binary floating point number and would be stored as something like 0.69999999999999996 in a Double. So it is not completely obvious how to get from there to 7/10.

It is therefore necessary to specify a precision. Then you can use Continued Fractions to efficiently create a (finite or infinite) sequence of fractions hn/kn that are arbitrary good approximations to a given real number x.

Here is a translation of this JavaScript implementation to Swift:

typealias Rational = (num : Int, den : Int)

func rationalApproximationOf(_ x0 : Double, withPrecision eps : Double = 1.0E-6) -> Rational {
    var x = x0
    var a = floor(x)
    var (h1, k1, h, k) = (1, 0, Int(a), 1)

    while x - a > eps * Double(k) * Double(k) {
        x = 1.0/(x - a)
        a = floor(x)
        (h1, k1, h, k) = (h, k, h1 + Int(a) * h, k1 + Int(a) * k)
    }
    return (h, k)
}

Examples:

rationalApproximationOf(0.7)      // (7, 10) i.e. 7/10
rationalApproximationOf(0.142857) // (1, 7)  i.e. 1/7

I have set the default precision to 1.0E-6, but you can adjust that to your needs.

Then you need functions for the GCD (greatest common divisor) and LCM (lowest common multiple). Here are simple implementation:

// GCD of two numbers:
func gcd(_ a: Int, _ b: Int) -> Int {
    var (a, b) = (a, b)
    while b != 0 {
        (a, b) = (b, a % b)
    }
    return abs(a)
}

// GCD of a vector of numbers:
func gcd(_ vector: [Int]) -> Int {
    return vector.reduce(0, gcd)
}

// LCM of two numbers:
func lcm(a: Int, b: Int) -> Int {
    return (a / gcd(a, b)) * b
}

// LCM of a vector of numbers:
func lcm(_ vector : [Int]) -> Int {
    return vector.reduce(1, lcm)
}

With all these utilities, your function can now be implemented as

func simplifyRatios(_ numbers : [Double]) -> [Int] {
    // Normalize the input vector to that the maximum is 1.0,
    // and compute rational approximations of all components:
    let maximum = numbers.map(abs).max()!
    let rats = numbers.map { rationalApproximationOf($0/maximum) }

    // Multiply all rational numbers by the LCM of the denominators:
    let commonDenominator = lcm(rats.map { $0.den })
    let numerators = rats.map { $0.num * commonDenominator / $0.den }

    // Divide the numerators by the GCD of all numerators:
    let commonNumerator = gcd(numerators)
    return numerators.map { $0 / commonNumerator }
}

Examples:

simplifyRatios([0.7, 0, -0.7])   // [1, 0, -1]
simplifyRatios([24, 12, 0])      // [2, 1, 0]
simplifyRatios([1.3, 0.26, 0.9]) // [65, 13, 45]
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Hi, thanks for your answer. Although this does work, after performing operations using the Accelerate framework and getting an array of [0.894427190999916, -0.447213595499958, 0.0], the simplifyRatios function returns [1762288.0, -881145.0, 0.0] where the expected is [2, -1, 0]. – Youssef Moawad Feb 06 '15 at 16:10
  • I was able to solve this by raising the default precision to 1e-9 which seems to be the highest it can handle as 1e-10 causes an exception. – Youssef Moawad Feb 06 '15 at 16:17
  • 1
    @YoussefSami: It seems that normalizing the vector (so that the maximum element is 1.0) seems to give better results. I have updated the answer accordingly. – Martin R Feb 06 '15 at 20:17
  • I have encountered some issues with this in certain cases involving zeroes, should I continue here or start a new question? – Youssef Moawad Feb 10 '15 at 10:39
  • @YoussefSami: Can you describe the issue shortly in a comment? Then I'll see if I can help or if a new question is needed. – Martin R Feb 10 '15 at 10:41
  • In this line `var (h1, k1, h, k) = (1, 0, Int(a), 1)` in the `rationalApproximation()` function, an exception is thrown: "floating point value can not be converted to Int because it is either infinite or NaN". This currently happens when I pass in 2 negative floats and a zero with the zero being anywhere in the vector to the `simplifyRatios()` functions. No error occurs though when I pass in 3 negative floats. – Youssef Moawad Feb 10 '15 at 10:45
  • 1
    @YoussefSami: There was an error in the calculation of `let maximum = ...` which should use the *absolute* value. I have fixed that in the above code. – Martin R Feb 10 '15 at 10:55