Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Safely down casting from Int to Int8 in Swift

I receive a set of integers via an api json response (which I convert into a [String: Any] dictionary). These integers are guaranteed to be in the range-10...-10 (inclusively). In my model I'd like to store these as Int8.

This is how I'm converting my json dictionary to it's model object but I'm wondering if there is a more idiomatic way of ensuring the ints in the json actually fit in an Int8?

func move(with dict: JSONDictionary) -> Move? {
    if let rowOffset = dict[JSONKeys.rowOffsetKey] as? Int,
       let colOffset = dict[JSONKeys.colOffsetKey] as? Int {

      if let rowInt8 = Int8(exactly: rowOffset),
         let colInt8 = Int8(exactly: colOffset) {

        return Move(rowOffset: rowInt8, colOffset: colInt8)
      }
      else {
        print("Error: values out of range: (row: \(rowOffset), col: \(colOffset)")
      }
    } else {
      print("Error: Missing key, either: \(JSONKeys.rowOffsetKey) or \(JSONKeys.colOffsetKey)")
    }

    return nil
  }

Note that doing the following always fails regardless of the value of the incoming ints:

if let rowOffset = dict[JSONKeys.rowOffsetKey] as? Int8,
   let colOffset = dict[JSONKeys.colOffsetKey] as? Int8 {
...

This is how I'm converting the incoming json to a dictionary. The json in question is deeply nested and contains several different types.

typealias JSONDictionary = [String: Any]
let jsonDict = try JSONSerialization.jsonObject(with: data) as? JSONDictionary
like image 892
RobertJoseph Avatar asked Jan 29 '17 13:01

RobertJoseph


2 Answers

Here are some options for converting your Int (which is coming from an NSNumber) into an Int8:

Just converting with Int8's initializer init(_: Int)

If your Int values are guaranteed to fit into an Int8, then converting them with Int8(value) is fine.

If you get an Int that doesn't fit in an Int8, your program will crash:

let i = 300
let j = Int8(i)  // crash!

Initializing with Int8's initializer init(truncatingIfNeeded: BinaryInteger)

For a bit of extra safety, you should use the init(truncatingIfNeeded: BinaryInteger) initializer:

let i = 300
let j = Int8(truncatingIfNeeded: i)  // j = 44

This does generate altered values which might not be desirable, but it would prevent a crash.

Explicitly checking the range of valid values

As another alternative, you could just check the range explicitly:

if (-10...10).contains(i) {
    j = Int8(i)
} else {
    // do something with the error case
}

The advantage of checking is that you can specify the valid range instead of just detecting an error when the range exceeds the values that will fit in an Int8.

Initializing with Int8's initializer init(exactly: Int)

Your code is currently using this method of safely initializing the Int8 values. This is robust since it will return nil if the value does not fit in an Int8. Thus it can checked with optional binding as you are doing, or it can be combined with the nil coalescing operator ?? to provide default values if you have appropriate ones:

// Determine Int8 value, use 0 if value would overflow an Int8
let j = Int8(exactly: i) ?? 0

Directly casting NSNumber with as Int8 in Swift 3.0.1 and above

As @Hamish and @OOPer mentioned in the comments, it is now possible to cast an NSNumber directly to Int8.

let i: NSNumber = 300
let j = i as Int8  // j = 44

This does have the same truncating effect as using init(truncatingIfNeeded: BinaryInteger).

In conclusion, your current code is safe, and probably is the best solution for your problem unless you would like to detect when the values exceed the current desired range of -10...10, in which case explicitly checking would be the better option.

like image 81
vacawama Avatar answered Oct 09 '22 01:10

vacawama


As said @vacawama

let j = Int8(exactly: i) ?? 0 

is a perfect and safe way to convert Int to UInt8, but sometimes I need to get max and min values of UInt8 instead of zero. I'm using extension:

extension UInt8 {

    /// Safely converts Int to UInt8, truncate remains that do not fit in UInt8.
    /// For instance, if Int value is 300, UInt8 will be 255, or if Int value is -100, UInt8 value will be 0
    init(truncateToFit int: Int) {
        switch int {
        case _ where int < UInt8.min: self.init(UInt8.min)
        case _ where int > UInt8.max: self.init(UInt8.max)
        default: self.init(int)
        }
    }

    /// Safely converts Float to UInt8, truncate remains that do not fit in UInt8.
    /// For instance, if Float value is 300.934, UInt8 will be 255, or if Float value is -100.2342, UInt8 value will be 0
    init(truncateToFit float: Float) {
        switch float {
        case _ where float < Float(UInt8.min): self.init(UInt8.min)
        case _ where float > Float(UInt8.max): self.init(UInt8.max)
        default: self.init(float)
        }
    }

    /// Safely converts Double to UInt8, truncate remains that do not fit in UInt8.
    /// For instance, if Double value is 300.934, UInt8 will be 255, or if Double value is -100.2342, UInt8 value will be 0
    init(truncateToFit double: Double) {
        switch double {
        case _ where double < Double(UInt8.min): self.init(UInt8.min)
        case _ where double > Double(UInt8.max): self.init(UInt8.max)
        default: self.init(double)
        }
    }
}

For instance:

    let value = UInt8(truncateToFit: Int.max) // value == 255

UPDATE:

I'm found standard realization for all numbers conform to BinaryInteger protocol, such that Int, Int8, Int16, Int32, and so on.

let value = UInt8(clamping: 500) // value == 255
let secondValue = UInt8(clamping: -500) // secondValue == 0

But for Double and Float, there is elegance solution, one extension for all BinaryInteger types

extension BinaryInteger where Self: FixedWidthInteger {

    /// Safely converts Float to BinaryInteger (Uint8, Uint16, Int8, and so on), truncate remains that do not fit in the instance of BinaryInteger range value.
    /// For instance, if Float value is 300.934, and self is UInt8, it will be 255, or if Float value is -100.2342, self value will be 0
    init(truncateToFit float: Float) {
        switch float {
        case _ where float < Float(Self.min): self.init(Self.min)
        case _ where float > Float(Self.max): self.init(Self.max)
        default: self.init(float)
        }
    }

    /// Safely converts Double to BinaryInteger (Uint8, Uint16, Int8, and so on), truncate remains that do not fit in the instance of BinaryInteger range value.
    /// For instance, if Double value is 300.934, and self is UInt8, it will be 255, or if Float value is -100.2342, self value will be 0
    init(truncateToFit double: Double) {
        switch double {
        case _ where double < Double(Self.min): self.init(Self.min)
        case _ where double > Double(Self.max): self.init(Self.max)
        default: self.init(double)
        }
    }
}

For instance:

let valueUInt16 = UInt16(truncateToFit: 5000000.0) // valueUInt16 == 65535
let valueInt8 = Int8(truncateToFit: 5000000.0)  // valueInt8 == 127
let valueUInt8 = UInt8(truncateToFit: -500.0)   // valueUInt8 == 0
like image 24
SilentGnom Avatar answered Oct 09 '22 00:10

SilentGnom