Launch School: Beginning Ruby (Part 4)

In their Beginning Ruby series, Launch School recorded some live student coding sessions to show how they approached some typical problems.

The first problem in part 4 of the series posed to student Derek was: create a method that reverses a string without using the built-in #reverse method. This was the simplest problem of the three that were posed, yet it was the one that caught me out when I tried all three for myself! Below are my solutions to all three problems.


Problem 1: String Reversal

def reverse_string(str)
temp_arr = str.split("")
reversed_array = []
  1.upto(str.length) do
reversed_array << temp_arr.pop
end
  reversed_array.join('')
end

Derek’s solution was very similar — he used #times to iterate over the arrayified string instead of #upto. Here it is:

def string_reverse(string)
new_array = []
reversed_string = string.split("")

string.length.times do
new_array << reversed_string.pop
end
new_array
end

The thing that got me initially was that I used #split without quotes, which resulted in an array with one element — the string being passed to the method. So, if I passed the string “hello” into the method I got:

temp_arr = str.split => ["hello"]

This was unhelpful. Lesson learned.


Problem 2: FizzBuzz

The second problem posed was the “Hello World” of job interview questions for programmers, according to Launch School instructor Chris Lee. Enter FizzBuzz. Funnily enough I fared better with this than the first problem! In a nutshell, the problem is, given a series of numbers from 1 to 15, print out “Fizz” for every number wholly divisible by 3, “Buzz” for every number wholly divisible by 5 and FizzBuzz for every number that both 3 and 5 go into without a remainder.

The clue is in those last 3 words: without a remainder. Enter the modulo operator. #modulo is actually a method of the Numeric class in Ruby and essentially returns the remainder when one number is divided by another. It has the symbol %. A few examples:

15 % 3 => 0 (3 goes into 15 five times, no remainder)
15 % 2 => 1 (2 goes into 15 seven times, remainder 1)
15 % 6 => 3 (6 goes into 15 twice, remainder 3)

Given the above, for a range of numbers 1 to 15, we want to see the following output from our program:

1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz

Armed with this information, we can now get coding.

def fizzbuzz(start_num, end_num)
start_num.to_i
end_num.to_i

start_num.upto(end_num) do |n|
if n % 3 == 0 && n % 5 == 0
print " FizzBuzz"
elsif n % 3 == 0
print " Fizz"
elsif n % 5 == 0
print " Buzz"
else
print " " + n.to_s
end
end
end

Although the above returns the correct output, it is somewhat dissatisfying. We can whittle the above 15 lines of code down to 11 by storing the range of numbers as an array and simplifying the branching conditions using Ruby’s case statement:

def fizzbuzz(start_num, end_num)
numbers = (start_num..end_num).to_a
  numbers.each do |n|
case
when (n % 3 == 0) && (n % 5 == 0) then numbers[n - 1] = "FizzBuzz"
when n % 3 == 0 then numbers[n - 1] = "Fizz"
when n % 5 == 0 then numbers[n - 1] = "Buzz"
end
end
numbers
end

Whilst the second solution is arguably better by virtue of being shorter and clearer I was not convinced it was the most elegant solution. Over to Google for a quick search…

Tom Dalling does a good job of considering the FizzBuzz problem to the nth degree. In his article, he starts out with a general solution — one he calls naïve — that is very similar to my own first attempt:

1.upto(100) do |i| 
if i % 3 == 0 && i % 5 == 0
puts 'FizzBuzz'
elsif i % 3 == 0
puts 'Fizz'
elsif i % 5 == 0
puts 'Buzz'
else
puts i
end
end

His second solution is a DRYed up version of the above:

1.upto(100) do |i| 
fizz = (i % 3 == 0)
buzz = (i % 5 == 0)
puts case
when fizz && buzz then 'FizzBuzz'
when fizz then 'Fizz'
when buzz then 'Buzz'
else i
end
end

This is a nice solution and one I could not have written with my current level of knowledge — I had not known that a case statement can be wrapped in a method call — and since #puts is a method, we get this very elegant bit of code. Nice!

Tom continues to refactor and add features to his code that is way outside of the scope of this beginners article and, once again, my current Ruby knowledge. However, it does make for an interesting read!

If I now refactor my own code in light of Tom’s second effort, we now have:

def fizzbuzz(start_num, end_num)
numbers = (start_num..end_num).to_a

numbers.each do |n|
fizz = (n % 3 == 0)
buzz = (n % 5 == 0)
location = n - 1
puts case
when fizz && buzz then numbers[location] = 'FizzBuzz'
when fizz then numbers[location] = 'Fizz'
when buzz then numbers[location] = 'Buzz'
else n
end
end
end

Derek’s solution to this problem was:

def fizzbuzz(beginning_number, ending_number)
fizzbuzz_array = []
(beginning_number..ending_number).each do |element|
if element % 3 == 0 && element % 5 == 0
fizzbuzz_array << "FizzBuzz"
elsif element % 3 == 0
fizzbuzz_array << "Fizz"
elsif element % 5 == 0
fizzbuzz_array << "Buzz"
else
fizzbuzz_array << element
end
end
  puts fizzbuzz_array.join(", ")
end

This is similar to elements of both of my attempts and works like a charm. Kudos to Derek!


Problem 3: Search Query

The final problem posed to Derek in this video was to write a method that, when passed a hash of query options, returns an array of hashes of items that meet the query criteria.

PRODUCTS = [
{ name: "Thinkpad x210", price: 220 },
{ name: "Thinkpad x220", price: 250 },
{ name: "Thinkpad x250", price: 979 },
{ name: "Thinkpad x230", price: 300 },
{ name: "Thinkpad x230", price: 330 },
{ name: "Thinkpad x230", price: 350 },
{ name: "Thinkpad x240", price: 700 },
{ name: "Macbook Leopard", price: 300 },
{ name: "Macbook Air", price: 600 },
{ name: "Macbook Pro", price: 700 },
{ name: "Macbook", price: 1449 },
{ name: "Dell Latitude", price: 200 },
{ name: "Dell Latitude", price: 650 },
{ name: "Dell Inspiron", price: 300 },
{ name: "Dell Inspiron", price: 450 }
]
query = {
price_min: 240,
price_max: 280,
q: 'thinkpad'
}
query2 = {
price_min: 300,
price_max: 600,
q: = 'dell'
}
def search(query)
# implementation goes here
end
search(query)
# [ { name: "Thinkpad x220", price: 250 } ]
search(query2)
# [ { name: "Dell Inspiron", price: 300 }, { name: "Dell Inspiron", price: 450 } ]

My first attempt at this is as follows:

def search(query)
query[:q].capitalize!
price_range = query[:price_min]..query[:price_max]
  results = []
  PRODUCTS.each do |product|
if product[:name].split(' ')[0] == query[:q] && price_range.cover?(product[:price])
results << product
end
end

results.empty? ? "Sorry your query returned no results" : results
end

This appears to work just fine and as a bonus provides a helpful message to the user if their query cannot be matched to a product in the main array. However, it is quite a lot of code and a bit clumsy. A better solution is to use the #select method on PRODUCTS array as was done by Derek in his solution (more on this below).

There are a couple of things I learned from this exercise, the first is the most important. Like Derek, I started to try and solve the problem in my head. The problem with that is that, for a relatively complex problem (or even a simple one if you are new to programming) is that you soon end up with very muddled thoughts. So, when coding, the trick is to take “baby steps” and not try and solve the whole thing in your head in one go, as instructor Chris Lee put it in the video.

the trick is to take “baby steps”

Once I realised what I was doing, I stopped and simply said “Okay, what are we taking into the method and do we want to return?”. My initial code then looked like this:

def search(query)
results = []

PRODUCTS.each do |product|
  end
  results
end

Which was the spark I needed to get going. The rest was pretty straight forward after a bit of experimenting in IRB. I know my solution may not be the most elegant, but it does at least appear to work!

Annoyingly though in doing the above I kind of forgot about #select and I got into flow writing my #each iterator despite knowing that using #select was the better option right at the start of the exercise (when I was doing the solve-it-in-my-head-in-one-go thing). As soon as I had finished though, I realised my mistake, but this is a post about what I learned — warts and all, not just a review of polished code. So my ugly, fat code stands.

The second thing I learned through this exercise is that if you are using the #cover? method on a range, the order of the minimum and maximum values matters — the following code returns false:

price_range = 280..240
price_range.cover?(250)
=> false

Originally (for reasons beyond me) I had coded the price range from price_max to price_min. When the first query didn’t return the expected result I was momentarily puzzled. This kind of bug is of the nasty variety, as my code was valid and not returning an error of any kind. Fortunately, I quickly guessed the problem and corrected the bad code. This is why testing is so important I guess.

Derek’s solution (much more succinct, much nicer effort using #select) was as follows:

def search(query)
output_array = []
output_array = PRODUCTS.select do |element|
(query[:price_min]..query[:price_max]).include?(element[:price]) && (element[:name].downcase.include?(query[:q])
end
p output_array
end

Instructor Chris Lee then pointed out that #select will return a new array by default and so refactored Derek’s code thus:

def search(query)
PRODUCTS.select do |element|
(query[:price_min]..query[:price_max]).include?(element[:price]) && (element[:name].downcase.include?(query[:q])
end
end

Nicer still and, yes, I was kicking myself for ‘forgetting’ about #select and jumping straight on #each when I coded my own search method!

Like what you read? Give Jamie Finlay a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.