5
\$\begingroup\$

I just started learning Ruby and thought that this relatively simple HackerRank challenge would be a good opportunity to try to write clean, tested and documented code without getting bogged down in very complex problems.

In this challenge, your task is to write a method which takes an array of strings (containing secret enemy message bits!) and decodes its elements using ROT13 cipher system; returning an array containing the final messages.

I'd like to know if this follows the "Ruby Way" (I'm willing to bet it doesn't!) and I am open to improvements on all aspects of the code, including the documentation, which is meant to follow RDoc format.

I use Ruby v 2.4. Note that, coming from a mostly Python background, there may be some patterns that are very Python-like, if so please point out if there's a more Ruby way of doing it!

I am also planning to learn about TDD libraries with Ruby soon, for now using that short solution from Stack Overflow seemed sufficient for this exercise.

# AssertionError class & assert method copied from Stack Overflow
# https://stackoverflow.com/a/3264330/3626537
class AssertionError < RuntimeError
 # Extends RunTimeError
end
def assert &block
 # Evaluates whether the input block is true and raises AssertionError if not
 # +block+:: the block to evaluate
 raise AssertionError unless yield
end
def is_uppercase_letter? str
 # Evaluates whether a single is a single uppercase letter, i.e.: A-Z
 # +str+:: string to evaluate
 str.length == 1 && /[[:upper:]]/.match(str)
end
assert { is_uppercase_letter? "A" }
assert { !is_uppercase_letter? "a" }
assert { !is_uppercase_letter? "Hello" }
def is_lowercase_letter? str
 # Evaluates whether a string is a single lowercase letter, i.e.: a-z
 # +str+:: string to evaluate
 str.length == 1 && /[[:lower:]]/.match(str)
end
assert { is_lowercase_letter? "a" }
assert { !is_lowercase_letter? "A" }
assert { !is_lowercase_letter? "hello" }
def rot13_char ch
 # Apply ROT13 cipher to a single character
 # - Each letter is rotated by half of the alphabet, or 13 places
 # - Original case is preserved
 # - Non-letter characters remain unchanged
 # - https://en.wikipedia.org/wiki/ROT13
 # +ch+:: the character to apply ROT13 to
 rot_byt = ch.ord + 13
 if is_uppercase_letter? ch
 rot_byt <= "Z".ord ? rot_byt.chr : (rot_byt - 26).chr
 elsif is_lowercase_letter? ch
 rot_byt <= "z".ord ? rot_byt.chr : (rot_byt - 26).chr
 else
 ch
 end
end
assert { rot13_char("W") == "J" }
assert { rot13_char("J") == "W" }
assert { rot13_char("j") == "w" }
assert { rot13_char("w") == "j" }
assert { rot13_char("?") == "?" }
def rot13_string str
 # Apply ROT13 cipher to all characters in a string
 # +str+:: the string to apply ROT13 to
 chars = str.split('')
 results = []
 chars.each do |ch|
 results.push(rot13_char ch)
 end
 results.join("")
end
assert { rot13_string("Why did the chicken cross the road?") == "Jul qvq gur puvpxra pebff gur ebnq?" }
assert { rot13_string("Gb trg gb gur bgure fvqr!") == "To get to the other side!" }
# puts rot13_string "Why did the chicken cross the road?"
# puts rot13_string "To get to the other side!"
# puts rot13_string "Jul qvq gur puvpxra pebff gur ebnq?"
# puts rot13_string "Gb trg gb gur bgure fvqr!"
def rot13 messages
 # Apply ROT13 to an array of "messages" i.e. strings
 # +messages+:: the array of strings to apply ROT13 to
 rotated = []
 messages.each do |msg|
 rotated.push(rot13_string msg)
 end
 rotated
end 
assert do
 input = ["Why did the chicken cross the road?", "To get to the other side!"]
 expected = ["Jul qvq gur puvpxra pebff gur ebnq?", "Gb trg gb gur bgure fvqr!"]
 actual = rot13 input
 expected == actual
end
assert do
 input = ["Jul qvq gur puvpxra pebff gur ebnq?", "Gb trg gb gur bgure fvqr!"]
 expected = ["Why did the chicken cross the road?", "To get to the other side!"]
 actual = rot13 input
 expected == actual
end
200_success
145k22 gold badges190 silver badges478 bronze badges
asked Oct 22, 2017 at 16:15
\$\endgroup\$

1 Answer 1

7
\$\begingroup\$

A Ruby string literal can be enclosed in single or double quotes. The difference is that the latter does string interpolation. Examples (from Double vs single quotes on Stack Overflow):

a = 2
puts "#{a}" # prints the value of a
puts 'a\nb' # just print a\nb 
puts "a\nb" # print a, then b at newline 

That may be wanted (e.g. to include newline characters), or not:

puts rot13_string "#{Wot?}"
// rot13.rb:100:in `<main>': undefined method `Wot?' for main:Object (NoMethodError)

so generally you should enclose the string literals in single-quotes.


The regular expression /[[:upper:]]/ matches all "uppercase alphabetical characters" (Unicode characters in the "Lu" general category, to be precise), and not just the ASCII uppercase letter A...Z.

Therefore your method can produce garbage, e.g. for German "umlauts", or even crash, e.g. for Greek letters:

puts rot13_string "ÄÖÜ"
puts rot13_string "ΔΘΣ"

Output:

???
rot13.rb:42:in `chr': 903 out of char range (RangeError)
 from rot13.rb:42:in `rot13_char'
 from rot13.rb:62:in `block in rot13_string'
 from rot13.rb:61:in `each'
 from rot13.rb:61:in `rot13_string'
 from rot13.rb:86:in `'

That might not be relevant for this specific programming challenge (which probably works with ASCII only).

But generally, to substitute only ASCII letters and ignore all other input, the patterns should be /[A-Z]/ and /[a-z]/, respectively.


The rot13_string can be shortened to

def rot13_string str
 # Apply ROT13 cipher to all characters in a string
 #str+:: the string to apply ROT13 to
 str.each_char.map { |ch| rot13_char ch }.join("")
end

Instead of creating an array with all characters and appending each mapped character to the result array, each_char returns an enumerator for the characters, and map returns an array with the transformed characters.

In the same way, the rot13 function becomes

def rot13 messages
 # Apply ROT13 to an array of "messages" i.e. strings
 # +messages+:: the array of strings to apply ROT13 to
 messages.map { |msg| rot13_string msg }
end 

As an alternative, you can use the tr method which takes two string arguments (with possible ranges) and transforms the given string by replacing each character in the first argument by the corresponding character in the second argument:

def rot13_string str
 # Apply ROT13 cipher to all characters in a string
 # +str+:: the string to apply ROT13 to
 str.tr('A-Za-z', 'N-ZA-Mn-za-m')
end

(From https://rosettacode.org/wiki/Rot-13#Ruby.)

This also works "Unicode-correct" in the sense that all other Unicode characters are preserved. It is less flexible than your approach however, since you cannot simply change the "offset" from 13 to any other number.

answered Oct 22, 2017 at 16:46
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Wow, good catch on Unicode vs. ASCII, I'll be sure to remember the important distinction! \$\endgroup\$ Commented Oct 22, 2017 at 18:02
  • 1
    \$\begingroup\$ Fun fact remark: See also man 1 tr for Ruby's tr's origin. \$\endgroup\$ Commented Oct 22, 2017 at 18:44

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.