2

Is there a way to do what we can easily achieve with to_bytes with python, in Swift?

In a few words I need to define the length of a byte sequence, filling the leading elements with 0s.

As an example, if I have to describe the number 5 with a sequence of 4 bytes I should get it as \x00\x00\x00\x05.

with the python to_bytes function I can easily use the first parameter to define the sequence length and I could write it like (5).to_bytes(4, byteorder='big') I can't find a way to easily obtain the same result with swift.

vacawama
  • 150,663
  • 30
  • 266
  • 294
MatterGoal
  • 16,038
  • 19
  • 109
  • 186
  • There is no “easy way” in the sense of a built-in function, as far as I know, but it should not be too difficult to implement (extract the lowest byte and divide by 256 in a loop). [This](https://stackoverflow.com/q/38023838/1187415) is remotely related, but converts the integers to `Data` with their native sizes. – Martin R Oct 19 '19 at 11:27

1 Answers1

2

Note: Python's to_bytes can take any arbitrary length. If you want to convert a value to its native size (eg. 1 byte for Int8, 2 bytes for Int16, 4 bytes for Int32, etc.), check out round trip Swift number types to/from Data and How can I convert data into types like Doubles, Ints and Strings in Swift?

For any arbitrary length with sign extension (for negative numbers) you can use & 255 to extract the lowest order byte and >> 8 to shift the value right 8 bits repeatedly in a loop to compute the bytes. (Here I used map to generate the array of bytes). Then use reversed() to put them in the desired big endian order:

var i = 5
let len = 4

let arr: [UInt8] = (0..<len).map { _ in
    let byte = UInt8(i & 255)
    i >>= 8
    return byte }
    .reversed()

print(arr)

Output:

[0, 0, 0, 5]

Notes:

  • >>= will sign extend a negative number, so -5 would give the expected 2's complement result: [255, 255, 255, 251].
  • Explicitly typing arr as [UInt8] ensures that arr is [UInt8] and not ReversedCollection<Array<UInt8>> and it aids Swift in the determination of the return type of map.
  • For little endian order, remove reversed() and just use the result of the map.

Implementing toBytes(length:bigEndian:) as an extension of Int:

This can be added as an extension to Int to further mimic the behavior of Python's to_bytes method:

extension Int {
    func toBytes(length: Int, bigEndian: Bool = true) -> [UInt8] {
        var i = self
        let bytes: [UInt8] = (0..<length).map { _ in
            let byte = UInt8(i & 255)
            i >>= 8
            return byte
        }

        return bigEndian ? bytes.reversed() : bytes
    }
}

Examples:

print(5.toBytes(length: 4))
[0, 0, 0, 5]
print((-5).toBytes(length: 4))
[255, 255, 255, 251]
print(5.toBytes(length: 8))
[0, 0, 0, 0, 0, 0, 0, 5]
print(5.toBytes(length: 8, bigEndian: false))
[5, 0, 0, 0, 0, 0, 0, 0]

Extending toBytes to work with any Int type

Simply extending FixedWidthInteger instead of Int makes this work for all Int and UInt types except for Int8 which doesn't handle the sign extension correctly. Checking for that type explicitly and converting it to an Int solves that problem.

extension FixedWidthInteger {
    func toBytes(length: Int, bigEndian: Bool = true) -> [UInt8] {
        if self is Int8 {
            return Int(self).toBytes(length: length, bigEndian: bigEndian)
        }

        var i = self
        let bytes: [UInt8] = (0..<length).map { _ in
            let byte = UInt8(i & 255)
            i >>= 8
            return byte
        }

        return bigEndian ? bytes.reversed() : bytes
    }
}

Examples:

print(Int8(-5).toBytes(length: 10))
print(Int16(-5).toBytes(length: 10))
print(Int32(-5).toBytes(length: 10))
print(Int64(-5).toBytes(length: 10))
[255, 255, 255, 255, 255, 255, 255, 255, 255, 251]
[255, 255, 255, 255, 255, 255, 255, 255, 255, 251]
[255, 255, 255, 255, 255, 255, 255, 255, 255, 251]
[255, 255, 255, 255, 255, 255, 255, 255, 255, 251]
print(Int8(5).toBytes(length: 10))
print(Int16(5).toBytes(length: 10))
print(Int32(5).toBytes(length: 10))
print(Int64(5).toBytes(length: 10))
print(UInt8(5).toBytes(length: 10))
print(UInt16(5).toBytes(length: 10))
print(UInt32(5).toBytes(length: 10))
print(UInt64(5).toBytes(length: 10))
[0, 0, 0, 0, 0, 0, 0, 0, 0, 5]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 5]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 5]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 5]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 5]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 5]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 5]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 5]
print(UInt64.max.toBytes(length: 10))
[0, 0, 255, 255, 255, 255, 255, 255, 255, 255]
vacawama
  • 150,663
  • 30
  • 266
  • 294
  • The array initialiser is not needed. Btw it would be better to extend `FixedWidthInteger` – Leo Dabus Oct 20 '19 at 01:47
  • Thanks @LeoDabus. I added the array initializer in the first case because I was ending up with `ReversedCollection>(_base: [5, 0, 0, 0])` as the result instead of `[0, 0, 0, 5]`. Explicitly setting the type of `arr` as `[UInt8]` solved that problem though. `FixedWidthInteger` is a good idea, but it will still need a little work to do the right thing in the case of `UInt8`. – vacawama Oct 20 '19 at 04:15
  • I meant `FixedWidthInteger` doesn't do the right thing with `Int8` and a negative number. – vacawama Oct 20 '19 at 04:32
  • The generic approach. `extension Sequence where Element == UInt8 {` `var array: [UInt8] { .init(self) }` `}` `extension Numeric {` `var data: Data {` `var source = self` `return .init(bytes: &source, count: MemoryLayout.size)` `}` `}` `Int32(-5).bigEndian.data.array` // [255, 255, 255, 251] https://stackoverflow.com/a/43244973/2303865 – Leo Dabus Oct 20 '19 at 11:32