How to convert Data to hex string in swift
Asked Answered
A

8

109

I want the hexadecimal representation of a Data value in Swift.

Eventually I'd want to use it like this:

let data = Data(base64Encoded: "aGVsbG8gd29ybGQ=")!
print(data.hexString)
Albinus answered 22/8, 2016 at 8:51 Comment(0)
E
248

A simple implementation (taken from How to hash NSString with SHA1 in Swift?, with an additional option for uppercase output) would be

extension Data {
    struct HexEncodingOptions: OptionSet {
        let rawValue: Int
        static let upperCase = HexEncodingOptions(rawValue: 1 << 0)
    }

    func hexEncodedString(options: HexEncodingOptions = []) -> String {
        let format = options.contains(.upperCase) ? "%02hhX" : "%02hhx"
        return self.map { String(format: format, $0) }.joined()
    }
}

I chose a hexEncodedString(options:) method in the style of the existing method base64EncodedString(options:).

Data conforms to the Collection protocol, therefore one can use map() to map each byte to the corresponding hex string. The %02x format prints the argument in base 16, filled up to two digits with a leading zero if necessary. The hh modifier causes the argument (which is passed as an integer on the stack) to be treated as a one byte quantity. One could omit the modifier here because $0 is an unsigned number (UInt8) and no sign-extension will occur, but it does no harm leaving it in.

The result is then joined to a single string.

Example:

let data = Data([0, 1, 127, 128, 255])
// For Swift < 4.2 use:
// let data = Data(bytes: [0, 1, 127, 128, 255])
print(data.hexEncodedString()) // 00017f80ff
print(data.hexEncodedString(options: .upperCase)) // 00017F80FF

The following implementation is faster by a factor about 50 (tested with 1000 random bytes). It is inspired to RenniePet's solution and Nick Moore's solution, but takes advantage of String(unsafeUninitializedCapacity:initializingUTF8With:) which was introduced with Swift 5.3/Xcode 12 and is available on macOS 11 and iOS 14 or newer.

This method allows to create a Swift string from UTF-8 units efficiently, without unnecessary copying or reallocations.

An alternative implementation for older macOS/iOS versions is also provided.

extension Data {
    struct HexEncodingOptions: OptionSet {
        let rawValue: Int
        static let upperCase = HexEncodingOptions(rawValue: 1 << 0)
    }

    func hexEncodedString(options: HexEncodingOptions = []) -> String {
        let hexDigits = options.contains(.upperCase) ? "0123456789ABCDEF" : "0123456789abcdef"
        if #available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) {
            let utf8Digits = Array(hexDigits.utf8)
            return String(unsafeUninitializedCapacity: 2 * self.count) { (ptr) -> Int in
                var p = ptr.baseAddress!
                for byte in self {
                    p[0] = utf8Digits[Int(byte / 16)]
                    p[1] = utf8Digits[Int(byte % 16)]
                    p += 2
                }
                return 2 * self.count
            }
        } else {
            let utf16Digits = Array(hexDigits.utf16)
            var chars: [unichar] = []
            chars.reserveCapacity(2 * self.count)
            for byte in self {
                chars.append(utf16Digits[Int(byte / 16)])
                chars.append(utf16Digits[Int(byte % 16)])
            }
            return String(utf16CodeUnits: chars, count: chars.count)
        }
    }
}
Excelsior answered 17/10, 2016 at 14:46 Comment(11)
While I typically don't like an extension on an Apple class when a func can be used I love the symmetry with base64EncodedString.Burdett
@reza_khalafi: There are many solutions for Objective-C, for example here: #1305725.Excelsior
Can you also provide a reverse operation from string to Hex dataCotopaxi
Is this answer works for utf8? Because my server only accepts utf8 characters.Rocketry
@MiladFaridnia: I do not understand your question. Data contains arbitrary bytes and has no encoding.Excelsior
You have mentioned "based on UTF-16 code units" , I want to send this data to server which only accepts utf-8 code units. I doubt it may not compatible with that server.Rocketry
@MiladFaridnia: UTF-16 is what Swift strings use internally. The above function returns a string containing only digits 0...9 and the letters A...F, so that should not be a problem.Excelsior
@IulianOnofrei: May I ask why you added self. at various places in the code? Were there any problems in compiling (or running) the code as it was before?Excelsior
@MartinR, I added them to make properties accessing explicit, hence, easier to understand for beginners, which might not know (yet) about the fact that you can omit them and have the same result.Wiley
This does not appear to work for me. I keep getting leading 0 or sublast character as 0 in some cases. Example: 0x1234 becomes 0x01234, 0x1 becomes 0x01Jarlen
@iOSBlacksmith: This code takes a Data object (a byte buffer) and produces a hexadecimal string representation. It will always output an even number of characters, two characters (00..FF) per byte. But I don't see how my function could output 0x01234. Can you give a short self-contained code example?Excelsior
A
45

This code extends the Data type with a computed property. It iterates through the bytes of data and concatenates the byte's hex representation to the result:

extension Data {
    var hexDescription: String {
        return reduce("") {$0 + String(format: "%02x", $1)}
    }
}
Albinus answered 22/8, 2016 at 8:51 Comment(4)
My favorite (from https://mcmap.net/q/196525/-how-to-hash-nsstring-with-sha1-in-swift): return map { String(format: "%02hhx", $0) }.joined()Excelsior
"For every byte, there is a string allocation and copy operation being performed." I don't think that's correct. I think Swift does a fairly good job of optimizing String manipulation. https://mcmap.net/q/196526/-fastest-leanest-way-to-append-characters-to-form-a-string-in-swiftJammie
I agree with your edit, removing the warning about poor efficiency. So my above comment should be ignored, but I'll leave it there for the sake of posterity.Jammie
I like this approach better as I assume it takes one less pass through the collection.Tawnatawney
P
31

My version. It's about 10 times faster than the [original] accepted answer by Martin R.

public extension Data {
    private static let hexAlphabet = Array("0123456789abcdef".unicodeScalars)
    func hexStringEncoded() -> String {
        String(reduce(into: "".unicodeScalars) { result, value in
            result.append(Self.hexAlphabet[Int(value / 0x10)])
            result.append(Self.hexAlphabet[Int(value % 0x10)])
        })
    }
}
Pediment answered 24/11, 2017 at 16:6 Comment(1)
Was able to make this a tidbit better if you wanna pull my changes into this answer: gist.github.com/BenLeggiero/916bf788000736a7c0e6d1cad6e54410Plato
E
16

Swift 4 - From Data to Hex String
Based upon Martin R's solution but even a tiny bit faster.

extension Data {
  /// A hexadecimal string representation of the bytes.
  func hexEncodedString() -> String {
    let hexDigits = Array("0123456789abcdef".utf16)
    var hexChars = [UTF16.CodeUnit]()
    hexChars.reserveCapacity(count * 2)

    for byte in self {
      let (index1, index2) = Int(byte).quotientAndRemainder(dividingBy: 16)
      hexChars.append(hexDigits[index1])
      hexChars.append(hexDigits[index2])
    }

    return String(utf16CodeUnits: hexChars, count: hexChars.count)
  }
}

Swift 4 - From Hex String to Data
I've also added a fast solution for converting a hex String into Data (based on a C solution).

extension String {
  /// A data representation of the hexadecimal bytes in this string.
  func hexDecodedData() -> Data {
    // Get the UTF8 characters of this string
    let chars = Array(utf8)

    // Keep the bytes in an UInt8 array and later convert it to Data
    var bytes = [UInt8]()
    bytes.reserveCapacity(count / 2)

    // It is a lot faster to use a lookup map instead of strtoul
    let map: [UInt8] = [
      0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // 01234567
      0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 89:;<=>?
      0x00, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x00, // @ABCDEFG
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00  // HIJKLMNO
    ]

    // Grab two characters at a time, map them and turn it into a byte
    for i in stride(from: 0, to: count, by: 2) {
      let index1 = Int(chars[i] & 0x1F ^ 0x10)
      let index2 = Int(chars[i + 1] & 0x1F ^ 0x10)
      bytes.append(map[index1] << 4 | map[index2])
    }

    return Data(bytes)
  }
}

Note: this function does not validate the input. Make sure that it is only used for hexadecimal strings with (an even amount of) characters.

Erelong answered 2/10, 2018 at 1:7 Comment(0)
S
7

Backward compatible and fast solution:

extension Data {
    /// Fast convert to hex by reserving memory (instead of mapping and join).
    public func toHex(uppercase: Bool = false) -> String {
        // Constants (Hex has 2 characters for each Byte).
        let size = self.count * 2;
        let degitToCharMap = Array((
            uppercase ? "0123456789ABCDEF" : "0123456789abcdef"
        ).utf16);
        // Reserve dynamic memory (plus one for null termination).
        let buffer = UnsafeMutablePointer<unichar>.allocate(capacity: size + 1);
        // Convert each byte.
        var index = 0
        for byte in self {
            buffer[index] = degitToCharMap[Int(byte / 16)];
            index += 1;
            buffer[index] = degitToCharMap[Int(byte % 16)];
            index += 1;
        }
        // Set Null termination.
        buffer[index] = 0;
        // Casts to string (without any copying).
        return String(utf16CodeUnitsNoCopy: buffer,
                      count: size, freeWhenDone: true)
    }
}

Note that above passes ownership of buffer to returned String object.

Also know that, because Swift's internal String data is UTF16 (but can be UTF8 since Swift 5), all solutions provided in accepted answer do full copy (and are slower), at least if NOT #available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) ;-)

As mentioned on my profile, usage under Apache 2.0 license is allowed too (without attribution need).

Sherrer answered 7/10, 2021 at 7:36 Comment(0)
J
5

This doesn't really answer the OP's question since it works on a Swift byte array, not a Data object. And it's much bigger than the other answers. But it should be more efficient since it avoids using String(format: ).

Anyway, in the hopes someone finds this useful ...

public class StringMisc {

   // MARK: - Constants

   // This is used by the byteArrayToHexString() method
   private static let CHexLookup : [Character] =
      [ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F" ]


   // Mark: - Public methods

   /// Method to convert a byte array into a string containing hex characters, without any
   /// additional formatting.
   public static func byteArrayToHexString(_ byteArray : [UInt8]) -> String {

      var stringToReturn = ""

      for oneByte in byteArray {
         let asInt = Int(oneByte)
         stringToReturn.append(StringMisc.CHexLookup[asInt >> 4])
         stringToReturn.append(StringMisc.CHexLookup[asInt & 0x0f])
      }
      return stringToReturn
   }
}

Test case:

  // Test the byteArrayToHexString() method
  let byteArray : [UInt8] = [ 0x25, 0x99, 0xf3 ]
  assert(StringMisc.byteArrayToHexString(byteArray) == "2599F3")
Jammie answered 7/2, 2017 at 1:5 Comment(1)
Here's a more Swifty version of this answer: gist.github.com/BenLeggiero/ce1e62fe0194ca969eb7cdda6639a011Plato
K
1

A bit different from other answers here:

extension DataProtocol {
    func hexEncodedString(uppercase: Bool = false) -> String {
        return self.map {
            if $0 < 16 {
                return "0" + String($0, radix: 16, uppercase: uppercase)
            } else {
                return String($0, radix: 16, uppercase: uppercase)
            }
        }.joined()
    }
}

However in my basic XCTest + measure setup this was fastest of the 4 I tried.

Going through a 1000 bytes of (the same) random data 100 times each:

Above: Time average: 0.028 seconds, relative standard deviation: 1.3%

MartinR: Time average: 0.037 seconds, relative standard deviation: 6.2%

Zyphrax: Time average: 0.032 seconds, relative standard deviation: 2.9%

NickMoore: Time average: 0.039 seconds, relative standard deviation: 2.0%

Repeating the test returned the same relative results. (Nick and Martins sometimes swapped)

Edit: Nowadays I use this:

    var hexEncodedString: String {
        return self.reduce(into:"") { result, byte in
            result.append(String(byte >> 4, radix: 16))
            result.append(String(byte & 0x0f, radix: 16))
        }
    }
Kapoor answered 19/6, 2020 at 7:8 Comment(0)
C
-1

Maybe not the fastest, but data.map({ String($0, radix: 16) }).joined() does the job. As mentioned in the comments, this solution was flawed.

Converter answered 11/2, 2019 at 16:53 Comment(1)
That is problematic because no leading zeros are inserted for single-digit hex numbers. Example: For Data(bytes: [0x11, 0x02, 0x03, 0x44]) it returns the string "112344" instead of "11020344".Excelsior

© 2022 - 2024 — McMap. All rights reserved.