Search code examples
rubystringencryptioncaesar-cipher

Simple Cipher - Ruby from Exercism


I am currently studying Ruby and have attempted Simple Cipher challenge. I am now studying the following solution and am trying to reverse engineer to understand the thought process behind of this solution. The following is the link for the solution. I will detail my understanding of each code snippet. If they are not right, could you please correct me? Thanks! https://exercism.io/tracks/ruby/exercises/simple-cipher/solutions/b200c3d9f10e497bbe2ca0d826df2661

class Cipher
  ALPHABET = [*'a'..'z']

  attr_reader :key

  def initialize(key = nil)
    @key = key || 100.times.map { ALPHABET.sample }.join
    fail ArgumentError, 'invalid chars in key' unless valid?(@key)
  end

  def encode(text)
    a = 'a'.ord
    text.chars.zip(@key.chars).map do |char, key|
      ALPHABET[(char.ord - a + key.ord - a) % ALPHABET.length]
    end.join
  end

  def decode(code)
    code.chars.zip(@key.chars).map do |char, key|
      ALPHABET[char.ord - key.ord]
    end.join
  end

private

  def valid?(key)
    !key.empty? && key !~ /[^a-z]/
  end

end

Part 1

@key = key is if key is matched. ALPHABET is randomized (with .sample method) and joined. Randomised joined alphabets are then iterated 100 times and a new array is returned (with map method).

To handle invalid key error, fail ArgumentError is used.

  def initialize(key = nil)
    @key = key || 100.times.map { ALPHABET.sample }.join
    fail ArgumentError, 'invalid chars in key' unless valid?(@key)
  end

Part 2 I understand that the purpose of this code is to convert the plain text into encrypted text. However, I am struggling to some part of the following code.

def encode(text)
    a = 'a'.ord
    text.chars.zip(@key.chars).map do |char, key|
      ALPHABET[(char.ord - a + key.ord - a) % ALPHABET.length]
    end.join
  end
  1. .ord method - convert a character to its ASCII value. a = 'a'.ord . Why is this character selected? Not z or other characters?
  2. .chars method - text string is separated into each individual character with .chars method. .chars more efficient than .split since it parses the underlying bytes to return the string's characters (I have read .chars vs .split difference on the stackover flow).
  3. .zip method - this method is used to compare original plain text characters and encryption characters.
  4. .map method - This method is called with a block and returns encrpyted text.
  5. Inside .map method block, there are two arguments: char and key. char represents character from the original plain text and key represents encrypted key character.

  6. I have a trouble understanding this ALPHABET[(char.ord - a + key.ord - a) % ALPHABET.length] part. ALPHABET original plain text and encryption key text characters are converted into ASCII values with .ord method. Why a value is subtracted from those values? Why use % operator and ALPHABET.length?

  7. .join method - I guess .join is used to join the transformed encrypted characters. Is my understanding correct?

Part 3 In this part, decode method, code is used. Code is the secret key shared between two parties (sender and receiver). But why ALPHABET[char.ord - key.ord]? Characters ascii - key ascii value will provide the decrypted plain text. I do not understand how it works?

def decode(code)
    code.chars.zip(@key.chars).map do |char, key|
      ALPHABET[char.ord - key.ord]
    end.join
end

Part 4 private method is used to separate this chunk of code from other classes. Valid method is to verify the key. There are two conditions: key must not be empty or must be lowercase alphabets.

private

  def valid?(key)
    !key.empty? && key !~ /[^a-z]/
  end

Solution

  • @key = key is if key is matched. ALPHABET is randomized (with .sample method) and joined. Randomised joined alphabets are then iterated 100 times and a new array is returned (with map method).

    @key = key if key is not falsy (nil or false); otherwise, iterate 100 times and collect a random character from ALPHABET; then join the resulting 100-element array into a string and assign the string to @key. Nothing is returned.

    ord method - convert a character to its ASCII value. a = 'a'.ord . Why is this character selected? Not z or other characters?

    Because "a" starts the alphabetic sequence in Unicode (In ASCII too, but .ord in this case operates on Unicode), with value 97. But we're interested in the position of the character in the alphabet, not in Unicode. Think of it this way: It's 20:37, and you're listening to opera. The performance started at 19:00. How long have you been listening to it? You subtract 20:37 - 19:00, to get to the answer (1 hour, 37 minutes). Same way, to know that 'c' is the #2 character (in English we'd say 3rd, but Ruby starts counting from 0, so 'a' is #0, 'c' is #2), you subtract the position of 'a' from the position of 'c': 99 - 97. You would not subtract 23:00 or any other time from 20:37, because that doesn't make any sense in finding out how long you've been listening; same reason why we use 'a'.ord, and not 'z' or another character.

    .zip method - this method is used to compare original plain text characters and encryption characters.

    No, it just makes a new array, by pairing up each element from text.chars with a corresponding one from @key.chars. There is no comparison going on.

    I have a trouble understanding this ALPHABET[(char.ord - a + key.ord - a) % ALPHABET.length] part. ALPHABET original plain text and encryption key text characters are converted into ASCII values with .ord method. Why a value is subtracted from those values? Why use % operator and ALPHABET.length?

    See above regarding why a.

    This cipher works by offsetting each character by the number of spaces corresponding to the alphabet position of the appropriate character in the key. % will wrap things around so that the result is always a valid index of ALPHABET. For example, if char.ord - a + key.ord - a happen to go over the length of ALPHABET, then it would wrap around to the start. For example, if you get 29 in the above calculation, you'd normally get nil for ALPHABET[29] since there's no letter #29; but 29 % 26 is 3, and ALPHABET[3] is valid.

    Part 3 In this part, decode method, code is used. Code is the secret key shared between two parties (sender and receiver). But why ALPHABET[char.ord - key.ord]? Characters ascii - key ascii value will provide the decrypted plain text. I do not understand how it works?

    What is shifted can be unshifted; this is what decoding looks like. Where before we were adding up the positions of the two characters — (char.ord - a) + (key.ord - a) — now we're subtracting them: (char.ord - a) - (key.ord - a). Do a bit of math, and you'll see that the two a will cancel each other in this subtraction, so (char.ord - a) - (key.ord - a) is equivalent to char.ord - key.ord.

    Before, we had a concern the addition might go over the size of the alphabet. Here one might be concerned that the subtraction might go negative. But Ruby has our back now: ALPHABET[-3] in Ruby means "3rd element from the end", and is thus equivalent to ALPHABET[23]; no need for %.