For a small project, I had to implement Caesar encryption in Swift(4). I'm mainly looking for performance optimizations.
This whole code should be copy and pasteable in a playground and is functioning as expected as far as I can see.
import Foundation
let caesarNumber:Int = 7;
let alphabet_array:[Character] = ["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z"]
func encode(inputString: String) -> String {
var resultString:String = ""
for char in inputString {
for i in 0...(alphabet_array.count-1) {
if (char == alphabet_array[i]) {
if (i + caesarNumber < alphabet_array.count-1) {
resultString.append(alphabet_array[i+caesarNumber])
}
else {
let subscripture:Int = (i+caesarNumber)%alphabet_array.count
resultString.append(alphabet_array[subscripture])
}
}
}
}
return resultString
}
func decode(inputString: String) -> String {
var resultString:String = ""
for char in inputString {
for i in 0...(alphabet_array.count-1) {
if (char == alphabet_array[i]) {
if (i - caesarNumber >= 0) {
resultString.append(alphabet_array[i-caesarNumber])
}
else {
resultString.append(
alphabet_array[alphabet_array.count+(i-caesarNumber)])
}
}
}
}
return resultString
}
let encodedString = encode(inputString: "zacharias")
let decodeString = decode(inputString: encodedString);
1 Answer 1
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.