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
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.
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?
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
@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 %
.