1

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
1
  • 1
    Can you show some sample inputs and outputs? Commented Jun 29, 2019 at 11:09

2 Answers 2

2

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()
}
Sign up to request clarification or add additional context in comments.

10 Comments

“Repeatedly divide value by 10” should now probably be “Repeatedly divide value by radix” :)
you could make your method generic to make it work with bytes or data: func bytesToRadix<C: RangeReplaceableCollection>(_ bytes: C, radix: Int, isBigEndian: Bool = true) -> String where C.Element == UInt8 { btw the guard can be simplified as guard !bytes.isEmpty, 2...36 ~= radix else { and I would use ternary operator here let bytes = isBigEndian ? bytes : C(bytes.reversed()) and var rem = numvalues % 3 == 0 ? 3 : numvalues % 3
and to remove leading Ints that have become zero. ints = .init(ints.drop { $0 == 0 })
@LeoDabus, thanks for your suggestions. I incorporated them and added isUppercase capability.
so 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)
|
2

Looks like you are trying to convert from data (bytes) to integer and then to base 36:

let bArray: [UInt8] = [255, 255, 255, 255, 255, 255, 255, 127] // Int.max 9223372036854775807
let foo = bArray.withUnsafeBytes { $0.load(as: Int.self) }
let baseString = String(foo, radix: 36, uppercase: true)   // "1Y2P0IJ32E8E7"

String(Int.max, radix: 36, uppercase: true) // 1Y2P0IJ32E8E7

3 Comments

And if your [UInt8] has more than 8 elements?
My array has more than 8 elements. I've tried slicing it up into 8 bytes, but I also need to add padding to make sure count % 8 == 0. Any attempt at padding gives me the wrong result....
added to question above as a comment

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.