Arrays & Floats
In this chapter we're going to deal with arrays and float numbers.
Arrays
Arrays allow you to store multiple elements in a variable in a particular order. Examples:
my_array = [1, 4, 2, 3]
# you can mix different types of objects
another_array = [nil, 1, 2.5, 'Fizz', true, 'Buzz']You can access the elements of an array by its index, where the first is 0, the second is 1, and so on. Example:
# IRB SESSION
> names = ['john', 'mary', 'chris']
#=> ["john", "mary", "chris"]
> names[0]
#=> "john"
> names[1]
#=> "mary"
> names[2]
#=> "chris"Floats
Floats are essentially numbers with a decimal point, unlike Integers, which are whole numbers.
Although we can think in floats simply as "numbers with a decimal point", they have some idiosyncrasies. Here's a notable example:
# IRB SESSION
> 0.1 + 0.2
#=> 0.30000000000000004😳
Do it on your computer and see it by yourself!
Such a weird thing happens because internally, computers use a fixed amount of bits to store these numbers. The binary representation can precisely store powers of two, but many decimal fractions (like 0.1 and 0.2) result in infinitely repeating binary fractions. Computers must truncate these infinite representations, leading to small rounding errors.
If you're really curious about this topic, this website can be interesting for you: https://floating-point-gui.de/.
Average
To illustrate the usage of arrays and floats, we are going to create a function to calculate the average of the numbers in an array.
For this calculation we need to:
sum all the given numbers
divide the sum by the amount of numbers
As we have more than one step to solve this problem, we better write down what we need to do. This helps us to stay focused.
Good, we can now start the project. Let's call it Statistics Calculator:
mkdir statistics_calculator
cd statistics_calculatorStep 1: Calculate the sum
Let's focus on calculating the sum. As usual: test first.
Write the test first
Before spending mental energy thinking about how to implement the sum, let's focus on how we would like to call a function that, hypothetically, already solves such problem.
Let's call the class as StatisticsCalculator and the method would be simply #sum. With this in mind, let's write the test in a file called statistics_calculator_test.rb:
require "minitest/autorun"
require_relative "statistics_calculator"
class StatisticsCalculatorTest < Minitest::Test
def test_sum_numbers_in_array
numbers = [1, 2, 3]
expected = 6
actual = StatisticsCalculator.new.sum(numbers)
assert_equal expected, actual
end
endRun the test and check the error.
Write the minimal amount of code for the test to run
Solve the error messages until the test fails without errors. By doing this you should now have a file named statistics_calculator.rb with contents like this:
class StatisticsCalculator
def sum(numbers)
end
endNow your test is failing with no errors.
Write enough code to make the test pass
When we have an array, it is very common to have to iterate over them. Getting the sum numbers in an array is a good example, as we need to go through each element and add them in an accumulator.
To iterate over each element we use the Array#each method, and to perform actions with the elements we also pass a block with a parameter (which we learned in the previous chapter).
So, to calculate the sum we do this:
class StatisticsCalculator
def sum(numbers)
accumulator = 0
numbers.each { |n| accumulator += n }
accumulator
end
endRun the test and it should pass. A passing test means that it's now time for refactoring...
Refactor
After carefully looking at the list of available methods in the the Array class documentation I noticed a method called #sum... 🤔
The documentation says:
sum(init = 0) → object sum(init = 0) {|element| ... } → object
When no block is given, returns the object equivalent to:
sum = init array.each {|element| sum += element } sumFor example,
[e1, e2, e3].sumreturnsinit + e1 + e2 + e3.
😳 This looks a lot like the code we just wrote for our sum function!
Let's see if we can replace our code with a simple method call, like this:
def sum(numbers)
numbers.sum
endRun the test and it should pass, which again triggers the decision: should we refactor more?
How could we refactor an oneliner like this?
In this case my refactoring is: delete the function and the test!
We must keep in mind that "code is liability". Each line of code in the repository has a cost and needs a reason to be kept there.
In this case I see no value in keeping a sum function just to be wrapper around the Array#sum call.
The same decision also applies to the tests. The main benefit of keeping our tests is to give us confidence. If I were doubtful about the Array#sum behavior, I would keep a test to validate it, but I trust the Ruby language maintainers (otherwise I wouldn't build serious web applications on top of this technology). I also believe they have tests for Array#sum. Therefore I'll delegate to them the responsibility to keep it working.
So, the result of our refactoring is an empty class in statistics_calculator.rb file and an empty test class in statistics_calculator_test.rb, like this:
# statistics_calculator.rb
class StatisticsCalculator
end# statistics_calculator_test.rb
require 'minitest/autorun'
require_relative 'statistics_calculator'
class StatisticsCalculatorTest < Minitest::Test
endOur TODO list is now like this:
Step 2: Calculate the average
Let's do some basic math with a simple example:
Given the numbers 1, 2, 3, and 4; we have
sum: 1 + 2 + 3 + 4 = 10
amount of numbers: 4
average: 10 / 4 = 2.5
This seems to be a good example for our first test.
Write the test first
Put this in your statistics_calculator_test.rb:
class StatisticsCalculatorTest < Minitest::Test
def test_average
numbers = [1, 2, 3, 4]
expected = 2.5
actual = StatisticsCalculator.new.average(numbers)
assert_equal expected, actual, "Given: #{numbers}"
end
endHave you noticed that the assertion expression is a bit different? Turns out that assert_equal method accepts a third (and optional) argument with a custom message to be shown when the assertion fails.
Sometimes it's useful to print a meaningful message when our assertion fails. It should help us to see what we're doing wrong. In this case we're printing the input numbers.
We're ready to run the tests and check the error messages.
Write the minimal amount of code for the test to run
By following the cycle we've been practicing, you should end up with an statistics_calculator.rb like this:
class StatisticsCalculator
def average(numbers)
end
end
Now your test should be failing with no errors, and with a clear message like this:
1) Failure:
StatisticsCalculatorTest#test_average [statistics_calculator_test.rb:8]:
Given: [1, 2, 3, 4].
Expected: 2.5
Actual: nilLook that extra line showing the given numbers. That's the result of that third argument we gave to the assert_equal in our test. This can be helpful so you can quickly check if your tests and assertions are correct.
Now we can work on the actual implementation.
Write enough code to make the test pass
Recapping the steps to get the average:
sum all the given numbers
divide the sum by the amount of numbers
We saw that we can get the sum of all numbers in an array simply using Array#sum.
Have you noticed that sometimes Ruby code reads almost like natural language? With that in mind, try to guess how you can get the amount of elements in an array...
You probably guessed right: Array#length or Array#size. Both do the same: return the count of elements in the array. (If you're in doubt, launch an irb session and perform some tests)
With this knowledge we can try our first implementation:
class StatisticsCalculator
def average(numbers)
numbers.sum / numbers.length
end
endTest failure message:
Failure:
StatisticsCalculatorTest#test_average [statistics_calculator_test.rb:9]:
Given: [1, 2, 3, 4].
Expected: 2.5
Actual: 2😳 What?! That's not what we expected...
I think we need an irb session for some experiments.
# IRB SESSION
> array = [1, 2, 3, 4]
#=> [1, 2, 3, 4]
> array.sum
#=> 10
> array.length
#=> 4
> array.sum / array.length
#=> 2
# let's check with actual numbers:
> 10 / 4
#=> 2As you can see, the expression 10 / 4 returns 2, and not 2.5 as we were expecting.
Now I need to tell you an interesting aspect of Ruby: arithmetic operators are actually methods called on objects.
This concept can be confusing, so let's clarify with an example: 10 is an object, and when we use 10 / 4 we are actually calling the / "slash" method on the 10 object.
In other words: 10 / 4 is a syntactic sugar for 10./(4).
# IRB SESSION
> 10 / 4
#=> 2
> 10./(4)
#=> 2If / "slash" is a method, then we have documentation for it! As we are calling it in an Integer, we should look for the Integer#/ documentation. And there we can see this description:
self / numeric → numeric_result
Performs division; for integer
numeric, truncates the result to an integer
That's why our average function is not working properly! As the divisor is an integer, the result is truncated to an integer.
To solve this we just need to convert the divisor to a float. Let's confirm:
# IRB SESSION
> 10 / 4.to_f
#=> 2.5Nice. Now we can fix our code:
class StatisticsCalculator
def average(numbers)
numbers.sum / numbers.length.to_f
end
endRun the test and it should pass.
Refactor
To be fair, the current function is pretty OK. No need for refactoring.
Assertion with floats
There's one more aspect of average calculation and float numbers that warrants extra exploration: how to write a test when the average results in a repeating decimal?
Let's update our TODO list marking what we did and adding a new test:
Adding a new test
Here's an example with a repeating decimal:
The numbers are 2, 3, and 5; then we have
sum: 2 + 3 + 5 = 10
amount of numbers: 3
average: 10 / 3 = 3.333...
If we want to write a test for this case, how many decimal digits should we use? Let's see what happens with just 3 decimals:
class StatisticsCalculatorTest < Minitest::Test
# ...
def test_average_with_repeating_decimal
numbers = [2, 3, 5]
expected = 3.333
actual = StatisticsCalculator.new.average(numbers)
assert_equal expected, actual, "Given: #{numbers}"
end
endThe tests results in a failure like this:
1) Failure:
StatisticsCalculatorTest#test_average_with_repeating_decimal [statistics_calculator_test.rb:16]:
Given: [2, 3, 5].
Expected: 3.333
Actual: 3.3333333333333335As we can see, creating an assertion with Floats is not that straightforward. Fortunately we have an specific assertion to deal with Floats: assert_in_delta. And it's documentation says:
assert_in_delta(exp, act, delta = 0.001, msg = nil)
For comparing Floats. Fails unless
expandactare withindeltaof each other.
Explaining in another way, it checks if the expected and the actual values are within delta difference of each other.
Let's rewrite our test using this new assertion:
class StatisticsCalculatorTest < Minitest::Test
# ...
def test_average_with_repeating_decimal
numbers = [2, 3, 5]
expected = 3.333
actual = StatisticsCalculator.new.average(numbers)
assert_in_delta expected, actual, 0.001, "Given: #{numbers}"
end
endIf you run the test now it should pass.
This test was added just to learn how to deal with repeating decimals and didn't require any change on our code. Then let's update our TODO list.
If you're doing version control, that's a good time to commit your changes.
Other useful Array methods
As you may have noticed, Ruby’s standard library comes with a lot of useful solutions (for example, Array#sum).
Other quite handy Array methods provided by default are:
#each_with_index– use this when you want to iterate over each element and need the index.#sort– returns a new array with the elements of the original array, but sorted.#sort_by– similar to#sort, but allows you to specify the sorting criterion.#min,#max– returns the minimum/maximum valued element in the array.#minmax– returns a new 2-element array containing the minimum and maximum values.
This is a short list of the Array’s methods. Be sure to spend some time reading the Array class documentation; it’s a worthwhile investment of your time.
Key Concepts
Ruby
Array#sum: get the sum the numbers in an array.
Array#length (aliased as Array#size): get the amount of elements in an array.
The default Ruby library has many other useful methods for handling Arrays.
Division of integers always result in a truncated integer.
Change the divisor to a float to get the result in a float.
Floating point numbers are not always accurate.
Testing
Creating a list of tests we need to write is useful to keep our focus.
assert_equalaccepts a third parameter where we can enrich the failure message.assert_in_deltais a better assertion for comparing floats.
By the way, spending sometime skimming the Minitest assertions documentation is recommended.
Last updated