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)
CodePudding user response:
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.
- 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.
- 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. - Don't capitalize your sentences. That limits your randomness, and prevents you from having separate values for capital letters.
- 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.
- 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.
CodePudding user response:
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 ;-)