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
vacawama
  • 150,663
  • 30
  • 266
  • 294
Nick Jones
  • 523
  • 1
  • 3
  • 17

2 Answers2

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()
}
vacawama
  • 150,663
  • 30
  • 266
  • 294
  • “Repeatedly divide value by 10” should now probably be “Repeatedly divide value by radix” :) – Martin R Jun 29 '19 at 14:54
  • 1
    you could make your method generic to make it work with bytes or data: `func bytesToRadix(_ 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` – Leo Dabus Jun 29 '19 at 16:30
  • minor change also `accum *= 256 + Int(value)` – Leo Dabus Jun 29 '19 at 16:32
  • 1
    and to remove leading Ints that have become zero. `ints = .init(ints.drop { $0 == 0 })` – Leo Dabus Jun 29 '19 at 16:43
  • @MartinR, lol I fixed that with my first edit but I did it on SO instead of on the computer, so I brought it back when I updated the code. – vacawama Jun 29 '19 at 19:21
  • 1
    @LeoDabus, thanks for your suggestions. I incorporated them and added `isUppercase` capability. – vacawama Jun 30 '19 at 12:15
  • 1
    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`) – Nick Jones Jul 02 '19 at 19:48
  • @vacawama Do you know where can I find a base36 string decoding algorithm? – Leo Dabus Jul 03 '19 at 21:58
  • @LeoDabus, base36 to [UInt8] or something else? – vacawama Jul 03 '19 at 22:01
  • @LeoDabus, I added a quick and dirty version of the reverse function. Obviously functionality and fine tuning to be done still. – vacawama Jul 03 '19 at 22:34
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
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571