Related
My Rubocop offense is telling me I need to 'Use the return of the conditional for variable assignment and comparison'
While I tried fixing it, it gave me another offense that my 'method line is too long'.
I've tried refactoring to another method but my code broke.
How do I shorten or refactor this code ?
HSH = { 'a' => 'z', 'b' => 'y', 'c' => 'x', 'd' => 'w', 'e' => 'v', \
'f' => 'u', 'g' => 't', 'h' => 's', \
'i' => 'r', 'j' => 'q', 'k' => 'p', 'l' => 'o', 'm' => 'n' }.freeze
def encoder(str)
encoded_string = ''
str.chars.each do |char|
encoded_string = if HSH.key?(char) then encoded_string += HSH[char]
elsif HSH.invert.key?(char) then encoded_string += HSH.invert[char]
else encoded_string += char
end
end
encoded_string
end
When I ran my test suite, everything was ok.
But the rubocop offense gave me method line is too long.
No hash:
ALPHABET = ("a".."z").to_a.join
def encoder(str)
str.tr(ALPHABET, ALPHABET.reverse)
end
HSH = {
'a' => 'z', 'b' => 'y', 'c' => 'x',
'd' => 'w', 'e' => 'v', 'f' => 'u',
'g' => 't', 'h' => 's', 'i' => 'r',
'j' => 'q', 'k' => 'p', 'l' => 'o',
'm' => 'n'
}.freeze
def encoder(str)
str.chars.map { |char| HSH[char] || HSH.invert[char] || char }.join
end
NB! Do not use this answer! the proper way to go is provided by #steenslag here.
Use the hash with all the letters explicitly mapped and the default proc:
HSH =
(?a..?z).zip((?a..?z).to_a.reverse).to_h.
tap { |h| h.default_proc = ->(_, k) { k }}.
freeze
def encoder(str)
str.chars.map(&HSH.method(:[])).join
end
Several of us suggested better ways to implement your encoder method. But all of us (myself included) didn't actually answer your question, or see a central problem in your code:
My Rubocop offense is telling me I need to 'Use the return of the conditional for variable assignment and comparison'
While I tried fixing it, it gave me another offense that my 'method line is too long'.
encoded_string = if HSH.key?(char) then encoded_string += HSH[char]
elsif HSH.invert.key?(char) then encoded_string += HSH.invert[char]
else encoded_string += char
end
You sort of followed Rubocop's advice...you assigned a conditional result to a value...but I think you missed the mark. I'm not even sure that's the conditional result it was referring to. I'm assuming you added the encoded_string = ... assignment.
That is a uselesss assignment, as you already appended the character to encoded_string within the if blocks. You don't have to assign it again.
Rewinding to what I'm guessing was your version 1 code, here's a more efficient way to follow Rubocop's advice. Don't make an assignment within each condition...only do one assignment, with the result of the conditionals:
encoded_string += if HSH.key?(char) then HSH[char]
elsif HSH.invert.key?(char) then HSH.invert[char]
else char
end
That ends up with less code, and matches your original coding style and approach. It might even make Rubocop happy. The next step to lovely code would be to eliminate the excessive key? tests:
encoded_string += if HSH[char] then HSH[char]
elsif HSH.invert[char] then HSH.invert[char]
else char
end
From there, it's a small step to eliminate the if/elsif blocks with ||'s. And while we're at it, we'll change += to << to avoid, "produce a gazillion of intermediate unnecessary String instances." (Thanks for the suggestion, #Aleksei Matiushkin )
encoded_string << HSH[char] || HSH.invert[char] || char
For your approach to this problem, that's about the minimal level of conciseness and readability that production code should strive for. Anyone can understand it without thinking real hard or hitting Stack Overflow.
As with #Steenslag's answer, there's no need to convert the string to an array, map each element of the array and join the result back into a string. The following is defined to be efficient, by avoiding the need for a linear search for each letter.
def encode_decode(str)
rng = 'a'..'z'
str.gsub(/./) { |c| rng.cover?(c) ? (219-c.ord).chr : c }
end
plain_text = "The launch code is 'Bal3De8Rd0asH'."
#=> "Tsv ozfmxs xlwv rh 'Bzo3Dv8Rw0zhH'."
coded_text = encode_decode(plain_text)
#=> "Tsv ozfmxs xlwv rh 'Bzo3Dv8Rw0zhH'."
encode_decode(coded_text)
#=> "The launch code is 'Bal3De8Rd0asH'."
I would go ahead and expand your hash to all 26 letters, so you can avoid the inverse lookup. That simplifies your code by removing one case, which will may appease Rubocop... But more importantly, you'll be using the hash index for greater efficiency and performance. Inverse hash lookups are expensive, as it must read (up to) every value.
Consider encoding "1+2”. It will do three quick index scans, then three full array scans, just to return the original string.
With a fully populated hash, it would only take three quick scans.
Here is your original code with minimal changes to meet your goal: (There are shorter ways to do this (hint: tr or map), but shorter is not as important as easy and comfortable to the programmer using the code.)
translation = {
'a' => 'z', 'b' => 'y', 'c' => 'x', 'd' => 'w', 'e' => 'v', 'f' => 'u', 'g' => 't',
'h' => 's', 'i' => 'r', 'j' => 'q', 'k' => 'p', 'l' => 'o', 'm' => 'n', 'n' => 'm',
'o' => 'l', 'p' => 'k', 'q' => 'j', 'r' => 'i', 's' => 'h', 't' => 'g', 'u' => 'f',
'v' => 'e', 'w' => 'd', 'x' => 'c', 'y' => 'b', 'z' => 'a'
}.freeze
def encoder(str)
encoded_string = ''
str.chars.each do |char|
encoded_string << translation[char] || char
end
encoded_string
end
You might even consider expanding the hash to upper and lower case letters, or even all 256 char values, depending on what problem you're solving. But let's agree to ignore Unicode chars!
Back to Rubocop... The easiest, sure-fire solution to any sort of "too long/too complex" warning is to pull code out to a new method. Write def charswap and use that as the body of your loop. That will make writing tests easier, to boot. But, by expanding your translation array to all 26 letters, the code gets so simple that refactoring isn't really needed.
I have a method that I want to use to replace characters in a string:
def complexity_level_two
replacements = {
'i' => 'eye', 'e' => 'eei',
'a' => 'aya', 'o' => 'oha'}
word = "Cocoa!55"
word_arr = word.split('')
results = []
word_arr.each { |char|
if replacements[char] != nil
results.push(char.to_s.gsub!(replacements[char]))
else
results.push(char)
end
}
end
My desired output for the string should be: Cohacohaa!55
However when I run this method it will not replace the characters and only outputs the string:
C
o
c
o
a
!
5
5
What am I doing wrong to where this method will not replace the correct characters inside of the string to match that in the hash and how can I fix this to get the desired output?
replacements = {
'i' => 'eye', 'e' => 'eei',
'a' => 'aya', 'o' => 'oha'}
word = "Cocoa!55"
word.gsub(Regexp.union(replacements.keys), replacements)
#⇒ "Cohacohaaya!55"
Regexp::union, String#gsub with hash.
replacements = { 'i' => 'eye', 'e' => 'eei', 'a' => 'aya', 'o' => 'oha' }.
tap { |h| h.default_proc = ->(h,k) { k } }
"Cocoa!55".gsub(/./, replacements)
#=> "Cohacohaaya!55"
See Hash#default_proc= and Object#tap.
gsub examines each character of the string. If replacements has that character as a key, the character is replaced with the value of that key in replacements; else (because of the default proc), the character is replaced with itself (that is, left unchanged).
Another way would be to use Hash#fetch:
replacements = { 'i' => 'eye', 'e' => 'eei', 'a' => 'aya', 'o' => 'oha' }
"Cocoa!55".gsub(/./) { |s| replacements.fetch(s) { |c| c } }
#=> "Cohacohaaya!55"
which, for Ruby v2.2+ (when Object#itself made its debut), can be written
"Cocoa!55".gsub(/./) { |s| replacements.fetch(s, &:itself) }
you can try to do this:
my_subs = { 'i' => 'eye', 'e' => 'eei','a' => 'aya', 'o' => 'oha' }
my_word = "Cocoa!55"
my_word.split('').map{|i| my_subs[i] || i}.join
=> "Cohacohaaya!55"
Constructing a method
Define your method with word and subs parameters:
def char_replacer word, subs
word.chars.map { |c| subs.key?(c) ? subs[c] : c }.join
end
Here we've used the ternary operator which is essentially an if-else expression in a more compact form. Key methods to note are String#chars, Array#map, Hash#key?, see ruby-docs for more info on these. Now with this all set up, you can call your method passing a word string and the subs hash of your choosing.
Example 1
my_subs = { 'i' => 'eye', 'e' => 'eei','a' => 'aya', 'o' => 'oha' }
my_word = "Cocoa!55"
char_replacer my_word, my_subs #=> "Cohacohaaya!55"
Example 2
my_subs = { 'a' => 'p', 'e' => 'c' }
my_word = "Cocoa!55"
char_replacer my_word, my_subs #=> "Cocop!55"
Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 8 years ago.
Improve this question
I'm building a basic encryptor that is outputting into an array and not a string. I'm guessing I need to use the .join method but for the life of me can't find out where, without getting an error.
class Encryptor
def cipher
{'a' => 'n', 'b' => 'o', 'c' => 'p', 'd' => 'q',
'e' => 'r', 'f' => 's', 'g' => 't', 'h' => 'u',
'i' => 'v', 'j' => 'w', 'k' => 'x', 'l' => 'y',
'm' => 'z', 'n' => 'a', 'o' => 'b', 'p' => 'c',
'q' => 'd', 'r' => 'e', 's' => 'f', 't' => 'g',
'u' => 'h', 'v' => 'i', 'w' => 'j', 'x' => 'k',
'y' => 'l', 'z' => 'm'}
end
def encrypt_letter(letter)
lowercase_letter = letter.downcase
end
def encrypt(string)
letters = string.split("")
letters.collect do |letter|
encrypted_letter = encrypt_letter(letter)
end
end
end
You could tighten up your encrypt_letter method by remembering that the last value evaluated in the method is also the return value.
def encrypt_letter(letter)
cipher[letter.downcase]
end
Encryptor.new.encrypt_letter('h') #=> "u"
Also, the collect method will actually return an array of all the values returned by the block (the last value evaluated by the block) so there's no need to assign it to a variable within the block. Since you have the array from collect (which is just all the encrypted letters, call join on that (and since that is the final evaluation in the method, it is the return value).
def encrypt(string)
letters = string.split("")
letters.collect {|letter| encrypt_letter(letter) }.join
end
Encryptor.new.encrypt("Hello") #=> "uryyb"
Technically, you could even just remove the letters variable and do it all in one line but I personally think it is a little more readable this way.
IMHO:
You could probably make all of the methods class methods since you aren't storing any instance variables and there doesn't seem to be any reason to keep it around outside of just encrypting a string.
class Encryptor
def cipher
{'a' => 'n', 'b' => 'o', 'c' => 'p', 'd' => 'q',
'e' => 'r', 'f' => 's', 'g' => 't', 'h' => 'u',
'i' => 'v', 'j' => 'w', 'k' => 'x', 'l' => 'y',
'm' => 'z', 'n' => 'a', 'o' => 'b', 'p' => 'c',
'q' => 'd', 'r' => 'e', 's' => 'f', 't' => 'g',
'u' => 'h', 'v' => 'i', 'w' => 'j', 'x' => 'k',
'y' => 'l', 'z' => 'm'}
end
def encrypt_letter(letter)
lowercase_letter = cipher[letter.downcase] #each letter passed is crypted here
end
def encrypt(string)
letters = string.split("")
encrypted_letter = [] #define an array to store each encrypted char
letters.collect do |letter|
encrypted_letter << encrypt_letter(letter) #accumulate encrypted chars in the array
end
encrypted_letter.join #now time to use join to form a string and return it
end
end
Encryptor.new.encrypt("something") #=> "fbzrguvat"
Hi I was wondering if someone could explain to me why the map function written in the below code is written in the way its written. Specifically why do we need to do
results = letters.map do |letter| encrypted_letter = encrypt_letter(letter)
instead of just doing
results = letters.map do |letter| encrypt_letter(letter)
class Encryptor
def cipher
{"a" => "n", "b" => "o", 'c' => 'p', 'd' => 'q',
'e' => 'r', 'f' => 's', 'g' => 't', 'h' => 'u',
'i' => 'v', 'j' => 'w', 'k' => 'x', 'l' => 'y',
'm' => 'z', 'n' => 'a', 'o' => 'b', 'p' => 'c',
'q' => 'd', 'r' => 'e', 's' => 'f', 't' => 'g',
'u' => 'h', 'v' => 'i', 'w' => 'j', 'x' => 'k',
'y' => 'l', 'z' => 'm'}
end
def encrypt_letter(letter)
lowercase_letter = letter.downcase
cipher[lowercase_letter]
end
def encrypt(string)
letters = string.split("")
results = letters.map do |letter|
encrypted_letter = encrypt_letter(letter)
end
results.join
end
def decrypt_letter(letter)
lowercase_letter = letter.downcase
cipher.key(lowercase_letter)
end
def decrypt(string)
letters = string.split("")
results = letters.map do |letter|
decrypted_letter = decrypt_letter(letter)
end
results.join
end
end
No reason; the variable is immediately discarded.
I'd argue it's misleading and uncommunicative on top of it.
Most of the code seems a bit verbose, for example:
def encrypt(string)
letters = string.split("")
results = letters.map do |letter|
encrypted_letter = encrypt_letter(letter)
end
results.join
end
IMO this would be more Ruby-esque as something closer to:
def encrypt(str)
str.chars.collect { |c| encrypt(c) }.join
end
It could be tighter than that, or written in other ways, although some of it is a matter of preference. For example, each_with_object could be used with the shovel operator, but that's less "functional".
(I prefer collect over map when collecting; a preference I find more communicative, if longer.)
Spreading functionality over more lines doesn't make things readable, but it depends on context. People new to Ruby or method chaining might be confused by the (IMO more canonical) one-liner.
As others say, it has no reason. It is obviously a code written by a beginner. In addition to Dave Newton's point, it is a bad habit to define a constant hash as a method cipher. Each time that code is called, a new hash is created. And this has to be done for each letter. That is a huge waste of resource.
Using the hash, you can simply do this:
h = {"a" => "n", "b" => "o", 'c' => 'p', 'd' => 'q',
'e' => 'r', 'f' => 's', 'g' => 't', 'h' => 'u',
'i' => 'v', 'j' => 'w', 'k' => 'x', 'l' => 'y',
'm' => 'z', 'n' => 'a', 'o' => 'b', 'p' => 'c',
'q' => 'd', 'r' => 'e', 's' => 'f', 't' => 'g',
'u' => 'h', 'v' => 'i', 'w' => 'j', 'x' => 'k',
'y' => 'l', 'z' => 'm'}
h.default_proc = ->x{x}
"hello world".gsub(/./, h)
# => "uryyb jbeyq"
But I would rather go with this:
from = "abcdefghijklmnopqrstuvwxyz"
to = "nopqrstuvwxyzabcdefghijklm"
"hello world".tr(from, to)
# => "uryyb jbeyq"
There is no functional reason for it. Sometimes programmers feel more comfortable having an explicit variable destination for their results. Maybe this is one of those cases. Same with the decrypted_letter case.
I would like to use Ruby 1.9.3 to replace accented UTF-8 characters with their ASCII equivalents. For example,
Acsády --> Acsady
The traditional way to do this is using the IConv package, which is part of Ruby's standard library. You can do something like this:
str = 'Acsády'
IConv.iconv('ascii//TRANSLIT', 'utf8', str)
Which will yield
Acsa'dy
One then has to delete the apostrophes. While this method still works in Ruby 1.9.3, I get a warning saying that IConv is deprecated and that String#encode should be used instead. However, String#encode does not offer exactly the same functionality. Undefined characters throw an exception by default, but you can handle them by either setting :undef=>:replace (which replaces undefined chars with the default '?' char) or the :fallback option to a hash which maps undefined source encoding characters to target encoding. I am wondering whether there are standard :fallback hashes available in the standard library or through some gem, such that I don't have to write my own hash to handle all possible accent marks.
#raina77ow:
Thanks for the response. That's exactly what I was looking for. However, after looking at the thread you linked to I realized that a better solution may be to simply match unaccented characters to their accented equivalents, in the way that databases use a character set collation. Does Ruby have anything equivalent to collations?
I use this:
def convert_to_ascii(s)
undefined = ''
fallback = { 'À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'A',
'Å' => 'A', 'Æ' => 'AE', 'Ç' => 'C', 'È' => 'E', 'É' => 'E',
'Ê' => 'E', 'Ë' => 'E', 'Ì' => 'I', 'Í' => 'I', 'Î' => 'I',
'Ï' => 'I', 'Ñ' => 'N', 'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O',
'Õ' => 'O', 'Ö' => 'O', 'Ø' => 'O', 'Ù' => 'U', 'Ú' => 'U',
'Û' => 'U', 'Ü' => 'U', 'Ý' => 'Y', 'à' => 'a', 'á' => 'a',
'â' => 'a', 'ã' => 'a', 'ä' => 'a', 'å' => 'a', 'æ' => 'ae',
'ç' => 'c', 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e',
'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i', 'ñ' => 'n',
'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'o',
'ø' => 'o', 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u',
'ý' => 'y', 'ÿ' => 'y' }
s.encode('ASCII',
fallback: lambda { |c| fallback.key?(c) ? fallback[c] : undefined })
end
You can check for other symbols you might want to provide fallback for here
I suppose what you look for is similar to this question. If it is, you can use the ports of Text::Unidecode written for Ruby - like this gem (or this fork of it, looks like it's ready to be used in 1.9), for example.
The following code will work for a pretty wide variety of European languages, including Greek, which is hard to get right and is not handled by the previous answers.
# Code generated by code at https://stackoverflow.com/a/68338690/1142217
# See notes there on how to add characters to the list.
def remove_accents(s)
return s.unicode_normalize(:nfc).tr("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÑÒÓÔÕÖØÙÚÛÜÝàáâãäåæçèéêëìíîïñòóôõöøùúûüýÿΆΈΊΌΐάέήίΰϊϋόύώỏἀἁἂἃἄἅἆἈἉἊἌἍἎἐἑἒἓἔἕἘἙἜἝἠἡἢἣἤἥἦἧἨἩἫἬἭἮἯἰἱἲἳἴἵἶἷἸἹἼἽἾὀὁὂὃὄὅὈὉὊὋὌὍὐὑὓὔὕὖὗὙὝὠὡὢὣὤὥὦὧὨὩὫὬὭὮὯὰὲὴὶὸὺὼᾐᾑᾓᾔᾕᾖᾗᾠᾤᾦᾧᾰᾱᾳᾴᾶᾷᾸᾹῂῃῄῆῇῐῑῒῖῗῘῙῠῡῢῥῦῨῩῬῳῴῶῷῸ","AAAAAAÆCEEEEIIIINOOOOOOUUUUYaaaaaaæceeeeiiiinoooooouuuuyyΑΕΙΟιαεηιυιυουωoαααααααΑΑΑΑΑΑεεεεεεΕΕΕΕηηηηηηηηΗΗΗΗΗΗΗιιιιιιιιΙΙΙΙΙοοοοοοΟΟΟΟΟΟυυυυυυυΥΥωωωωωωωωΩΩΩΩΩΩΩαεηιουωηηηηηηηωωωωααααααΑΑηηηηηιιιιιΙΙυυυρυΥΥΡωωωωΟ")
end
It was generated by the following long, slow program, which shells out to the linux command-line utility "unicode." If you come across characters that are missing from this list, add them to the longer program, re-run it, and you'll get code output that will handle those characters. For example, I think the list is missing some characters that occur in Czech, such as a c with a wedge on it, as well as Latin-language vowels with macrons. If these new characters have accents on them that aren't on the list below, the program will not strip them until you add the names of the new accents to names_of_accents.
$stderr.print %q{
This program generates ruby code to strip accents from characters in Latin and Greek scripts.
Progress will be printed to stderr, the final result to stdout.
}
all_characters = %q{
ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÑÒÓÔÕÖØÙÚÛÜÝàáâãäåæçèéêëìíîïñòóôõöøùúûüýÿ
ΆΈΊΌΐάέήίϊόύώỏἀἁἃἄἅἈἐἑἒἔἕἘἙἜἡἢἣἤἥἦἨἩἫἬἮἰἱἲἴἵἶἸὀὁὂὃὄὅὊὍὐὑὓὔὕὖὗὝὡὢὣὤὥὧὨὩὰὲὴὶὸὺὼᾐᾗᾳᾴᾶῂῆῇῖῥῦῳῶῷῸᾤᾷἂἷ
ὌᾖὉἧἷἂῃἌὬὉἷὉἷῃὦἌἠἳᾔἉᾦἠἳᾔὠᾓὫἝὈἭἼϋὯῴἆῒῄΰῢἆὙὮᾧὮᾕὋἍἹῬἽᾕἓἯἾᾠἎῗἾῗἯἊὭἍᾑᾰῐῠᾱῑῡᾸῘῨᾹῙῩ
}.gsub(/\s/,'')
# The first line is a list of accented Latin characters. The second and third lines are polytonic Greek.
# The Greek on this list includes every character occurring in the Project Gutenberg editions of Homer, except for some that seem to be
# mistakes (smooth rho, phi and theta in symbol font). Duplications and characters out of order in this list have no effect at run time.
# Also includes vowels with macron and vrachy, which occur in Project Perseus texts sometimes.
# The following code shells out to the linux command-line utility called "unicode," which is installed as the debian package
# of the same name.
# Documentation: https://github.com/garabik/unicode/blob/master/README
names_of_accents = %q{
acute grave circ and rough smooth ypogegrammeni diar with macron vrachy tilde ring above diaeresis cedilla stroke
tonos dialytika hook perispomeni dasia varia psili oxia
}.split(/\s+/).select { |x| x.length>0}.sort.uniq
# The longer "circumflex" will first be shortened to "circ" in later code.
def char_to_name(c)
return `unicode --string "#{c}" --format "{name}"`.downcase
end
def name_to_char(name)
list = `unicode "#{name}" --format "{pchar}" --max 0` # returns a string of possibilities, not just exact matches
# Usually, but not always, the unaccented character is the first on the list.
list.chars.each { |c|
if char_to_name(c)==name then return c end
}
raise "Unable to convert name #{name} to a character, list=#{list}."
end
regex = "( (#{names_of_accents.join("|")}))+"
from = ''
to = ''
all_characters.chars.sort.uniq.each { |c|
name = char_to_name(c).gsub(/circumflex/,'circ')
name.gsub!(/#{regex}/,'')
without_accent = name_to_char(name)
from = from+c.unicode_normalize(:nfc)
to = to+without_accent.unicode_normalize(:nfc)
$stderr.print c
}
$stderr.print "\n"
print %Q{
# Code generated by code at https://stackoverflow.com/a/68338690/1142217
# See notes there on how to add characters to the list.
def remove_accents(s)
return s.unicode_normalize(:nfc).tr("#{from}","#{to}")
end
}