Skip to content

Floating point to integer conversion in Swift

Mert Buran edited this page Apr 10, 2020 · 3 revisions

Real quick floating point recap

TL;DR: FPs can go up to very very large numbers

Binary representation

Unsigned binary 4-bit: 1111 = 1 + 2 + 4 + 8 = 15 = 2**4 - 1

FP representation

FPs have 3 components:

  1. radix
  2. significand
  3. exponent

let magnitude = x.significand * F.radix ** x.exponent

NOTE: radix is 2 for Float and Double

FP with unsigned 4-bit exponent: 2 ** (2 ** 15 - 1)

..so what?

So, that means it is not trivial to tell if the conversion is going to succeed or not before actually executing it.

Click for the actual code doing the conversion

Source: Swift Standard Library

static func _convert<Source: BinaryFloatingPoint>(
    from source: Source
  ) -> (value: Self?, exact: Bool) {
    guard _fastPath(!source.isZero) else { return (0, true) }
    guard _fastPath(source.isFinite) else { return (nil, false) }
    guard Self.isSigned || source > -1 else { return (nil, false) }
    let exponent = source.exponent
    if _slowPath(Self.bitWidth <= exponent) { return (nil, false) }
    let minBitWidth = source.significandWidth
    let isExact = (minBitWidth <= exponent)
    let bitPattern = source.significandBitPattern
    // `RawSignificand.bitWidth` is not available if `RawSignificand` does not
    // conform to `FixedWidthInteger`; we can compute this value as follows if
    // `source` is finite:
    let bitWidth = minBitWidth &+ bitPattern.trailingZeroBitCount
    let shift = exponent - Source.Exponent(bitWidth)
    // Use `Self.Magnitude` to prevent sign extension if `shift < 0`.
    let shiftedBitPattern = Self.Magnitude.bitWidth > bitWidth
      ? Self.Magnitude(truncatingIfNeeded: bitPattern) << shift
      : Self.Magnitude(truncatingIfNeeded: bitPattern << shift)
    if _slowPath(Self.isSigned && Self.bitWidth &- 1 == exponent) {
      return source < 0 && shiftedBitPattern == 0
        ? (Self.min, isExact)
        : (nil, false)
    }
    let magnitude = ((1 as Self.Magnitude) << exponent) | shiftedBitPattern
    return (
      Self.isSigned && source < 0 ? 0 &- Self(magnitude) : Self(magnitude),
      isExact)
  }

Given the complexity of the case; instead of using init(:) method which may call fatalError, we use init(exactly:) failable initializer so that we can catch the error state (ie: returned nil) and recover from it.

extension FixedWidthInteger {
    init<T: BinaryFloatingPoint>(withReportingOverflow floatingPoint: T) throws {
        guard let converted = Self(exactly: floatingPoint.rounded()) else {
            throw CustomErrorWhichCouldBeNamedOverflowErrorOrSmthElse()
        }
        self = converted
    }
}