I've stumbled upon this pretty old article about a hashing interview question, and here it is converted to Swift in a more generic way:
struct CustomHasher {
/// An enum of the errors that may be thrown
enum HashingError: Error {
/// Thrown by the initializer
/// when the alphabet contains repeating letters
case invalidAlphabet
/// Thrown by the initializer
/// when the base is negative
case invalidBase
/// Thrown by the initializer
/// when the offset is negative
case invalidOffset
/// Thrown by the hash(_:) function
/// when the string provided uses illegal letters
case outOfAlphabet(String)
/// Thrown by the hash(_:) function
/// when the string provided uses illegal letters
case exceedsInt64
/// Thrown by the reverseHash(_:) function
/// when the string provided uses illegal letters
case invalidHash
}
//Parameters
private let base: Int64
private let offset: Int64
private let alphabet: String
// An array that eases the access to the elements of the alphabet
private let alphabetArray: [Character]
private let stringLengthLimit: Int
/// Convinience inializer
/// - Parameters:
/// - alphabet: Must be a string of unique characters
/// - offset: A strictly positive Int64
/// - base: A strictly positive Int64
/// - Throws:
/// - HashingError.outOfAlphabet(String)
/// - HashingError.invalidOffset
init(alphabet: String, offset: Int64, base: Int64) throws {
self.alphabet = alphabet
self.alphabetArray = Array(alphabet)
guard alphabetArray.count == Set(alphabet).count else {
throw HashingError.invalidAlphabet
}
guard offset > 0 else {
throw HashingError.invalidOffset
}
self.offset = offset
guard base > 1 else {
throw HashingError.invalidBase
}
self.base = base
let b = Double(base)
let c = Double(alphabetArray.count)
let dOffset = Double(offset)
let int64limit = Double(Int64.max)
self.stringLengthLimit = ((0...).first {
let power = pow(b, Double($0))
let tail = $0 == 1 ? c * power : c * (power - 1) / (b - 1)
let head = dOffset * power
return head + tail > int64limit
} ?? 0) - 1
}
/// Takes a string and converts it to a corresponding Int64
/// - Parameters:
/// - str: The string to be hashed
/// - Throws:
/// - HashingError.outOfAlphabet(String)
/// - HashingError.exceedsInt64
func hash(_ str: String) throws -> Int64 {
guard Array(str).count <= stringLengthLimit else {
throw HashingError.exceedsInt64
}
var h: Int64 = offset
for char in str {
if let index: Int = alphabetArray.firstIndex(of: char) {
h = h * base + Int64(index)
} else {
throw HashingError.outOfAlphabet(alphabet)
}
}
return h
}
/// Reverses the hashing process
/// - Parameters:
/// - str: The string to be hashed
/// - Throws:
/// - HashingError.invalidHash
func reverseHash(_ hash: Int64) throws -> String {
guard hash >= offset else {
throw HashingError.invalidHash
}
//Reached the end
if hash == offset {
return ""
}
let remainder: Int64 = hash % base
let quotient: Int64 = (hash - remainder)/base
let index: Int = Int(truncatingIfNeeded: remainder)
guard index < alphabetArray.count else {
throw HashingError.invalidHash
}
let char: Character = alphabetArray[index]
return try reverseHash(quotient) + String(char)
}
}
And here it is in use:
let base37 = try! CustomHasher(alphabet: "acdegilmnoprstuw",
offset: 7,
base: 37)
do {
try base37.hash("leepadg")
} catch {
print(error)
}
do {
try base37.reverseHash(680131659347)
} catch {
print(error)
}
Feedback on all aspects of the code is welcome, such as (but not limited to):
- Should such a hasher throw? Or would it be more natural/idiomatic to return nil if it fails?
- Possible improvements (speed, clarity, especially the latter),
- Naming,
- Better comments.