Search code examples
iosswiftfloating-point-precision

Loss of precision in float substraction with Swift


I'm trying to make a function to create the fraction version of a floating point number in my app with Swift. It's working perfectly right now, except when it has to build a mixed number (whole and fraction part).

As an example below, when I call the function with 0.4 it works fine, but not with 1.4 due to it has a whole part (1). It seems when I substract the whole part (integerPart) to the original quantity it loses precision. You can check that directly in a playground.

Examples:

0.4 -> 2/5  
1.4 -> 1 2/5  
2.4 -> 2 2/5  
0.5 -> 1/2  
0.7 -> 7/10  
etc...  



func fractionize(quantity: Float) -> String {

let integerPart: Float = floor(quantity);
var numerator: Float = quantity - integerPart;
var firstNumerator: Float = numerator;
var denominator: Float = 1;

if (isInteger(quantity)) {

    return "\(Int(integerPart))";

} else {

    do {
        denominator++;
        numerator = firstNumerator * denominator;
        println(numerator);
    } while (!isInteger(numerator) && denominator <= 10);

    if (integerPart > 0) {

        if (isInteger(numerator)) {
            return "\(integerPart) \(Int(numerator))/\(Int(denominator))";
        } else {
            return "\(quantity) couldn't be fractionized. Result = \(integerPart) \(numerator) / \(denominator)";
        }

    } else {

        if (isInteger(numerator)) {
            return "\(Int(numerator))/\(Int(denominator))";
        } else {
            return "\(quantity) couldn't be fractionized. Result = \(numerator) / \(denominator)";
        }

    }

}

}

fractionize(1.4);

As an extra example, it's working perfectly with 1.5 but not with 1.4, 2.4, 3.4, etc... due to exactly the same. I don't know how to make a good substraction so that the isInteger method works fine. This is my isInteger function. I already tested it and it's working fine:

func isInteger(quantity: Float) -> Bool {
    return floor(quantity) == quantity;
}

Check in a playground and you'll see what happens when you try to fractionize 1.3 or 1.4 for example.


Solution

  • You should calculate by an integer to avoid the floating point precision issue. Therefore, convert the float to an integer at first.

    Is what you want the following code?

    func gcd(var m: Int, var n: Int) -> Int {
        if m < n {
            (m, n) = (n, m)
        }
        if n == 0 {
            return m
        } else if m % n == 0 {
            return n
        } else {
            return gcd(n, m % n)
        }
    }
    
    func fractionize(var quantity: Float) -> String {
        var i = 0
        while quantity % 1 != 0 {
            quantity = quantity * 10
            i += 1
        }
    
        var numerator = Int(quantity)
        var denominator = Int(pow(Double(10), Double(i)))
    
        let divisor = gcd(numerator, denominator)
    
        numerator /= divisor
        denominator /= divisor
    
        var wholeNumber = 0
        if numerator > denominator {
            wholeNumber = numerator / denominator
            numerator -= denominator * wholeNumber
        }
    
        if wholeNumber > 0 {
            return "\(wholeNumber) \(numerator)/\(denominator)"
        } else {
            return "\(numerator)/\(denominator)"
        }
    }
    
    println(fractionize(0.4)) // 2/5
    println(fractionize(1.4)) // 1 2/5
    println(fractionize(2.4)) // 2 2/5
    println(fractionize(0.5)) // 1/2
    println(fractionize(0.7)) // 7/10