rubyord

Is it possible to keep the non letter symbols intact?


I am a Ruby beginner and i am working on a cypher program. It takes a phrase , transforms the string to numbers , increments the numbers with a given value then transforms them again in to a string. I would like to know how can i can keep the non letter symbols unchanged. like the " ! " or the space. The code i have wrote is bellow:

def caesar_cypher ( phrase, number=0)
    letters = phrase.downcase.split(//)
    
    letters_to_numbers= letters.map { |idx|  idx.ord }
    
    incrementor = letters_to_numbers.map { |idx| idx+number}
    
    numbers_to_letters = incrementor.map { |idx| idx.chr}.join.capitalize
    p numbers_to_letters
    #binding.pry
end

caesar_cypher("Hello world!", 4)
caesar_cypher("What a string!", 6)

Solution

  • Solution Using Array#rotate and Hash#fetch

    Yes, you can pass characters through unmodified, but to do so you'll need to define what's a "letter" and what you want to include or exclude from the encoding within your #map block. Here's a slightly different way to approach the problem that does those things, but is also much shorter and adds some additional flexibility.

    1. Create an Array of all uppercase and lowercase characters in the English alphabet, and assign each to a replacement value using the inverted hashed value of Array#rotate, where the rotation value is your reproducible cypher key.
    2. Warn when you won't have an encrypted value because the rotation is key % 26 == 0, but allow it anyway. This helps with testing. Otherwise, you could simply raise an exception if you don't want to allow plaintext results, or set a default value for key.
    3. Don't capitalize your sentences. That limits your randomness, and prevents you from having separate values for capital letters.
    4. Using a default value with Hash#fetch allows you to return any character that isn't in your Hash without encoding it, so UTF-8 or punctuation will simply be passed through as-is.
    5. Spaces are not part of the defined encoding in the Hash, so you can use String#join without having to treat them specially.

    Using Ruby 3.0.2:

    def caesar_cypher phrase, key
      warn "no encoding when key=#{key}" if (key % 26).zero?
    
      letters = [*(?A..?Z), *(?a..?z)]
      encoding = letters.rotate(key).zip(letters).to_h.invert
      phrase.chars.map { encoding.fetch _1, _1 }.join
    end
    

    You can verify that this gives you repeatable outputs with some of the following examples:

    # verify your mapping with key=0,
    # which returns the phrase as-is
    caesar_cypher "foo bar", 0
    #=> "foo bar"
    
    caesar_cypher "foo bar", 5
    #=> "ktt gfw"
    
    caesar_cypher "Et tu, Brute?", 43
    #=> "vk kl, silkV?"
    
    # use any other rotation value you like;
    # you aren't limited to just 0..51
    caesar_cypher "Veni, vidi, vici", 152
    #=> "Raje, reZe, reYe"
    
    # UTF-8 and punctuation (actually, anything
    # not /[A-Za-z]/) will simply pass through
    # unencoded since it's not defined in the
    # +encoding+ Hash
    caesar_cypher "î.ô.ú.", 10
    #=> "î.ô.ú."
    

    Syntax Note for Numbered Arguments

    The code above should work on most recent Ruby versions, but on versions older than 2.7 you may need to replace the _1 variables inside the block with something like:

      phrase.chars.map { |char| encoding.fetch(char, char) }.join
    

    instead of relying on numbered positional arguments. I can't think of anything else that would prevent this code from running on any Ruby version that's not past end-of-life, but if you find something specific please add a comment.