简体   繁体   中英

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)

To iterate through all the characters of your phrase you can use String#each_char . Then, while iterating, you can shift the letters (looping through the alphabet) and leave alone the other characters.

For coding the shift with looping, you'll need to take a look at the ASCII table; each character has a numeric value, and there are noticeable ranges:

from 0(48) to 9(57)
from A(65) to Z(90)
from a(97) to z(122)

Lets say that you want to loop through the lowercase letters; then adding 3 to 'y' should give you a 'b' . How do you code that? That would be (in C language):

('y' - 'a' + 3) % ('z' - 'a') + 'a' 

Note: I'll stop with the explanations because there're a few things going on in the formula and I'm not sure about how to express them.

So here is an incomplete code for you:

def caesar_cypher ( string, shift = 0)
  result = ""
  string.each_char do |char|
    case char
    when 'a'..'z'
      result << ((char.ord + shift - 'a'.ord) % 26 + 'a'.ord).chr
    when 'A'..'Z'
      # MISSING PART
    when '0'..'9' # OOPS! ROMANS DIDN'T KNOW OF ZERO?
      # MISSING PART
    else
      result << char
    end
  end
  result
end

I hope this helps ;-)

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.

A_ORD = 'A'.ord
def caesar_cypher(str, offset)
  h = ('A'..'Z').each_with_object(Hash.new(&:last)) do |ch,h|
    h[ch] = (A_ORD + (ch.ord - A_ORD + offset) % 26).chr
    h[ch.downcase] = (h[ch].ord + 32).chr
  end
  str.gsub(/./, h)
end 

Try it.

caesar_cypher("Hello world!", 4)
  #=> "Lipps asvph!"
caesar_cypher("What a string!", 6)
  #=> "Cngz g yzxotm!"

In executing the first example the hash held by the variable h equals

{"A"=>"E", "a"=>"e", "B"=>"F", "b"=>"f", "C"=>"G", "c"=>"g", "D"=>"H",
 "d"=>"h", "E"=>"I", "e"=>"i", "F"=>"J", "f"=>"j", "G"=>"K", "g"=>"k",
 "H"=>"L", "h"=>"l", "I"=>"M", "i"=>"m", "J"=>"N", "j"=>"n", "K"=>"O",
 "k"=>"o", "L"=>"P", "l"=>"p", "M"=>"Q", "m"=>"q", "N"=>"R", "n"=>"r",
 "O"=>"S", "o"=>"s", "P"=>"T", "p"=>"t", "Q"=>"U", "q"=>"u", "R"=>"V",
 "r"=>"v", "S"=>"W", "s"=>"w", "T"=>"X", "t"=>"x", "U"=>"Y", "u"=>"y",
 "V"=>"Z", "v"=>"z", "W"=>"A", "w"=>"a", "X"=>"B", "x"=>"b", "Y"=>"C",
 "y"=>"c", "Z"=>"D", "z"=>"d"}

The snippet

Hash.new(&:last)

if the same as

Hash.new { |h,k| k }

where the block variable h is the (initially-empty) hash that is being created and k is a key. If a hash is defined

hash = Hash.new { |h,k| k }

then (possibly after adding key-value pairs) if hash does not have a key k , hash[k] returns k (that is, the character k is left unchanged).

See the form of Hash::new that takes a block but no argument.


We can easily create a decrypting method.

def caesar_decrypt(str, offset)
  caesar_cypher(str, 26-offset)
end
offset = 4
s = caesar_cypher("Hello world!", offset)
  #=> "Lipps asvph!"
caesar_decrypt(s, offset)
  #=> "Hello world!"
offset = 24
s = caesar_cypher("Hello world!", offset)
  #=> Fcjjm umpjb!
caesar_decrypt(s, offset)
  #=> "Hello world!"

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM