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 of n 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.

#
# rubocop:disable Metrics/BlockLength
#

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