HackerRank Ruby Tutorial¶
Here are my notes on the HackerRank Ruby tutorial. Many examples are my own (some based on their examples, some entirely mine). Most notes and explanations are my own too and not just a verbatim copy from the website.
While solving the challenges, one can observe that to make things simple, many of them do not handle edge cases.
One such example is the skip_animals()
method.
What if we skip more then the length of the input array?
Nothing seems to account for that in the description or the example.
In any case, most of the examples on this page try to be a bit careful with edge cases.
For example, in the mask_article()
, we do not add <strike>
tags around empty strings and have proper test cases for such scenarios.
We apply the same thoughtful care to in many other situations as well.
The solutions are implemented using the methods and approaches indicated in the description of the challenge, but keep in mind that many of them could d probably implemented in a better way if we used other ideas and concepts.
Hello World¶
print "Hello, world!"
self¶
self
is the default receiver for messages.
methods¶
Every object has methods.
> 1.even?
» false
> 1.odd?
» true
> (1..3).to_a.inject(&:+)
» 6
Monkey Patch Integer and create `range?’¶
class Integer
##
# Checks whether the receiver is between `x`
# and `y`, inclusive.
#
def range?(x, y)
self >= x && self <= y
end
end
#
# Is 1 between 0 and 3‽
#
p 1.range?(0, 3)
Accessing Array Elements¶
$ irb --simple-prompt
> xs = (-3..5).to_a
» [-3, -2, -1, 0, 1, 2, 3, 4, 5]
> xs.first
» -3
> xs[0]
» -3
> xs.last
» 5
> xs[-1]
» 5
> xs.take(3)
» [-3, -2, -1]
> xs.drop(3)
» [0, 1, 2, 3, 4, 5]
Modifying Arrays¶
push
adds to the end.insert
inserts at given index.unshift
prepend elements to the beginning.
> xs = [1, 2]
» [1, 2]
At position 1, insert 10 and 20, moving 2 “to the right”.
> xs.insert(1, 10, 20)
» [1, 10, 20, 2]
Add 3 and 4 to the end of the array.
> xs.push(3, 4)
» [1, 10, 20, 2, 3, 4]
Prepend -1 and 0 to the beginning of the array.
> xs.unshift(-1, 0)
» [-1, 0, 1, 10, 20, 2, 3, 4]
pop
deletes from the end.shift
deletes from the beginning.delete_at
deletes at given index.delete
deletes all occurrences of given element.
> xs = (1..9).to_a
» [1, 2, 3, 4, 5, 6, 7, 8, 9]
Delete the last element. 9 is gone from xs
.
> xs.pop
» 9
> xs
» [1, 2, 3, 4, 5, 6, 7, 8]
Delete the first element. 1 is gone from xs
.
> xs.shift
» 1
> xs
» [2, 3, 4, 5, 6, 7, 8]
Delete at position 3. 5 is gone from xs
.
> xs.delete_at(3)
» 5
> xs
» [2, 3, 4, 6, 7, 8]
Delete all occurrences of 6. We only have one 6, but it is gone from xs
.
> xs.delete(6)
» 6
>
> xs
» [2, 3, 4, 7, 8]
Filtering Arrays¶
Both select
and reject
return a new array without modifying the original array.
> xs = (1..9).to_a
» [1, 2, 3, 4, 5, 6, 7, 8, 9]
> xs.select(&:odd?)
» [1, 3, 5, 7, 9]
> xs.reject(&:odd?)
» [2, 4, 6, 8]
To modify the array in place, we use keep_if
and delete_if
.
> xs = (1..9).to_a
» [1, 2, 3, 4, 5, 6, 7, 8, 9]
> xs.keep_if { |x| x < 5 }
» [1, 2, 3, 4]
> xs
» [1, 2, 3, 4]
> xs = (1..9).to_a
» [1, 2, 3, 4, 5, 6, 7, 8, 9]
>
> xs.delete_if { |x| x < 5 }
» [5, 6, 7, 8, 9]
>
> xs
» [5, 6, 7, 8, 9]
Reject all elements divisible by 3:
> xs = (1..9).to_a
» [1, 2, 3, 4, 5, 6, 7, 8, 9]
> xs.reject { |n| n % 3 == 0 }
» [1, 2, 4, 5, 7, 8]
Or we can create a proc and convert it to a block with the &
trick:
div_by_3 = Proc.new do |n|
n % 3 == 0
end
p (1..9).to_a.reject(&div_by_3)
#
# → [1, 2, 4, 5, 7, 8]
##
Select only numbers divisible by 4:
div_by_4 = Proc.new { |n| n % 4 == 0 }
p (1..9).to_a.select(&div_by_4)
#
# → [4, 8]
##
Keep only negative numbers:
is_negative = lambda { |n| n < 0 }
p (-3..3).to_a.reject(&is_negative)
#
# → [0, 1, 2, 3]
##
Keep only positive numbers:
is_positive = -> (n) { n > 0 }
p (-3..3).to_a.select(&is_positive)
#
# → [1, 2, 3]
##
Careful with precedence:
(-3..3).reject({ |n| n < 0 })
#
# syntax error, unexpected '|', expecting '}'
# possibly useless use of < in void context
# syntax error, unexpected '}', expecting end-of-input
##
The parentheses on reject()
causes problems. Drop them:
Hashes¶
Create an empty hash:
h = {}
g = Hash.new
Create hash with all keys having 1 as default value:
> h = Hash.new(1)
» {}
> h[:k]
» 0
> g = {}
» {}
> g.default = 1
» 1
> g[:foo]
» 1
Two syntaxes:
> yoda = { 'name' => 'Yoda', 'skill' => 'The Force' }
» {"name"=>"Yoda", "skill"=>"The Force"}
> luke = { :name => 'Luke', :skill => 'Fast Learner' }
» {:name=>"Luke", :skill=>"Fast Learner"}
> ahsoka = { name: 'Ahsoka Tano', skill: 'Lightsaber' }
» {:name=>"Ahsoka Tano", :skill=>"Lightsaber"}
We can iterate over hash keys with each
:
jedi {
id: 1,
name: 'Yoda',
skill: 'The Force'
}
jedi.each do |k, v|
p k v
p v
end
#
# → :id
# → 1
# → :name
# → "Yoda"
# → :skill
# → "The Force"
##
jedi.each do |arr|
p arr
end
#
# → [:id, 1]
# → [:name, "Yoda"]
# → [:skill, "The Force"]
##
Other cool stuff:
> h = { 1 => 1, 2 => 4, 3 => 9, 4 => 16, 5 => 25 }
> h.keep_if { |k, v| k > 3 }
» {4=>16, 5=>25}
h = { 4 => 16, 5 => 25 }
> h[:foo] = :bar
» :bar
> h
» {4=>16, 5=>25, :foo=>:bar}
> h.delete_if { |k, v| k.is_a? Integer }
» {:foo=>:bar}
unless¶
class User
def initialize(name, is_admin)
@name = name
@admin = is_admin
end
def say_hello
p "Hello, #{@name}!"
end
def admin?
@admin
end
end
users = [
User.new('Yoda', true),
User.new('Ahsoka', false),
User.new('Aayla', false),
]
users.each do |user|
unless user.admin?
user.say_hello
end
end
#
# → "Hello, Ahsoka!"
# → "Hello, Aayla!"
##
loop, break if¶
class Coder
def initialize(name)
@name = name
@level = 0;
end
def level
@level
end
def master?
@level >= 100
end
def practice
@level = @level + 10
p "Got to level #{@level}"
end
end
##
# Practice until you become a master.
#
coder = Coder.new('Aayla Secura')
loop do
break if coder.master?
coder.practice
end
#
# → "Got to level 10"
# → "Got to level 20"
# → "Got to level 30"
# → "Got to level 40"
# → "Got to level 50"
# → "Got to level 60"
# → "Got to level 70"
# → "Got to level 80"
# → "Got to level 90"
# → "Got to level 100"
##
What about this oneliner?
developer = Coder.new('Ahsoka Tano')
developer.practice until developer.master?
#
# → "Got to level 10"
# → "Got to level 20"
# → "Got to level 30"
# → "Got to level 40"
# → "Got to level 50"
# → "Got to level 60"
# → "Got to level 70"
# → "Got to level 80"
# → "Got to level 90"
# → "Got to level 100
##
group_by¶
Note how in the first two cases numbers are grouped into true
and false
, while in the third example, they are grouped into 0
and 1
.
The comparison inside the block causes the grouping to be in a certain way.
With the first two cases, the block returns a boolean, while in the third case, it returns ether 0 or 1.
> (1..5).group_by(&:odd?)
» {true=>[1, 3, 5], false=>[2, 4]}
> (1..5).group_by { |n| n % 2 == 0 }
» {false=>[1, 3, 5], true=>[2, 4]}
> (1..5).group_by { |n| n % 2 }
» {1=>[1, 3, 5], 0=>[2, 4]}
Hash Gotcha!¶
yoda = { name: 'Yoda', level: 100 }
p yoda.level
#
# NoMethodError (undefined method `level' for
# {:name=>"Yoda", :level=>100}
##
yoda.level
syntax is trying to send the message level
(call the method level
) to the receiver yoda
.
What we need is to access the symbol:
p yoda[:level]
To be a jedi master, your skill level must be >= 80. A padawan has skill level < 80.
jedis = {
'Yoda': 100,
'Ahsoka Tano': 93,
'Aayla Secura': 91,
'Luke Skywalker': 93,
'Anakin Skywalker': 60
}
groups = jedis.group_by do |_k, v|
v < 80 ? :padawan : :master
end
ap groups[:padawan]
#
# → [
# → [0] [
# → [0] :"Anakin Skywalker",
# → [1] 60
# → ]
# → ]
##
ap groups[:master]
#
# → [
# → [0] [
# → [0] :Yoda,
# → [1] 100
# → ],
# → [1] [
# → [0] :"Ahsoka Tano",
# → [1] 93
# → ],
# → [2] [
# → [0] :"Aayla Secura",
# → [1] 91
# → ],
# → [3] [
# → [0] :"Luke Skywalker",
# → [1] 93
# → ]
# → ]
##
Arrays¶
Create an empty array:
xs = []
ys = Array.new
An array with 3 single nil
elements:
xs = [nil, nil, nil]
ys = Array.new(3)
zs = Array.new(3, nil)
An array with 5 elements whose values are false.
xs = [false, false, false, false, false]
ys = Array.new(5, false)
We can use subscript notation with range syntax to return a slice of the array:
> xs = (1..9).to_a
> xs[0..3]
» [1, 2, 3, 4]
> xs[0...3]
» [1, 2, 3]
>xs[2, 5]
» [3, 4, 5, 6, 7]
Currying¶
Currying is a technique in which a function accepts
n
parameters and turns it into a sequence ofn
functions, each of them take 1 parameter.
add = ->(x, y) { x + y }
ap add.call(-1, 1)
#
# → 0
##
##
# Partially apply `call` to 1.
#
add1 = add.curry.call(2)
ap add1.call(10)
#
# → 11
##
Remember we can use the .()
short syntax (among others, more obscure 😱).
add = ->(x, y) { x + y }
ap add.(-1, 1)
#
# → 0
##
##
# Partially apply `add` to 1.
#
add1 = add.curry.(2)
ap add1.(10)
#
# → 11
##
Lazy¶
Ruby 2.0 introduced lazy evaluation, which can work with potentially infinite data structures (more or less like in Haskell 💖 λ).
Some initial, simple examples:
The first five positive integers:
> 1.upto(Float::INFINITY).lazy.first(5)
[1, 2, 3, 4, 5]
The first five negative integers:
>> -1.downto(-Float::INFINITY).lazy.first(5)
=> [-1, -2, -3, -4, -5]
The first eight odd positive numbers:
1.upto(Float::INFINITY).lazy.select {|n| n.odd?}.first(8)
[1, 3, 5, 7, 9, 11, 13, 15]
10 negative even integers starting at -1e5:
(-1e5.to_i).downto(-Float::INFINITY).lazy.select {|n| n.even?}.first(10)
=> [-100000,
-100002,
-100004,
-100006,
-100008,
-100010,
-100012,
-100014,
-100016,
-100018]
Note that we do .to_i
because exponential notation makes the value a Float
, not a Integer
, and upto
and downto
work on Integer
(not Float
). See:
>> -1e1.class
=> Float
>> (-1e1.to_i).class
=> Integer
Lazy Array of Powers¶
An example with lazy to generate an array of powers:
require 'rspec'
##
# Make array of `len` powers to the `exp` exponent.
#
def make_pow_arr(exp, len)
##
# Each integer from 1 to infinity is the base,
# which we raise to the `exp` exponent until
# we reach `len` length.
#
1.upto(Float::INFINITY).lazy.map do |base|
base ** exp
end.first(len)
end
describe 'lazy pow()' do
it 'should find power of single number' do
expect(make_pow_arr(2, 1)).to eq [1 ** 2]
expect(make_pow_arr(4, 1)).to eq [1 ** 4]
end
it 'should find power of first two integers' do
expect(make_pow_arr(3, 2)).to eq [1 ** 3, 2 ** 3]
end
it 'should find power of first three integers' do
expect(make_pow_arr(2, 3)).to eq [1 ** 2, 2 ** 2, 3 ** 2]
end
it 'should find pwer of first 6 integers' do
expect(make_pow_arr(2, 6)).to eq [
1 ** 2,
2 ** 2,
3 ** 2,
4 ** 2,
5 ** 2,
6 ** 2
]
end
end
Lazy Array of Palindromic Primes¶
require 'rspec'
require_relative 'is_palindrome_v2'
describe 'palind?()' do
it 'should be true for empty string' do
expect(palind?('')).to eq true
end
it 'shold be true for single char strings' do
expect(palind?('z')).to eq true
expect(palind?('7')).to eq true
end
it 'shold work with 2-char strings' do
expect(palind?('zz')).to eq true
expect(palind?('77')).to eq true
expect(palind?('xy')).to eq false
expect(palind?('76')).to eq false
end
it 'should work with 3-or-more char strings' do
expect(palind?('ana')).to eq true
expect(palind?('anna')).to eq true
expect(palind?('racecar')).to eq true
expect(palind?('rotator')).to eq true
expect(palind?('')).to eq true
expect(palind?('level')).to eq true
expect(palind?('any')).to eq false
expect(palind?('thought')).to eq false
end
end
##
# Checks wheter `s` is a palindrome.
#
# This is a recursive solution.
#
# ASSUME: `s` is a string with no uppercase chars.
#
# REFERENCES:
#
# • https://www.dictionary.com/e/palindromic-word/
#
def palind?(s)
return true if [0, 1].include?(s.size)
return false if s[0] != s[-1]
palind?(s[1, s.size - 2])
end
require 'rspec'
require_relative 'is_prime_v1'
describe 'prime?(n)' do
it 'should return false for non-prime numbers' do
expect(prime?(0)).to eq false
expect(prime?(1)).to eq false
expect(prime?(4)).to eq false
expect(prime?(22)).to eq false
expect(prime?(192)).to eq false
end
it 'should return true for non-prime numbers' do
expect(prime?(2)).to eq true
expect(prime?(13)).to eq true
expect(prime?(191)).to eq true
end
end
##
# Checks whether `n` is prime.
#
# 0 and 1 are not primes.
#
# References:
#
# • https://en.wikipedia.org/wiki/Prime_number
# • https://math.stackexchange.com/questions/5277/determine-whether-a-number-is-prime
# • https://www.geeksforgeeks.org/c-program-to-check-whether-a-number-is-prime-or-not/
#
def prime?(n)
return false if n.zero?
return false if n == 1
lim = Math.sqrt(n)
is_prime = true
i = 2
while i <= lim
if (n % i).zero?
is_prime = false
break
end
i += 1
end
is_prime
end
require 'rspec'
require_relative 'palindromic_primes_v1'
describe 'prime_palind()' do
it 'should generate the first two palindromic primes' do
expect(prime_palind(2)).to eq [2, 3]
end
it 'should generate the first three palindromic primes' do
expect(prime_palind(3)).to eq [2, 3, 5]
end
it 'should generate the first ten palindromic primes' do
expect(prime_palind(10)).to eq [
2,
3,
5,
7,
11,
101,
131,
151,
181,
191
]
end
end
require_relative 'is_prime_v1'
require_relative 'is_palindrome_v1'
##
# Generates the first `len` palindromic primes.
#
def prime_palind(len)
1.upto(Float::INFINITY).lazy.select do |n|
prime?(n) && palind?(n.to_s)
end.first(len)
end
Blocks¶
This is what HackerRank expect in that “fill the blanks” ill-explained exercise:
def factorial
yield
end
n = gets.to_i
factorial do
puts (1..n).inject(:*)
end
Just for kicks, here’s a recursive definition of factorial:
require 'rspec'
##
# Computes the factorial of `n`.
#
# This is a recursive definition.
#
# ASSUME: `n` is an integer greater than or equal to 1.
#
def factorial(n)
return 1 if n == 1 || n.zero?
n * factorial(n - 1)
end
describe 'factorial()' do
it 'should compute the factorial of 1' do
expect(factorial(1)).to eq 1
end
it 'should compute the factorial of 2' do
expect(factorial(2)).to eq 2 * 1
end
it 'should compute the factorial of 5' do
expect(factorial(5)).to eq 5 * 4 * 3 * 2 * 1
end
end
And this using inject
:
##
# Computes the factorial of `n`.
#
# This approach uses `inject` cleverly :)
#
# ASSUME: `n` is an integer greater than or equal to 1.
#
def factorial(n)
(1..n).inject(&:*)
end
Blocks are not objects (a rare exception when something is not an object in Ruby) therefore they can’t be referenced by variables.
Procs¶
Procs are like “saved blocks”, except that procs are objects (unlike blocks). They can be bound to a set of local variables.
##
# A function-like object that takes one numeric parameter and
# increments it by 1.
#
add1 = proc { |n| n + 1 }
##
# Takes an `x` value and a proc and call the proc with the value.
#
def f(x, a_proc)
a_proc.call(x)
end
p f(0, add1)
We run the passed proc with .call()
. Other way would be a_proc.(x)
and a_proc[x]
and a_proc === 1
.
You don’t believe in the force me, do you‽
See it for yourself:
>> f = proc {|n| n + 1}
=> #<Proc:0x000055ca6bc6b200 (pry):1>
>> f.call(1)
=> 2
>> f.(1)
=> 2
>> f[1]
=> 2
>> f === 1
=> 2
This is the challenge in HackerRank:
def square_of_sum (my_array, proc_square, proc_sum)
sum = proc_sum.call(my_array)
proc_square.call(sum)
end
proc_square_number = proc { |x| x * x }
proc_sum_array = proc { |xs| xs.inject(&:+) }
my_array = gets.split().map(&:to_i)
puts square_of_sum(my_array, proc_square_number, proc_sum_array)
Lambdas¶
A method that returns a lambda using the affectionately called stabby lambda syntax.
def add1(x)
-> { x + 1 }
end
p add1.call(0)
Note that x
is in scope inside the lambda braces.
Define add1
without the method surrounding the returned lambda.
Again, using the stabby syntax:
add1 ->(x) { x + 1 }
p add1.call(x)
No spaces between ->
and the opening parenthesis.
It has to do with Rubocop default rule checking for that space.
It seems Ruby 1.8 would produce an error if there was a space there.
Later versions started allowing it, but now some people think it should always be without the space as a matter of style.
See:
Using lambda
keyword:
add1 = lambda { |x| x + 1 }
Example for calculating area of rectangle and triangle:
##
# A lambda that computes the area of a rectangle.
#
# The math formula is:
#
# A = base * length
#
area ->(b, l) { b * l }
#
# The area of a triangle is computed by the following formula:
#
# A = 1/2 * base * length
#
area_rectangle = area(2, 3).call
area_triangle = (1 / 2) * area(2.0, 3).call
p area_rectangle
p area_triangle
1/2 is 0.5, but in Ruby operations with integers results in integers.
1 / 2
is 0
😲, unless we make at least one of the numbers a decimal, like 1.0 / 2
, which then prints 0.5
.
That is why we pass 2.0 above, so that we have at least one decimal/floating point number, which in turn causes all the others to be treated as floats as well.
Finally, this is the challenge on HackerRank:
# Write a lambda which takes an integer and square it.
square = ->(x) { x * x }
# Write a lambda which takes an integer and increment it by 1.
plus_one = lambda { |x| x + 1 }
# Write a lambda which takes an integer and multiply it by 2.
into_2 = lambda { |x| x * 2 }
# Write a lambda which takes two integers and adds them.
adder = ->(x, y) { x + y }
# Write a lambda which takes a hash and returns an array of hash values.
values_only = lambda { |h| h.values }
input_number_1 = gets.to_i
input_number_2 = gets.to_i
input_hash = eval(gets)
a = square.(input_number_1)
b = plus_one.(input_number_2)
c = into_2.(input_number_1)
d = adder.(input_number_1, input_number_2);
e = values_only.(input_hash)
p a
p b
p c
p d
p e
Closures¶
A closure is a function or method that can be passed around like objects and remembers scope after parent scope function has returned. Blocks, procs and lambdas are closures in Ruby.
Example challenge from HackerRank:
def block_message_printer
message = "Welcome to Block Message Printer"
if block_given?
yield
end
puts "But in this function/method message is :: #{message}"
end
message = gets
block_message_printer { puts "This message remembers message :: #{message}" }
#####################################################################################
def proc_message_printer(my_proc)
message = "Welcome to Proc Message Printer"
my_proc.call(message) # Call my_proc
puts "But in this function/method message is :: #{message}"
end
my_proc = proc { puts "This message remembers message :: #{message}" }
proc_message_printer(my_proc)
######################################################################################
def lambda_message_printer(my_lambda)
message = "Welcome to Lambda Message Printer"
my_lambda.call # Call my_lambda
puts "But in this function/method message is :: #{message}"
end
my_lambda = -> { puts "This message remembers message :: #{message}" }
lambda_message_printer(my_lambda)
Example 1 (remembers x)¶
def add1(f)
f.call
end
x = 1
fn = -> { x + 1 }
#
# `x` is in the top level scope. Yet, `fn` can remember its value
# when called inside `add1`.
#
# When we define `fn`, it references `x`. `fn` will remember the
# value of `x`
p add1(fn)
# → 2
Example 2 (error)¶
def add1(f)
f.call
end
#
# We intentionally do NOT define `x` before creating the lambda/closure.
#
# x = 1 (we don't do this on purpose).
#
#
# `x` is not yet defined. It is defined only later.
#
fn = -> { x + 1 }
x = 10
#
# Useless assignment to variable - `x`
##
p add1(fn)
#
# undefined local variable or method `x'
#
# Because no `x` was defined before we defined the lambda/closure,
# we get an error saying `x` does not exist.
##
Example 3 (remembers 2nd x)¶
def add1(f)
f.call
end
x = 1
fn = -> { x + 1 }
x = 10
p add1(fn)
#
# Which `x` will the closure remember?
#
# x = 10 will be remembered. The output is 12.
#
# So, if `x` is defined earlier, and then reassigned, it remembers
# its last value. But as we saw in the previous example, if it is
# only defined after we declare the closure, then it throws an error.
#
tip
Take a look at the source code in Gitlab and compare the closure examples with Ruby and JavaScript.
Currying and Partial Application¶
This is manual currying. We return a lambda that takes one parameter, which returns another lambda that takes another parameter, which then returns the final result.
require 'rspec'
##
# Raises `base` to the `exponent`. Curried
#
power = lambda do |base|
lambda do |exponent|
base ** exponent
end
end
describe 'power() curried' do
it 'should work with 2 and 3' do
expect(power.call(2).call(3)).to eq 8
end
it 'should work with -2 and 3' do
expect(power.call(-2).call(3)).to eq(-8)
end
it 'should work with 2 and 8' do
expect(power.call(2).call(8)).to eq(256)
end
it 'should work with 2 and -2' do
#
# 2 ** -3 is 1/8.
#
expect(power.call(2).call(-3)).to eq(1.0 / 2 / 2 / 2)
end
end
Another example, this one from the HackerRank challenge:
require 'rspec'
def fact(n)
(1..n).inject(&:*)
end
combination = lambda do |n|
lambda do |r|
fact(n) / (fact(r) * fact(n - r))
end
end
n_c_r = combination.call(4)
p n_c_r.call(2)
describe 'combination() curried' do
it 'should work on 4 and 2' do
expect(combination.call(4).call(2)).to eq 6
end
end
Arguments and Splat Operator¶
We can use *
splat operator in many situations.
Here’s one case where it works as the rest ...params
in ECMAScript, that is, it collects all parameters into an array.
Zero params would mean an empty array.
require 'rspec'
##
# Sums all arguments.
#
# Note the `*` in front of `xs`. It is the splat operator.
#
def sum_arr(*xs)
xs.inject(0) { |acc, e| acc + e }
end
describe 'sum_arr()' do
it 'should sum zero arguments' do
expect(sum_arr).to eq 0
end
it 'should sum 1 arguments' do
expect(sum_arr(42)).to eq 42
expect(sum_arr(-1)).to eq(-1)
end
it 'should sum 2 or more arguments' do
expect(sum_arr(0, 0, 0)).to eq 0
expect(sum_arr(-1, 1)).to eq 0
expect(sum_arr(-1, -10, -100, -1000)).to eq(-1111)
end
end
Example from HackerRank challenge:
require 'rspec'
def full_name(first, *rest)
[first, *rest].join(' ')
end
describe 'full_name()' do
it 'should work with single name' do
expect(full_name('Yoda')).to eq 'Yoda'
end
it 'should work with two-word names' do
expect(full_name('Ahsoka', 'Tano')).to eq('Ahsoka Tano')
end
it 'should work with three-or-more-word names' do
expect(
full_name('Albus', 'Percival', 'Wulfric', 'Brian', 'Dumbledore')
).to eq 'Albus Percival Wulfric Brian Dumbledore'
end
end
Keyword Arguments¶
Before Ruby 2, people used the “options (or config) object” pattern (like we do in ECMAScript) to provide multiple parameters to a function/method in a saner way than having too many positional parameters. Ruby 2 introduced keyword arguments.
Here’s one example where we use tries
keyword argument defaulting to 2.
It is a made-up example show to showcase keyword arguments (and as a by-product, also show how to mock Kernel#random
).
require 'rspec'
##
# Tries two generate the number 3. Attempts two times by default
# before giving up.
#
# Returns 3 if it is generated within `tries` tries; else,
# return `nil`.
#
def gen3(tries: 2)
##
# A random number between 1 and 4, inclusive.
#
num = rand(1..4)
return num if num == 3 && tries.positive?
##
# Recurse while `tries` is greater than zero.
#
return gen3(tries: tries - 1) if tries.positive?
end
##
# Tries two times to generate 3.
p gen3
##
# 0 tries. Will never find 3.
#
p gen3(tries: 0)
##
# 10 tries will make it very likely to find 3 before giving up.
#
p gen3(tries: 10)
describe 'gen3()' do
it 'should never generate 3 with zero tries' do
expect(gen3(tries: 0)).to eq nil
end
it 'should return 3 with a few tries and 3 is generated' do
allow_any_instance_of(Kernel).to receive(:rand).and_return(3)
expect(gen3(tries: 4)).to eq 3
end
it 'should return nil with a few tries when no 3 is generated' do
allow_any_instance_of(Object).to receive(:rand).and_return(2)
expect(gen3(tries: 4)).to eq nil
end
end
Temperature Converter¶
This is my solution for the temperature converter challenge using keyword arguments.
require 'rspec'
##
# A temperature converter lookup table of sorts with a lambda
# for each case.
#
CONVERSION_TABLE = {
fahrenheit: {
celsius: ->(t) { (t - 32) * (5.0 / 9.0) },
kelvin: ->(t) { (t + 459.67) * (5.0 / 9.0) }
},
celsius: {
kelvin: ->(t) { t + 273.15 },
fahrenheit: ->(t) { t * (9.0 / 5.0) + 32 }
},
kelvin: {
fahrenheit: ->(t) { t * (9.0 / 5.0) - 459.67 },
celsius: ->(t) { t - 273.15 }
}
}.freeze
##
# A temperature converter which handles converstions between fahrenheit,
# celsius, and kelvin.
#
def convert(t, input_scale: 'celsius', output_scale: 'kelvin')
CONVERSION_TABLE[input_scale.to_sym][output_scale.to_sym].call(t)
end
describe 'convert()' do
describe 'celsius → kelvin' do
it 'should convert zero' do
expect(convert(0)).to eq 273.15
end
it 'should convert -7' do
expect(convert(
-7,
input_scale: 'celsius',
output_scale: 'kelvin'
)).to eq 266.15
end
end
describe 'celsius → fahrenheit' do
it 'should convert zero' do
expect(convert(
0,
input_scale: 'celsius',
output_scale: 'fahrenheit'
)).to eq 32
end
it 'should convert -7' do
expect(convert(
-7,
input_scale: 'celsius',
output_scale: 'fahrenheit'
)).to eq(19.4)
end
end
describe 'fahrenheit → kelvin' do
it 'should convert zero' do
expect(convert(
0,
input_scale: 'fahrenheit',
output_scale: 'kelvin'
)).to be_within(0.01).of(255.37)
end
it 'should convert -7' do
expect(convert(
-7,
input_scale: 'fahrenheit',
output_scale: 'kelvin'
)).to be_within(0.01).of(251.48)
end
end
describe 'fahrenheit → celsius' do
it 'should convert zero' do
expect(convert(
0,
input_scale: 'fahrenheit',
output_scale: 'celsius'
)).to be_within(0.01).of(-17.77)
end
it 'should convert -7' do
expect(convert(
-7,
input_scale: 'fahrenheit',
output_scale: 'celsius'
)).to be_within(0.01).of(-21.66)
end
end
describe 'kelvin → fahrenheit' do
it 'should convert zero' do
expect(convert(
0,
input_scale: 'kelvin',
output_scale: 'fahrenheit'
)).to be_within(0.01).of(-459.67)
end
it 'should convert -7' do
expect(convert(
-7,
input_scale: 'kelvin',
output_scale: 'fahrenheit'
)).to be_within(0.01).of(-472.27)
end
end
describe 'kelvin → celsius' do
it 'should convert zero' do
expect(convert(
0,
input_scale: 'kelvin',
output_scale: 'celsius'
)).to be_within(0.01).of(-273.15)
end
it 'should convert -7' do
expect(convert(
-7,
input_scale: 'kelvin',
output_scale: 'celsius'
)).to be_within(0.01).of(-280.15)
end
end
end
String Indexing¶
Accessing¶
Consider this string:
>> s = 'Hello!'
=> "Hello!"
Get the char at the last position:
>> s[s.size - 1]
=> "!"
>> s[-1]
=> "!"
Last but one (penultimate, second last):
>> s[-2]
=> "o"
Last but 2 (antepenultimate, third last):
>> s[-3]
=> "l"
First:
>> s[0]
=> "H"
From first to fourth, inclusive:
>> s[0,4]
=> "Hell"
A range from 0 to fourth means five characters:
>> s[0..4]
=> "Hello"
A range from the first to the last one (the entire string):
```irb
>> s[0..-1]
=> "Hello!"
Looks like it would mean “from first to the last one”, but nope…
>> s[0,-1]
=> nil
From the last position, get the next zero chars:
>> s[-1,0]
=> ""
From the last position, get the next one char:
>> s[-1,1]
=> "!"
From the last position, get the next two chars (except from the last position, we can only get that last one, there no next two, only the single one):
>> s[-1, 2]
=> "!"
BEWARE! Our string has length 6, and indexed from 0 to 5 (not 0 to 6 or 1 to 6).
>> s.size
=> 6
But compare these results. Why don’t [6,1]
and [6,3]
return nil
?:
>> s[6]
=> nil
>> s[6,1]
=> ""
>> s[6,3]
=> ""
Only after index 6 we get nil
with the interval:
>> s[7]
=> nil
>> s[7,3]
=> nil
Mutating, Changing¶
Let’s start with this string:
=> "Hello!"
Replace the last char:
>> s[-1] = '.'
=> "."
>> s
=> "Hello."
Delete the last char (the “.”):
Append ‘ World!’:
>> s[5,0] = ' World!'
=> " World!"
>> s
=> "Hello World!"
Add a comma after the 5th position without replacing/overriding any other char, effectively shifting the space after “Hello” and the rest of the string to the right:
>> s[5,0] = ','
=> ","
>> s
=> "Hello, World!"
Position 7 is “W”. Let’s replace from that position, all characters in “World”, with “Ruby”. “World” has 5 chars. So, from position 7, replace the next 5 chars:
>> s = 'Hello, World!'
=> "Hello, World!"
>> s[7]
=> "W"
>> s[7,5] = "Ruby"
=> "Ruby"
>> s
=> "Hello, Ruby!"
Serial Average¶
Note that -10.00
and -20.00
have 6 chars.
Also note we index from 0 to 3, then from 3 to 6, which combined makes 9. That is why we start at 0, then at 3, then at 9.
>> s = '002-10.00-20.00'
=> "002-10.00-20.00"
>> s[0,3]
=> "002"
>> s[3,6]
=> "-10.00"
>> s[9,6]
=> "-20.00"
require 'rspec'
##
# Calculate the averate of XX.XX and YY.YY serial number.
#
# Result on average calculation is rounded to the two decimal
# places. Example:
#
# >> (-12.43 + -56.78) / 2.0
# => -34.605000000000004
# >> ((-12.43 + -56.78) / 2.0).round(2)
# => -34.61
#
# Note how -34.605 becames -34.61.
#
def serial_avg(s)
sss = s[0, 3]
x = s[3, 6].to_f
y = s[9, 6].to_f
avg = ((x + y) / 2.0).round(2)
"#{sss}#{format('%.2f', avg)}"
end
describe 'serial_avg()' do
it 'should work with .00 decimals' do
expect(serial_avg('002-10.00-20.00')).to eq '002-15.00'
end
it 'should work with odd decimals' do
expect(serial_avg('003-10.13-20.48')).to eq '003-15.31'
end
it 'should work with hackerrank test case' do
expect(serial_avg('001-12.43-56.78')).to eq '001-34.61'
end
end
String Iteration¶
Before ruby 1.9, strings where enumerable, and we could do my_str.each
(from Enumerable
).
There were some problems with it because of encoding and people could not iterate over bytes without resorting to tricks.
Since ruby 1.9, the String
class does not bear a each
method anymore.
Instead, we have each_char
, each_byte
, each_codepoint
, and each_line
(among other string methods, of course).
It is said each_char
is more performant than []
and character indexing.
Count Multibyte Chars¶
Here’s the HackerRank challenge about counting multibyte chars in a string.
Unit tests:
describe 'count_mbc()' do
it 'should work with empty string' do
expect(count_mbc('')).to eq 0
end
it 'should work with a single multibyte char' do
# 0x2714
expect(count_mbc('✔')).to eq 1
# 0x0001f4a9
expect(count_mbc('💩')).to eq 1
end
it 'should work with multiple multibyte chars' do
expect(count_mbc('✔💩')).to eq 2
end
it 'should work with mixed ASCII-like and multibyte chars' do
expect(count_mbc('lambda λ')).to eq 1
expect(count_mbc('¥1000')).to eq 1
expect(count_mbc('May the ✔ source be 💩 with λ you!')).to eq 3
end
end
Version 1 using single monolithic method:
##
# Counts the number of multibyte chars in the string `s`.
#
# Example: 'ab λ' has four chars, but only 'λ' is a multibye char.
# The others are ASCII-compabitle, single byte chars (including
# the space). Therefore, 'ab λ' has 1 multibyte char.
#
def count_mbc(s)
num_multibyte_chars = 0
s.each_char do |c|
num_bytes = 0
c.each_byte do |b|
num_bytes += 1
end
if num_bytes > 1
num_multibyte_chars += 1
end
end
num_multibyte_chars
end
Version 2 using helper method:
##
# Counts the number of bytes in the char `c`.
#
def count_bytes(c)
count = 0
c.each_byte do |b|
count += 1
end
count
end
##
# Counts the number of multibyte chars in the string `s`.
#
# Example: 'ab λ' has four chars, but only 'λ' is a multibyte char.
# The others are ASCII-compatible, single byte chars (including
# the space). Therefore, 'ab λ' has 1 multibyte char.
#
def count_mbc(s)
num_multibyte_chars = 0
s.each_char do |c|
if count_bytes(c) > 1
num_multibyte_chars += 1
end
end
num_multibyte_chars
end
String Methods I¶
String#chomp
removes \n
, \r
and \r\n
from the end of a string (unless the default separator has been changed to something else).
String#chop
removes the last char, and note that \n
, \r
and \r\n
are all threated as one single char.
String#strip
is like trim()
in some other languages, which removes leading and trailing whitespace.
Here’s my solution to the process text challenge:
require 'rspec'
require_relative 'process_text_v1'
describe 'process_text()' do
it 'should work with empty array of lines' do
expect(process_text([])).to eq ''
end
it 'should work with array with a few lines' do
expect(
process_text(["Hi, \n", " Are you having fun? "])
).to eq 'Hi, Are you having fun?'
end
end
#
# A sanitized string is one that:
#
# - Contains no newlines.
# - No tabs.
# - Words and punctuation are separated by a single space.
#
##
# Sanitizes a string.
#
def sanitize_string(s)
s.chomp.strip.gsub(/\s+/, ' ')
end
##
# Sanitizes an array of strings.
#
def process_text(lines)
lines.map do |line|
sanitize_string(line).strip
end.join(' ')
end
String Methods II¶
We’ll use includes?
and gsub
.
require 'rspec'
##
# Add HTML `<strike>` tag around `s`.
#
# Empty strings do not get surrounded with the tag lest we would get
# `<strike></strike>`.
#
def strike(s)
return s if s.empty?
"<strike>#{s}</strike>"
end
##
# Strikes all strings in `strs_to_mask` found in `str`.
#
# NOTE: We use `String#dup` so we don't modify the original string
# parameter.
#
def mask_article(str, strs_to_mask)
strs_to_mask.inject(str.dup) do |acc_str, str_to_mask|
acc_str.gsub(str_to_mask, strike(str_to_mask))
end
end
describe 'strike()' do
it 'should not strike empty strings' do
expect(strike('')).to eq ''
end
it 'should strike non-empty strings' do
expect(strike('crap')).to eq '<strike>crap</strike>'
expect(
strike('crappy shit')
).to eq '<strike>crappy shit</strike>'
end
end
describe 'mask_article()' do
it 'should mask one matching string' do
expect(
mask_article('What a crap!', %w[crap])
).to eq 'What a <strike>crap</strike>!'
end
it 'should mask multiple matching strings' do
expect(
mask_article('What a shitty dumb crap!', %w[crap shitty])
).to eq 'What a <strike>shitty</strike> dumb <strike>crap</strike>!'
end
end
Enumerables ‘each_with_index’¶
require 'rspec'
##
# Return the array without the first `skip` elements.
#
def skip_animals(animals, skip)
skipped_with_index = []
animals.each_with_index do |animal, idx|
idx >= skip && skipped_with_index << "#{idx}:#{animal}"
end
skipped_with_index
end
describe 'skip_animals()' do
it 'should not skip any animals with skip of 0' do
expect(
skip_animals(%w[penguin bear fox wolf], 0)
).to eq %w[0:penguin 1:bear 2:fox 3:wolf]
end
it 'should skip first animal with skip of 1' do
expect(
skip_animals(%w[penguin bear fox wolf], 1)
).to eq %w[1:bear 2:fox 3:wolf]
end
it 'should skip first animal with skip of 2' do
expect(
skip_animals(%w[penguin bear fox wolf], 2)
).to eq %w[2:fox 3:wolf]
end
it 'should return only last animal if skip arraay length - 1' do
expect(
skip_animals(%w[penguin bear fox wolf], 3)
).to eq ['3:wolf']
end
it 'should skip all animals if skip is >= array length' do
##
# `skip` is exactly the array length (4).
#
expect(
skip_animals(%w[penguin bear fox wolf], 4)
).to eq []
##
# `skip` is more than the array length (5).
#
expect(
skip_animals(%w[penguin bear fox wolf], 4)
).to eq []
end
end
rot13, map, collect¶
require 'rspec'
##
# Rotate characters 13 positions to the right.
#
# TIP: In vim, `:help rot13` or `:help g?`.
#
def rot13(s)
s.tr('A-Za-z', 'N-ZA-Mn-za-m')
end
def decrypt_msgs(msgs)
msgs.collect { |msg| rot13(msg) }
end
describe 'rot13()' do
it 'should rot13 simple strings' do
expect(rot13('abc')).to eq 'nop'
end
it 'should rot13 strings which wrap arond the alphabet' do
expect(rot13('yza')).to eq 'lmn'
end
it 'should rot13 upper and lower case stringis' do
expect(rot13('aBc')).to eq 'nOp'
end
end
describe 'decrypt_msgs()' do
it 'should return empty array with empty msgs' do
expect(decrypt_msgs([])).to eq []
end
it 'should return decrypted 1-element array message' do
expect(decrypt_msgs(['aBc'])).to eq ['nOp']
end
it 'should return decrypted multiple-element array messages' do
expect(
decrypt_msgs(
[
'aBc',
'yZa',
'Why did the chicken cross the road?'
]
)
).to eq [
'nOp',
'lMn',
'Jul qvq gur puvpxra pebff gur ebnq?'
]
end
end
The String#tr
version above is the same one I once learned with the tr
command line:
$ printf '%s' aBc | tr 'A-Za-z' 'N-ZA-Mn-za-m'
nOp
Or, using bash Here Strings:
$ tr 'A-Za-z' 'N-ZA-Mn-za-m' | <<< 'aBc'
nOp
Enumerable reduce, inject¶
require 'rspec'
require_relative 'arith_geometric_seq_sum_v1'
##
# The function is t(n) = n ** 2 + 1.
#
describe 'sum_terms()' do
it 'should total 0 when n is 0' do
##
# Sum zero terms of the series. Nothing to sum.
# Don't even apply the function.
#
expect(sum_terms(0)).to eq(0)
end
it 'should total 3 when n is 1' do
##
# 0 + 1 ** 2 + 1
# 0 + 1 + 1
# 2
#
expect(sum_terms(1)).to eq(2)
end
it 'should total 7 when n is 2' do
##
# 2 + 2 ** 2 + 1
# 2 + 4 + 1
# 7
#
expect(sum_terms(2)).to eq(7)
end
it 'should total 17 when n is 3' do
##
# 7 + 3 ** 2 + 1
# 7 + 9 + 1
# 17
#
expect(sum_terms(3)).to eq(17)
end
it 'should total 27 when n is 4' do
##
# 17 + 4 ** 2 + 1
# 17 16 + 1
# 34
#
expect(sum_terms(4)).to eq(34)
end
end
##
# Sum the first `n` terms of an arithmetico-geometric sequence.
#
def sum_terms(n)
(1..n).inject(0) { |acc, x| acc + x ** 2 + 1 }
end
Enumerables any, all, none, find¶
any¶
Is there any even number in the array?
>> [1, 3, 5].any? { |x| x.even? }
=> false
>> [1, 3, 8, 5].any? { |x| x.even? }
=> true
Are any values in the hash of the type Integer
?
>> h = { one: 1, two_1: 2.1 }
>> h.any? { |k, v| v.is_a? Integer }
=> true
Are any of the keys a Symbol
?
>> h = { 'one' => 1, :two => 2 }
>> :foo.is_a? Symbol
=> true
>> h.any? { |k, v| k.is_a? Symbol }
=> true
all¶
Do all elements satisfy the predicate? E.g, are all elements integers?
>> [1, 1.1, 2].all? { |x| x.is_a? Integer }
=> false
>> [1, 2].all? { |x| x.is_a? Integer }
=> true
none¶
Are none of the elements are nil
:
>> [:foo, nil, :bar].none? { |e| e.nil? }
=> false
>> [:foo, :bar].none? { |e| e.nil? }
=> true
TIP: Remember we could simply do arr.none?(&:nil?)
.
find¶
Can we find an element that is greater than 5?
>> (1..5).find { |x| x > 5 }
=> nil
>> (1..6).find { |x| x > 5 }
=> 6
find
returns nil
if it can’t find what we are looking for, or it returns the thing we are looking for if we actually find it.
Solution for the challenge¶
#
# Check and return true if any key object within the hash is of the type
# Integer If not found, return false.
#
def func_any(hash)
hash.any? { |k, _v| k.is_a? Integer }
end
#
# Check and return true if all the values within the hash are Integers
# and are < 10 If not all values satisfy this, return false.
#
def func_all(hash)
hash.all? do |_k, v|
v.is_a?(Integer) && v < 10
end
end
#
# Check and return true if none of the values within the hash are nil If
# any value contains nil, return false.
#
def func_none(hash)
hash.none? { |_k, v| v.nil? }
end
#
# Check and return the first object that satisfies either of the
# following properties:
# 1. There is a [key, value] pair where the key and value are both
# Integers and the value is < 20
# 2. There is a [key, value] pair where the key and value are both
# Strings and the value starts with `a`.
#
def func_find(hash)
hash.find do |k, v|
(k.is_a?(Integer) && v.is_a?(Integer) && v < 20) ||
(k.is_a?(String) && v.is_a?(String) && v.start_with?('a'))
end
end
String Encoding¶
Useful snippets regarding encoding:
str.encoding
str.encoding.name
str.encode(dst_encoding, **options)
str.encode(dst_encoding, src_encoding, **options)
Solution:
require 'rspec'
##
# Returns the string `s` in UTF-8 encoding.
#
def transcode(s)
s.encode('UTF-8', 'ISO-8859-1')
end
describe 'transcode()' do
it 'should transcode from ISO-8851-1 to UTF-8' do
expect(transcode('').encoding.name).to eq 'UTF-8'
str = 'coração'.encode('ISO-8859-1')
expect(transcode(str).encoding.name).to eq 'UTF-8'
end
end
NOTE: The solution presented here DOES NOT pass HackerRank tests.
The comments in the challenge explain the tests are probably buggy.
To make tests pass, use s.force_encoding('UTF-8')
.
See these quotes from the challenge comments:
First of all, the exercise is wrong.
force_encoding
forces the encoding of a string without changing the internal byte representation of the string.encode
changes the encoding and the internal byte representation. The exercise asks for transcode a string, butforce_encoding
isn’t a transcode method but encode actually is a transcode method.
this exercise passes only with a wrong answer (as the official doc states,
force_encoding
doesn’t change the internal byte representation).force_encoding
should be used only when a string’s internal representation doesn’t match the Encoding information associated to the string. K-— Torumori
Methods Intro¶
Global methods like:
def hello
'Hello!'
end
p hello
#
# → "Hello!"
##
Are the same as:
class Object
private
def hello
'Hello!'
end
end
p Object.send(:hello)
#
# → "Hello!"
##
# tags: [self, method, object]
def f
##
# Returns the receiver class name (`Object`).
#
self.class.name
end
##
# Does `object` have a method, public or private, called `f`?
#
p Object.respond_to?(:f, true)
#
# → true
##
##
# Prints `f`'s receiver call name as it is what `f`'s
# implementaion returns.
#
p f
#
# → "Object"
##
We already did a prime?
method before. Here’s another version, though:
##
# Checks whether `n` is prime.
#
# 0 and 1 are not primes.
#
# References:
#
# • https://en.wikipedia.org/wiki/Prime_number
# • https://math.stackexchange.com/questions/5277/determine-whether-a-number-is-prime
# • https://www.geeksforgeeks.org/c-program-to-check-whether-a-number-is-prime-or-not/
#
def prime?(n)
return false if n < 2
(2..n).each do |d|
return false if (n % d).zero?
end
true
end
Method Arguments¶
The challenge calls this method take
, but it sounds more like drop
, because it drops, or skips the first skip
elements:
def take(xs, skip = 1)
xs[skip, xs.size]
end
>> take([1, 2, 3, 4], 2)
=> [3, 4]
Anyway, the above method take
skips skip
elements and return the rest of the array.
See what drop
and take
do in Haskell:
ghci> drop 2 [1..5]
[3,4,5]
ghci> take 2 [1..5]
[1,2]
Made a comment about this naming thin in the discussions for this challenge.
Ruby Enumerable Intro¶
def iterate_colors(colors)
color_values = []
colors.each do |color|
color_values << color
end
color_values
end