3
\$\begingroup\$

I really like Ruby and want to get better at it. Any input is appreciated.

Description

This program prompts the user for a string which is ciphertext resulting from a Caesar cipher given (English) plaintext and a key on the range [0, 25]. The program then uses a table of character frequencies of the English language to perform simple cryptanalysis to determine the top 5 most probable shift keys, and displays the resulting plaintext when these keys are used to decrypt the ciphertext.

It should also be noted that the only input expected is letters and whitespace, but I think it works with most punctuation.

Code:

class Cryptanalysis
 def initialize
 @alphabet = ('A'..'Z').to_a.join
 @english_frequency = { :A => 0.080, :B => 0.015, :C => 0.030,
 :D => 0.040, :E => 0.130, :F => 0.020,
 :G => 0.015, :H => 0.060, :I => 0.065,
 :J => 0.005, :K => 0.005, :L => 0.035,
 :M => 0.030, :N => 0.070, :O => 0.080,
 :P => 0.020, :Q => 0.002, :R => 0.065,
 :S => 0.060, :T => 0.090, :U => 0.030,
 :V => 0.010, :W => 0.015, :X => 0.005,
 :Y => 0.020, :Z => 0.002 }
 end
 def decrypt_caesar(ciphertext, shift)
 i = shift % @alphabet.size
 decrypt = @alphabet
 encrypt = @alphabet[i..-1] + @alphabet[0...i]
 ciphertext.tr(encrypt, decrypt)
 end
 def analyze(ciphertext)
 chars = ciphertext.gsub(/[^A-Z]/i, '').split(//)
 ciphertext_frequency = character_frequency(chars)
 correlation_of_frequency = Hash.new
 alphabet = @alphabet.split(//)
 (0..25).each { |i|
 sum = 0.0
 alphabet.each { |c|
 e = alphabet.index(c)
 sum += ciphertext_frequency[c.to_sym] * @english_frequency[alphabet[(26 + e - i) % 26].to_sym]
 }
 correlation_of_frequency[i] = sum
 }
 correlation_of_frequency.sort_by { |_, v| -v }.take(5).to_h
 end
 def character_frequency(chars)
 frequency = Hash.new
 inc = 1.0 / chars.size
 @alphabet.each_char { |c| frequency[c.to_sym] = 0.0 }
 chars.each { |c| frequency[c.to_sym] += inc }
 frequency
 end
end
crypt = Cryptanalysis.new
puts 'Enter a string to analyze'
text = gets.chomp.upcase
result = crypt.analyze(text)
puts 'Top 5 possible shifts: '
choice = 1
result.each do |k, v|
 puts '%10s) Shift amount: %2s Correlation: %%%.4f Result: %s' % [choice, k, v, crypt.decrypt_caesar(text, k)]
 choice += 1
end
asked Feb 26, 2016 at 3:17
\$\endgroup\$
2
  • \$\begingroup\$ Please pick one version of the code to be reviewed. \$\endgroup\$ Commented Feb 26, 2016 at 3:33
  • \$\begingroup\$ Done, sorry. Noted for the future. \$\endgroup\$ Commented Feb 26, 2016 at 3:35

1 Answer 1

2
\$\begingroup\$

Nice work. You can get some easy improvements by:

  • Taking advantage of ruby's functional methods to eliminate temp variables and generally make the code more declarative
  • Use constants where appropriate
  • Make your code reflect your style by using static method on a module rather than a class -- you're not actually using the class for anything in the original code. Alternatively, you can pass in a save the ciphertext in the class constructor, and then do something like crypt_instance.analyze.
  • Avoid "magic numbers" like 25 or 26.

Rewrite:

module Cryptanalysis
 ALPHABET = ('A'..'Z').to_a
 ENGLISH_FREQUENCY = { :A => 0.080, :B => 0.015, :C => 0.030,
 :D => 0.040, :E => 0.130, :F => 0.020,
 :G => 0.015, :H => 0.060, :I => 0.065,
 :J => 0.005, :K => 0.005, :L => 0.035,
 :M => 0.030, :N => 0.070, :O => 0.080,
 :P => 0.020, :Q => 0.002, :R => 0.065,
 :S => 0.060, :T => 0.090, :U => 0.030,
 :V => 0.010, :W => 0.015, :X => 0.005,
 :Y => 0.020, :Z => 0.002 }
 def self.decrypt_caesar(ciphertext, shift)
 decrypt = ALPHABET
 encrypt = ALPHABET.rotate(shift)
 ciphertext.tr(encrypt, decrypt)
 end
 def self.analyze(ciphertext)
 freqs = frequencies(clean(ciphertext))
 (0..ALPHABET.size)
 .map{|i| [i, score(freqs, i)]}
 .sort_by { |x| -x[1] }
 .take(5)
 .to_h
 end
 def self.clean(text)
 text.upcase.gsub(/[^A-Z]/i, '')
 end
 def self.score(freqs, shift)
 ENGLISH_FREQUENCY
 .values
 .rotate(shift)
 .zip(freqs.values)
 .map{|x| x.first * x.last}
 .reduce(:+)
 end
 def self.frequencies(text)
 ENGLISH_FREQUENCY
 .keys
 .map {|k| [k.to_sym, text.count(k.to_s) / text.size.to_f]}
 .to_h
 end
end
text = "the quick brown fox jumps over the lazy dog"
puts 'Top 5 possible shifts: ' 
puts Cryptanalysis.analyze(text)
answered Feb 27, 2016 at 6:34
\$\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.