Round Trip Swift Number Types To/From Data

round trip Swift number types to/from Data

Note: The code has been updated for Swift 5 (Xcode 10.2) now. (Swift 3 and Swift 4.2 versions can be found in the edit history.) Also possibly unaligned data is now correctly handled.

How to create Data from a value

As of Swift 4.2, data can be created from a value simply with

let value = 42.13
let data = withUnsafeBytes(of: value) { Data($0) }

print(data as NSData) // <713d0ad7 a3104540>

Explanation:

  • withUnsafeBytes(of: value)
    invokes the closure with a buffer pointer covering the raw bytes of the value.
  • A raw buffer pointer is a sequence of bytes, therefore Data($0) can be used to create the data.

How to retrieve a value from Data

As of Swift 5, the withUnsafeBytes(_:) of Data invokes the closure with an “untyped” UnsafeMutableRawBufferPointer to the bytes. The load(fromByteOffset:as:) method the reads the value from the memory:

let data = Data([0x71, 0x3d, 0x0a, 0xd7, 0xa3, 0x10, 0x45, 0x40])
let value = data.withUnsafeBytes {
$0.load(as: Double.self)
}
print(value) // 42.13

There is one problem with this approach: It requires that the memory is property aligned for the type (here: aligned to a 8-byte address). But that is not guaranteed, e.g. if the data was obtained as a slice of another Data value.

It is therefore safer to copy the bytes to the value:

let data = Data([0x71, 0x3d, 0x0a, 0xd7, 0xa3, 0x10, 0x45, 0x40])
var value = 0.0
let bytesCopied = withUnsafeMutableBytes(of: &value, { data.copyBytes(to: $0)} )
assert(bytesCopied == MemoryLayout.size(ofValue: value))
print(value) // 42.13

Explanation:

  • withUnsafeMutableBytes(of:_:) invokes the closure with a mutable buffer pointer covering the raw bytes of the value.
  • The copyBytes(to:) method of DataProtocol (to which Data conforms) copies bytes from the data to that buffer.

The return value of copyBytes() is the number of bytes copied. It is equal to the size of the destination buffer, or less if the data does not contain enough bytes.

Generic solution #1

The above conversions can now easily be implemented as generic methods of struct Data:

extension Data {

init<T>(from value: T) {
self = Swift.withUnsafeBytes(of: value) { Data($0) }
}

func to<T>(type: T.Type) -> T? where T: ExpressibleByIntegerLiteral {
var value: T = 0
guard count >= MemoryLayout.size(ofValue: value) else { return nil }
_ = Swift.withUnsafeMutableBytes(of: &value, { copyBytes(to: $0)} )
return value
}
}

The constraint T: ExpressibleByIntegerLiteral is added here so that we can easily initialize the value to “zero” – that is not really a restriction because this method can be used with “trival” (integer and floating point) types anyway, see below.

Example:

let value = 42.13 // implicit Double
let data = Data(from: value)
print(data as NSData) // <713d0ad7 a3104540>

if let roundtrip = data.to(type: Double.self) {
print(roundtrip) // 42.13
} else {
print("not enough data")
}

Similarly, you can convert arrays to Data and back:

extension Data {

init<T>(fromArray values: [T]) {
self = values.withUnsafeBytes { Data($0) }
}

func toArray<T>(type: T.Type) -> [T] where T: ExpressibleByIntegerLiteral {
var array = Array<T>(repeating: 0, count: self.count/MemoryLayout<T>.stride)
_ = array.withUnsafeMutableBytes { copyBytes(to: $0) }
return array
}
}

Example:

let value: [Int16] = [1, Int16.max, Int16.min]
let data = Data(fromArray: value)
print(data as NSData) // <0100ff7f 0080>

let roundtrip = data.toArray(type: Int16.self)
print(roundtrip) // [1, 32767, -32768]

Generic solution #2

The above approach has one disadvantage: It actually works only with "trivial"
types like integers and floating point types. "Complex" types like Array
and String have (hidden) pointers to the underlying storage and cannot be
passed around by just copying the struct itself. It also would not work with
reference types which are just pointers to the real object storage.

So solve that problem, one can

  • Define a protocol which defines the methods for converting to Data and back:

    protocol DataConvertible {
    init?(data: Data)
    var data: Data { get }
    }
  • Implement the conversions as default methods in a protocol extension:

    extension DataConvertible where Self: ExpressibleByIntegerLiteral{

    init?(data: Data) {
    var value: Self = 0
    guard data.count == MemoryLayout.size(ofValue: value) else { return nil }
    _ = withUnsafeMutableBytes(of: &value, { data.copyBytes(to: $0)} )
    self = value
    }

    var data: Data {
    return withUnsafeBytes(of: self) { Data($0) }
    }
    }

    I have chosen a failable initializer here which checks that the number of bytes provided
    matches the size of the type.

  • And finally declare conformance to all types which can safely be converted to Data and back:

    extension Int : DataConvertible { }
    extension Float : DataConvertible { }
    extension Double : DataConvertible { }
    // add more types here ...

This makes the conversion even more elegant:

let value = 42.13
let data = value.data
print(data as NSData) // <713d0ad7 a3104540>

if let roundtrip = Double(data: data) {
print(roundtrip) // 42.13
}

The advantage of the second approach is that you cannot inadvertently do unsafe conversions. The disadvantage is that you have to list all "safe" types explicitly.

You could also implement the protocol for other types which require a non-trivial conversion, such as:

extension String: DataConvertible {
init?(data: Data) {
self.init(data: data, encoding: .utf8)
}
var data: Data {
// Note: a conversion to UTF-8 cannot fail.
return Data(self.utf8)
}
}

or implement the conversion methods in your own types to do whatever is
necessary so serialize and deserialize a value.

Byte order

No byte order conversion is done in the above methods, the data is always in
the host byte order. For a platform independent representation (e.g.
“big endian” aka “network” byte order), use the corresponding integer
properties resp. initializers. For example:

let value = 1000
let data = value.bigEndian.data
print(data as NSData) // <00000000 000003e8>

if let roundtrip = Int(data: data) {
print(Int(bigEndian: roundtrip)) // 1000
}

Of course this conversion can also be done generally, in the generic
conversion method.

How can I convert data into types like Doubles, Ints and Strings in Swift?

Xcode 11 • Swift 5.1 or later

To convert from String or any Numeric type to Data:

extension StringProtocol {
var data: Data { .init(utf8) }
}

extension Numeric {
var data: Data {
var source = self
// This will return 1 byte for 8-bit, 2 bytes for 16-bit, 4 bytes for 32-bit and 8 bytes for 64-bit binary integers. For floating point types it will return 4 bytes for single-precision, 8 bytes for double-precision and 16 bytes for extended precision.
return .init(bytes: &source, count: MemoryLayout<Self>.size)
}
}

To convert from Data (bytes) back to String

extension DataProtocol {
var string: String? { String(bytes: self, encoding: .utf8) }
}

To convert from Data back to generic Numeric value

extension Numeric {
init<D: DataProtocol>(_ data: D) {
var value: Self = .zero
let size = withUnsafeMutableBytes(of: &value, { data.copyBytes(to: $0)} )
assert(size == MemoryLayout.size(ofValue: value))
self = value
}
}

extension DataProtocol {
func value<N: Numeric>() -> N { .init(self) }
}

let value = 12.34                      // implicit Double 12.34
let data = value.data // double data - 8 bytes
let double = Double(data) // implicit Double 12.34
let double1: Double = .init(data) // explicit Double 12.34
let double2: Double = data.value() // explicit Double 12.34
let double3 = data.value() as Double // casting to Double 12.34

Now we can easily add a property for each Numeric type:

extension DataProtocol {
var integer: Int { value() }
var int32: Int32 { value() }
var float: Float { value() }
var cgFloat: CGFloat { value() }
var float80: Float80 { value() }
var double: Double { value() }
var decimal: Decimal { value() }
}

Playground testing

let intData = 1_234_567_890_123_456_789.data    // 8 bytes (64 bit Integer)
let dataToInt: Int = intData.integer // 1234567890123456789

let intMinData = Int.min.data // 8 bytes (64 bit Integer)
let backToIntMin = intMinData.integer // -9223372036854775808

let intMaxData = Int.max.data // 8 bytes (64 bit Integer)
let backToIntMax = intMaxData.integer // 9223372036854775807

let myInt32Data = Int32(1_234_567_890).data     // 4 bytes (32 bit Integer)
let backToInt32 = myInt32Data.int32 // 1234567890

let int32MinData = Int32.min.data // 4 bytes (32 bit Integer)
let backToInt32Min = int32MinData.int32 // -2147483648

let int32MaxData = Int32.max.data // 4 bytes (32 bit Integer)
let backToInt32Max = int32MaxData.int32 // 2147483647

let myFloatData = Float.pi.data                 // 4 bytes (32 bit single=precison FloatingPoint)
let backToFloat = myFloatData.float // 3.141593
backToFloat == .pi // true

let myCGFloatData = CGFloat.pi.data // 4 bytes (32 bit single=precison FloatingPoint)
let backToCGFloat = myCGFloatData.cgFloat // 3.141593
backToCGFloat == .pi // true

let myDoubleData = Double.pi.data // 8 bytes (64 bit double-precision FloatingPoint)
let backToDouble = myDoubleData.double // 3.141592653589793
backToDouble == .pi // true

let myFloat80Data = Float80.pi.data // 16 bytes (128 bit extended-precision FloatingPoint)
let backToFloat80 = myFloat80Data.float80 // 3.141592653589793116
backToFloat80 == .pi // true

let decimalData = Decimal.pi.data // 20 bytes Decimal type
let backToDecimal = decimalData.decimal // 3.14159265358979323846264338327950288419
backToDecimal == .pi // true

let stringBytes = "Hello World !!!".data.prefix(4)  // 4 bytes
let backToString = stringBytes.string // "Hell"

How to convert Data to Int in Swift 3?

Maybe try like this:

var src: Int = 12345678
var num: Int = 0 // initialize

let data = NSData(bytes: &src, length: MemoryLayout<Int>.size)
data.getBytes(&num, length: MemoryLayout<Int>.size)
print(num) // 12345678

Convert Data Object to array of structs

You should now use the new withUnsafeBytes that takes a closure that takes a UnsafeRawBufferPointer as argument. You now don't need to do all the MemoryLayout calculations by hand, and can just bind the memory directly to the desired type.

pointData.withUnsafeBytes { rawBufferPointer in
newPoints = Array(rawBufferPointer.bindMemory(to: Point.self))
}

Convert UInt32 to 4 bytes Swift

let value: UInt32 = 1
var u32LE = value.littleEndian // or simply value
let dataLE = Data(bytes: &u32LE, count: 4)
let bytesLE = Array(dataLE) // [1, 0, 0, 0]

var u32BE = value.bigEndian
let dataBE = Data(bytes: &u32BE, count: 4)
let bytesBE = Array(dataBE) // [0, 0, 0, 1]

How to convert `Date` to `Data`, and vice versa in Swift 5?

A Date is a juste a Timestamp (number of seconds since reference date of 1970 at UTC) as an object with fancy methods and other utilities. That's nothing more.

So based on the answer Double to Data (and reverse):

Input

let date = Date()
print(date)

Date -> Timestamp -> Data

let timestamp = date.timeIntervalSinceReferenceDate
print(timestamp)
let data = withUnsafeBytes(of: timestamp) { Data($0) }
print("\(data) - \(data.map{ String(format: "%02hhx", $0) }.joined())")

Data -> Timestamp -> Date

let retrievedTimestamp = data.withUnsafeBytes { $0.load(as: Double.self) }
print(retrievedTimestamp)
let retrievedDate = Date(timeIntervalSinceReferenceDate: retrievedTimestamp)
print(retrievedDate)

Output:

$>2021-09-13 08:17:50 +0000
$>1631521070.6852288
$>8 bytes - cadaab4bc24fd841
$>1631521070.6852288
$>2021-09-13 08:17:50 +0000


Related Topics



Leave a reply



Submit