Naming
According to the Swift API Design Guidelines, variable names are lower camel case (not alphabet_array),
and types should not be part of the variable name (not caesarNumber, inputString, resultString).
It is also clear that the first (and only) argument of encode() and decode()
is the input string, here we can omit the argument label:
func encode(_ input: String) -> String
which is then called as
let encodedString = encode("zacharias")
Improving the API
The current API has two drawbacks:
- It uses the global variable
caesarNumber to pass information to the
functions.
- The function names (“encode”, “decode”) are too common, they to not tell
what the function actually does.
Here are two alternative suggestions:
- Make the function names more specific, and pass the shift amount as an
additional argument.
You can also define a default parameter value for the shift amount.
func caesarEncode(_ input: String, shiftBy: Int = 7) -> String
func caesarDecode(_ input: String, shiftBy: Int = 7) -> String
- Define a cipher type with parameters, and encode/decode methods.
That would allow to substitute the Caesar cipher by other methods easily.
struct CaesarCipher {
let shiftAmount: Int
init(shiftAmount: Int = 7) {
self.shiftAmount = shiftAmount
}
func encode(_ input: String) -> String { ... }
func decode(_ input: String) -> String { ... }
}
Example usage:
let rot13 = CaesarCipher(shiftAmount: 13)
let encrypted = rot13.encode("Hello World")
I'll stick with the first API for the remainder of this review.
Simplify the code
The alphabet can be initialized simply with
let alphabet: [Character] = Array("abcdefghijklmnopqrstuvwxyz")
because a String is a sequence of its Characters. Locating a character
in the array can be done with
if let idx = alphabet.index(of: char) { ... }
instead of a loop. The test for
if (i + caesarNumber < alphabet_array.count-1)
is not needed, because the “else case” (with the modulo arithmetic) works actually in both cases, with or without wrap-around.
Summarizing the above suggestions so far, we have for the encoding method:
func caesarEncode(_ input: String, shiftBy: Int = 7) -> String {
var result: String = ""
for char in input {
if let idx = alphabet.index(of: char) {
let newIdx = (idx + shiftBy) % alphabet.count
result.append(alphabet[newIdx])
}
}
return result
}
Decoding is the same as encoding, only with a shift in the opposite direction.
If we modify the encoding method slightly to work with negative shift amounts then it can be used for the decoding as well:
func caesarEncode(_ input: String, shiftBy: Int = 7) -> String {
var result: String = ""
for char in input {
if let idx = alphabet.index(of: char) {
var newIdx = (idx + shiftBy) % alphabet.count
if newIdx < 0 { newIdx += alphabet.count }
result.append(alphabet[newIdx])
}
}
return result
}
func caesarDecode(_ input: String, shiftBy: Int = 7) -> String {
return caesarEncode(input, shiftBy: -shiftBy)
}
Improve the performance
For optimal performance on really long strings you can operate on the
UTF-16 view instead, that allows to determine the offset within the alphabet with pure integer arithmetic instead of the array lookup.
Enumerating the UTF-16 view also seems to be faster than enumerating
the characters (which represent extended grapheme clusters).
A possible implementation could look like this:
func caesarEncode(_ input: String, shiftBy: Int = 7) -> String {
let letterA = Int("a".utf16.first!)
let letterZ = Int("z".utf16.first!)
let letterCount = letterZ - letterA + 1
var result = [UInt16]()
result.reserveCapacity(input.utf16.count)
for char in input.utf16 {
let value = Int(char)
switch value {
case letterA...letterZ:
let offset = value - letterA
var newOffset = (offset + shiftBy) % letterCount
if newOffset < 0 { newOffset += letterCount }
result.append(UInt16(letterA + newOffset))
default:
break
}
}
return String(utf16CodeUnits: &result, count: result.count)
}
On my computer this was faster than the previous method (3 milliseconds
instead of 11 milliseconds for a string with 100,000 characters).
For shorter strings that probably won't matter, and you can choose what you
feel more familiar with.