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);
200_success
145k22 gold badges190 silver badges478 bronze badges
asked Oct 3, 2018 at 17:13
\$\endgroup\$

1 Answer 1

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.

answered Oct 3, 2018 at 22:03
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.