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
\$\endgroup\$
  • \$\begingroup\$ Please pick one version of the code to be reviewed. \$\endgroup\$ – 200_success Feb 26 '16 at 3:33
  • \$\begingroup\$ Done, sorry. Noted for the future. \$\endgroup\$ – JRTN Feb 26 '16 at 3:35
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)
\$\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.