Skip to content

Instantly share code, notes, and snippets.

@janodev
Last active April 2, 2023 15:48
Show Gist options
  • Save janodev/9fb63a408251e66d7f1756f2537bc4b6 to your computer and use it in GitHub Desktop.
Save janodev/9fb63a408251e66d7f1756f2537bc4b6 to your computer and use it in GitHub Desktop.
import Foundation
enum BaseConverter
{
static let asciiScalars = (0...127).compactMap { UnicodeScalar($0) }
static let alphanumericsAndSymbolsSet = CharacterSet.alphanumerics.union(CharacterSet.symbols)
static let base64AlphabetSet = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "+/="))
static let asciiAlphabet = scalarsToString(asciiScalars)
static let asciiAlphanumericsAlphabet = scalarsToString(asciiScalars.filter { CharacterSet.alphanumerics.contains($0) })
static let asciiAlphanumericsAndSymbolsAlphabet = scalarsToString(asciiScalars.filter { alphanumericsAndSymbolsSet.contains($0) })
static let base64Alphabet = scalarsToString(asciiScalars.filter { base64AlphabetSet.contains($0) })
enum BaseConverterError: Error {
case positionOfEmptyString(String)
case outOfRange(String)
case alphabetTooSmall(String)
}
static func scalarsToString(_ scalars: [UnicodeScalar]) -> String {
scalars.map { String(Character($0)) }.joined()
}
static func singleCharacter(of string: String, at offset: Int) throws -> Character {
guard !string.isEmpty else {
throw BaseConverterError.positionOfEmptyString("Requested character at offset \(offset) for an empty string")
}
guard let index = string.index(string.startIndex, offsetBy: offset, limitedBy: string.endIndex) else {
throw BaseConverterError.outOfRange("Index out of range for offset \(offset) and string \(string)")
}
return string[index]
}
/// Represent number `n` using the given `alphabet`.
/// The base of the representation is inferred from the alphabet length. e.g. alphabet "AB" uses base 2.
static func representNumber(number: Int, alphabet: String) throws -> String
{
guard alphabet.count > 1 else {
throw BaseConverterError.alphabetTooSmall("Alphabet '\(alphabet)' is too small. Try at least two characters.")
}
let base = alphabet.count
if (number < base){
// it can be represented as a single character so return that character
return try String(singleCharacter(of: alphabet, at: number))
} else {
let rightmostDigit = number % base // e.g. 769 % 10 = 9
let rightmostChar = try singleCharacter(of: alphabet, at: rightmostDigit)
let remainder = Int(trunc(Double(number)/Double(base)))
let prefix = try representNumber(number: remainder, alphabet: alphabet)
return "\(String(prefix))\(rightmostChar)"
}
}
static func representNumber(number: Int, inBase base: Int) throws -> String
{
guard base <= asciiAlphanumericsAlphabet.count else {
throw BaseConverterError.alphabetTooSmall("Requested base \(base) but my alphabet allows at most base \(asciiAlphanumericsAlphabet.count)")
}
let alphabet = String(asciiAlphanumericsAlphabet.prefix(base))
return try representNumber(number: number, alphabet: alphabet)
}
}
// 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
print("asciiAlphanumericsAlphabet: \(BaseConverter.asciiAlphanumericsAlphabet)")
// $+0123456789<=>ABCDEFGHIJKLMNOPQRSTUVWXYZ^`abcdefghijklmnopqrstuvwxyz|~
print("asciiAlphanumericsAndSymbolsAlphabet: \(BaseConverter.asciiAlphanumericsAndSymbolsAlphabet)")
// +/0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
print("base64Alphabet: \(BaseConverter.base64Alphabet)")
assert(try! BaseConverter.representNumber(number: 4, inBase: 2) == "100")
assert(try! BaseConverter.representNumber(number: 4, alphabet: "01") == "100")
assert(try! BaseConverter.representNumber(number: 3735928559, inBase: 16) == "DEADBEEF")
assert(try! BaseConverter.singleCharacter(of: "01234", at: 4) == "4")
assert(try! BaseConverter.singleCharacter(of: "01234", at: 0) == "0")
// alphabetTooSmall("Alphabet \'0\' is too small. Try at least two characters.")
// try BaseConverter.representNumber(number: 4, inBase: 1)
// positionOfEmptyString("Requested character at offset 0 for an empty string")
// try BaseConverter.singleCharacter(of: "", at: 0)
// alphabetTooSmall("Requested base 4000 but my alphabet allows at most base 62")
// try BaseConverter.representNumber(number: 4, inBase: 4000)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment