Search code examples
arraysswiftstringradix

Swift: Create arbitrary base string from byte array


I have an array of UInt8 that I need to turn into a string of base 36. I believe I need to convert it into something conforming to Binary Integer, but not sure how.

let bArray: [UInt8] = ..... //some array of UInt8
let foo = ? // bArray -> a binary integer ... or another step?
let baseString = String(foo, radix: 36, uppercase: false)

Is this the correct process or should I be using a different approach?


Sample data

The string "test" gets hashed using SHA1 to get byte array:

[169, 74, 143, 229, 204, 177, 155, 166, 28, 76, 8, 115, 211, 145, 233, 135, 152, 47, 187, 211]

with an expected base36:

jrwjerxiekdtj9k82lg930wpkr6tq6r

and hex:

a94a8fe5ccb19ba61c4c0873d391e987982fbbd3

Solution

  • I had already written this for base 10, so here is a general version for any base from 2...36:

    func bytesToRadix<C: RangeReplaceableCollection>(_ bytes: C, radix: Int, isUppercase: Bool = false, isBigEndian: Bool = true) -> String where C.Element == UInt8 {
    
        // Nothing to process or radix outside of 2...36, return an empty string.
        guard !bytes.isEmpty, 2...36 ~= radix else { return "" }
    
        let bytes = isBigEndian ? bytes : C(bytes.reversed())
    
        // For efficiency in calculation, combine 7 bytes into one Int.
        let chunk = 7
        let numvalues = bytes.count
        var ints = Array(repeating: 0, count: (numvalues + chunk - 1)/chunk)
        var rem = numvalues % chunk == 0 ? chunk : numvalues % chunk
        var index = 0
        var accum = 0
    
        for value in bytes {
            accum = (accum << 8) + Int(value)
            rem -= 1
            if rem == 0 {
                rem = chunk
                ints[index] = accum
                index += 1
                accum = 0
            }
        }
    
        // Array to hold the result, in reverse order
        var digits = [Int]()
    
        // Repeatedly divide value by radix, accumulating the remainders.
        // Repeat until original number is zero
        while !ints.isEmpty {
            var carry = 0
            for (index, value) in ints.enumerated() {
                var total = (carry << (8 * chunk)) + value
                carry = total % radix
                total /= radix
                ints[index] = total
            }
    
            digits.append(carry)
    
            // Remove leading Ints that have become zero.
            ints = .init(ints.drop { $0 == 0 })
        }
    
        // Create mapping of digit Int to String
        let letterOffset = Int(UnicodeScalar(isUppercase ? "A" : "a").value - 10)
        let letters = (0 ..< radix).map { d in d < 10 ? "\(d)" : String(UnicodeScalar(letterOffset + d)!) }
    
        // Reverse the digits array, convert them to String, and join them
        return digits.reversed().map { letters[$0] }.joined()
    }
    

    Examples:

    let face: [UInt8] = [0xFA, 0xCE]
    
    print(bytesToRadix(face, radix: 16))  // "face"
    print(bytesToRadix(face, radix: 16, isUppercase: true))  // "FACE"
    print(bytesToRadix(face, radix: 16, isBigEndian: false))  // "cefa""
    print(bytesToRadix(face, radix: 16, isUppercase: true, isBigEndian: false))  // "CEFA"
    print(bytesToRadix(face, radix: 10)) // "64206"
    print(bytesToRadix(face, radix: 2))  // "111101011001110"
    print(bytesToRadix(face, radix: 36)) // "1dji"
    
    // also works with Data
    let faceData = Data([0xFA, 0xCE])
    print(bytesToRadix(face, radix: 16))  // "face"
    

    Some edge cases:

    print(bytesToRadix([9], radix: 16))   // "9"
    print(bytesToRadix([10], radix: 16))  // "a"
    print(bytesToRadix([15], radix: 16))  // "f"
    print(bytesToRadix([16], radix: 16))  // "10"
    print(bytesToRadix([35], radix: 36))  // "z"
    print(bytesToRadix([36], radix: 36))  // "10"
    

    Big test:

    let bArray = (0...255).map(UInt8.init)
    print(bytesToRadix(bArray, radix: 16, isBigEndian: false))
    

    fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0efeeedecebeae9e8e7e6e5e4e3e2e1e0dfdedddcdbdad9d8d7d6d5d4d3d2d1d0cfcecdcccbcac9c8c7c6c5c4c3c2c1c0bfbebdbcbbbab9b8b7b6b5b4b3b2b1b0afaeadacabaaa9a8a7a6a5a4a3a2a1a09f9e9d9c9b9a999897969594939291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100

    print(bytesToRadix(bArray, radix: 36))
    

    168swoi6iuzj4fbwknlnh695zl88v65qcfgnwrwepqcxb9dysmluowqahvt3r9gsc1v47ssxdivjda3nttl6r044pzz7zwhtgu2mkow5ts28x2mbwenh3wfz4s1sarspfhlrakvqrgpmzb66sgtz2lzbotl7r28wcq8925c747b44l60vrk3scrin4zvnwn7pdsukgo6lgjhu1nuwj7yt1h9ujpe3os17onsk7sp4ysmytu568do2tqetwnrmbxb2dtd8kqorcoakaizlm9svr8axe1acxfursz11nubrhighfd64yhmp99ucvzr944n8co01o4x64cmbd8be0hqbm2zy5uwe4uplc4sa50xajel4bkkxb1kh21pisna37eqwpbpq11ypr


    Test with your sample data:

    let bArray: [UInt8] = [169, 74, 143, 229, 204, 177, 155, 166, 28, 76, 8, 115, 211, 145, 233, 135, 152, 47, 187, 211]
    
    print(bytesToRadix(bArray, radix: 16))
    

    a94a8fe5ccb19ba61c4c0873d391e987982fbbd3

    print(bytesToRadix(bArray, radix: 36))
    

    jrwjerxiekdtj9k82lg930wpkr6tq6r


    Reverse function: radixToBytes

    Here is a quick version of the reverse function. It doesn't yet have the ability to use uppercase digits or handle endian (big endian is assumed).

    func radixToBytes(_ radixString: String, radix: Int) -> [UInt8] {
    
        let digitMap: [Character : Int] = [
            "0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5,
            "6": 6, "7": 7, "8": 8, "9": 9, "a": 10, "b": 11,
            "c": 12, "d": 13, "e": 14, "f": 15, "g": 16, "h": 17,
            "i": 18, "j": 19, "k": 20, "l": 21, "m": 22, "n": 23,
            "o": 24, "p": 25, "q": 26, "r": 27, "s": 28, "t": 29,
            "u": 30, "v": 31, "w": 32, "x": 33, "y": 34, "z": 35
        ]
    
        // Convert input string into array of Int digits
        let digits = Array(radixString).compactMap { digitMap[$0] }
    
        // Nothing to process? Return an empty array.
        guard digits.count > 0 else { return [] }
    
        let numdigits = digits.count
    
        // Array to hold the result, in reverse order
        var bytes = [UInt8]()
    
        // Convert array of digits into array of Int values each
        // representing 6 digits of the original number.  Six digits
        // was chosen to work on 32-bit and 64-bit systems.
        // Compute length of first number.  It will be less than 6 if
        // there isn't a multiple of 6 digits in the number.
        let chunk = 6
        var ints = Array(repeating: 0, count: (numdigits + chunk - 1)/chunk)
        var rem = numdigits % chunk
        if rem == 0 {
            rem = chunk
        }
        var index = 0
        var accum = 0
        for digit in digits {
            accum = accum * radix + digit
            rem -= 1
            if rem == 0 {
                rem = chunk
                ints[index] = accum
                index += 1
                accum = 0
            }
        }
    
        // Repeatedly divide value by 256, accumulating the remainders.
        // Repeat until original number is zero
        var mult = 1
        for _ in 1...chunk {
            mult *= radix
        }
    
        while ints.count > 0 {
            var carry = 0
            for (index, value) in ints.enumerated() {
                var total = carry * mult + value
                carry = total % 256
                total /= 256
                ints[index] = total
            }
    
            bytes.append(UInt8(truncatingIfNeeded: carry))
    
            // Remove leading Ints that have become zero
            ints = .init(ints.drop { $0 == 0 })
        }
    
        // Reverse the array and return it
        return bytes.reversed()
    }