1
\$\begingroup\$

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);
\$\endgroup\$
4
\$\begingroup\$

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:

  1. 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 
  1. 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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.