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 Character
s. 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.