hotpaw2
hotpaw2

Reputation: 70733

Safe conversion of Float to Int?

Float to Int conversion is documented to be a simple as:

let i = Int(x)

However this alone is unsafe, as a Swift app will crash if x is too big to fit in an integer, or is a NaN of some kind.

So what is the simplest, yet safe way to convert the unknown contents of a Float or Double to an Int (Int32, UInt16, etc.), e.g. without risking a crash? Is there an Int?() type? Or equivalent "if let" statement?

Upvotes: 7

Views: 2040

Answers (3)

Martin R
Martin R

Reputation: 539965

Int(exactly:) might be what you are looking for:

Creates an integer from the given floating-point value, if it can be represented exactly.

If the value passed as source is not representable exactly, the result is nil.

Example:

let x = 123e20
if let i = Int(exactly: x) {
    print(i)
} else {
    print("not representable")
}

This will also fail if the floating point number is not integral, so you might want to round it before the conversion:

let x = 12.3
if let i = Int(exactly: x.rounded(.towardZero)) {
    print(i)
}

Rounding towards zero is what Int(x) would do, you can pick your desired rounding mode.

Upvotes: 5

Alexander
Alexander

Reputation: 63331

Martin R's answer shows the right way, but I'm still writing this in order to teach what's going on "under the hood."

The limited precision of Double means that only that at the magnitude of Int64.max and Int64.min, Double representations are only available for every 4,096 Integers. As a result, there are a set of integers, which are valid and within range of Int64, which after (lossy) conversion to Double, end up rounded to a magnitude no longer representable as Int64. To account for these values, we need to ensure we only accept the range Double(Self.min).nextUp ... Double(Self.max).nextDown, rather than Double(Self.min)... Double(Self.max)

Int.min                 -9,223,372,036,854,775,808 
Float(Int.min)          -9,223,372,036,854,780,000 lower than Int.min by 4096, thus not representable by Int
Float(Int.min).nextUp   -9,223,371,487,098,960,000 greater than Int.min by 549,755,820,032, thus representable by Int
Int.max                 +9,223,372,036,854,775,807  
Float(Int.max)          +9,223,372,036,854,780,000 greater than Int.max by 4096, thus not representable by Int
Float(Int.max).nextDown +9,223,371,487,098,960,000 lower than Int.max by 549,755,820,032, thus representable by Int

Here's what that looks like, in action

import Foundation

extension FixedWidthInteger {
    static var representableDoubles: ClosedRange<Double> {
        return Double(Self.min).nextUp ... Double(Self.max).nextDown
    }

    init?(safelyFromDouble d: Double) {
        guard Self.representableDoubles.contains(d) else { return nil }
        self.init(d)
    }
}

func formatDecimal(_ d: Double) -> String{
    let numberFormatter = NumberFormatter()
    numberFormatter.numberStyle = .decimal
    numberFormatter.positivePrefix = "+"
    return numberFormatter.string(from: NSNumber(value: d))!
}

let testCases: [Double] = [
    Double.nan,
    -Double.nan,
    Double.signalingNaN,
    -Double.signalingNaN,

    Double.infinity,
    Double(Int.max),
    Double(Int.max).nextDown,
    +1,
    +0.6,
    +0.5,
    +0.4,
    +0,
    -0,
    -0.4,
    -0.5,
    -0.6,
    -1,
    -1.5,
    Double(Int.min).nextUp,
    Double(Int.min),
    -Double.infinity,
]

for d in testCases {
    print("Double: \(formatDecimal(d)), as Int: \(Int(safelyFromDouble: d)as Any)")
}

which prints:

Double: NaN, as Int: nil
Double: NaN, as Int: nil
Double: NaN, as Int: nil
Double: NaN, as Int: nil
Double: +∞, as Int: nil
Double: +9,223,372,036,854,780,000, as Int: nil
Double: +9,223,372,036,854,770,000, as Int: Optional(9223372036854774784)
Double: +1, as Int: Optional(1)
Double: +0.6, as Int: Optional(0)
Double: +0.5, as Int: Optional(0)
Double: +0.4, as Int: Optional(0)
Double: +0, as Int: Optional(0)
Double: +0, as Int: Optional(0)
Double: -0.4, as Int: Optional(0)
Double: -0.5, as Int: Optional(0)
Double: -0.6, as Int: Optional(0)
Double: -1, as Int: Optional(-1)
Double: -1.5, as Int: Optional(-1)
Double: -9,223,372,036,854,770,000, as Int: Optional(-9223372036854774784)
Double: -9,223,372,036,854,780,000, as Int: nil
Double: -∞, as Int: nil

Upvotes: 3

ielyamani
ielyamani

Reputation: 18591

I think nil would be the appropriate return value of the conversion of Float.nan, Float.infinity, or outside the range Int.min...Int.max.

You could define a range in which the floating number should be included :

let validRange: ClosedRange<Float> = Float(Int.min)...Float(Int.max)

And use it like so :

func convert(_ f: Float) -> Int? {
    var optInteger: Int? = nil

    if  validRange.contains(f) {
        optInteger = Int(f)
    }

    return optInteger
}

print(convert(1.2))  //Optional(1)

You should take into consideration that the conversion between Float and Int loses precision with big values because of the limitation of the number of bits given to each type. For example, using @MartinR's Int.init(exactly:) isn't precise enough :

let x: Float = 9223371487098961919.0
if let i = Int(exactly: x.rounded()) {
    print(i) 
}

yields

9223371487098961920

A possible solution would be to use a more precise type. For example Float80 :

let validRange: ClosedRange<Float80> = Float80(Int.min)...Float80(Int.max)

func convert(_ f80: Float80) -> Int? {
    var optInteger: Int? = nil

    if  validRange.contains(f80) {
        optInteger = Int(f80)
    }

    return optInteger
}

Here are some use cases:

convert(Float80(Int.max))        //Optional(9223372036854775807)
convert(Float80(Int.max) - 1.0)  //Optional(9223372036854775806)
convert(Float80(Int.max) + 1.0)  //nil
convert(Float80(Int.min)))       //Optional(-9223372036854775808)
convert(Float80(Int.min) - 1.0)) //nil
convert(Float80.infinity)        //nil
convert(Float80.nan)             //nil

Upvotes: 0

Related Questions