Can you please help me?
I'm solving an exercise in Ruby and the result has to be like this:
it 'multiplication table de 1 a 10' do
expect(ArrayUtils.tabuada(10)).to eq [
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20],
[3, 6, 9, 12, 15, 18, 21, 24, 27, 30],
[4, 8, 12, 16, 20, 24, 28, 32, 36, 40],
[5, 10, 15, 20, 25, 30, 35, 40, 45, 50],
[6, 12, 18, 24, 30, 36, 42, 48, 54, 60],
[7, 14, 21, 28, 35, 42, 49, 56, 63, 70],
[8, 16, 24, 32, 40, 48, 56, 64, 72, 80],
[9, 18, 27, 36, 45, 54, 63, 72, 81, 90],
[10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
]
end
it 'multiplication table de 1 a 3' do
expect(ArrayUtils.tabuada(3)).to eq [
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20],
[3, 6, 9, 12, 15, 18, 21, 24, 27, 30],
]
end
My code:
def self.tabuada(n)
(1..n).each do |element|
(1..n-1).each { |item| print "#{element * item}, " }
puts element * n
end
end
tabuada(3)
It's the result:
1, 2, 3
2, 4, 6
3, 6, 9
Any suggestion?
There are two issues in your code:
The test expects you to return a nested array, not to print the result
The inner loop always ends a 10, it is not depending on the n at all
I would start with something like this:
def self.tabuada(n)
(1..n).map do |n|
(1..10).map do |i|
n * i
end
end
end
Or:
def self.tabuada(n)
(1..n).map { |element| Array.new(10) { |i| (i + 1) * element } }
end
Good error messages are very important for a good testing framework.
The error messages should guide you to a solution.
It looks like you are using RSpec, which does indeed have good error messages. So, let's just look at the error message we get:
expected: [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [2, 4, 6, 8, 10, 12, 14, 16, 18, 20], [3, 6, 9, 12, 15, 18, 21, 24,...56, 64, 72, 80], [9, 18, 27, 36, 45, 54, 63, 72, 81, 90], [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]]
got: 1..10
(compared using ==)
So, the error message is telling us that RSpec expected to find an array, but instead your method returned a range. That's interesting, so let's look at what does your method return.
Well, in the absence of an explicit return keyword, a method definition body evaluates to the value of the last expression that was evaluated inside the method definition body. In your case, there is only a single expression inside the method, so the value of that expression is returned:
(1..n).each do |element|
(1..n-1).each { |item| print "#{element * item}, " }
puts element * n
end
What is the return value of (1..n).each? We can simply look at the documentation, and we see that Range#each returns self, i.e. the object that it was called on. So, the return value of your method will always be simply the range 1..n, and not the array of arrays that the test expects.
[Sidenote: I did, in fact, not look up what Range#each returns. Every implementation of each for every collection always returns self, that is part of the contract of each. This is one of the things you just learn over time when you program in Ruby.]
Let's do the simplest possible thing we can do to change the message. That is all we want to do. We don't want to fix everything and we don't want to take a huge step. We just want to make a tiny change that changes the error message.
If we look at the methods available to us, we find the method Enumerable#map, which transforms the elements and returns an array of the transformed elements. That actually sounds quite good, since transforming some numbers into a table of multiplied numbers is pretty much exactly what we want to do.
So, we just change the each to map. Nothing more:
(1..n).map do |element|
(1..n-1).each { |item| print "#{element * item}, " }
puts element * n
end
And then we see what happens:
got: [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil]
That is already better. We are expecting an array, and we are getting an array. Just the wrong array, but we already have the correct type! Before, we were getting a range, now we are getting an array.
Why is this array filled with nils, though? We want numbers! If we look at the documentation of map again, we see that the value of the block is used to fill the array. So, what is the value of the block?
Just as above, the value of the whole block is the value of the last expression evaluated inside the block. The last expression inside the block is
puts element * n
And if we look at the documentation of Kernel#puts, we can see that it does, indeed, return nil! But we want a number instead. Well, we already have a number: element * n is a number! So, instead of printing the number (which the problem description never asked for in the first place) and returning nil, let's just return the number. In other words, just remove the puts:
(1..n).map do |element|
(1..n-1).each { |item| print "#{element * item}, " }
element * n
end
And this is the result:
got: [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
Again, one step closer. We want an array, we have an array. We want numbers, we have numbers. What we actually want, though is not an array of numbers, but an array-of-arrays-of-numbers. If you look closely, you can see that we have here is actually the last array of the array-of-arrays-of-numbers we are expecting (or to put it another way, it is the last row of the table).
Hey, but we already know something that can produce arrays: map can do that! Turning the outer each into a map gave us an array. Naturally, nesting a map within a map will give us nested arrays. So, let's just do that:
(1..n).map do |element|
(1..n-1).map { |item| print "#{element * item}, " }
element * n
end
Hmm … actually, that didn't change anything. It makes sense, really: the value of the block is the value of the last expression, and the last expression is element * n. The map before it does return an array, but we're not doing anything with that array, we are just throwing it away.
So, let's just as an experiment remove the element * n and see what happens:
got: [[nil, nil, nil, nil, nil, nil, nil, nil, nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil], [nil, ... nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil]]
One step forward, one step back, I guess. We wanted an array-of-arrays-of-numbers, originally we had an array-of-numbers and were missing the "nesting" part, now we have a nested array-of-arrays, but we lost the numbers.
But we already know the cause, since we figured it out once before: Kernel#print returns nil, so let's just delete it:
(1..n).map do |element|
(1..n-1).map { |item| "#{element * item}, " }
end
And this is the result:
expected: [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [2, 4, 6, 8, 10, 12, 14, 16, 18, 20], [3, 6, 9, 12, 15, 18, 21, 24,...56, 64, 72, 80], [9, 18, 27, 36, 45, 54, 63, 72, 81, 90], [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]]
got: [["1, ", "2, ", "3, ", "4, ", "5, ", "6, ", "7, ", "8, ", "9, "], ["2, ", "4, ", "6, ", "8, ", "10, "..., "63, ", "72, ", "81, "], ["10, ", "20, ", "30, ", "40, ", "50, ", "60, ", "70, ", "80, ", "90, "]]
Hey, that's actually pretty close! We want an array-of-arrays-of-numbers, and we have an array-of-arrays-of-strings-with-numbers-in-them. Let's see what happens when we remove the string and simply keep the number:
(1..n).map do |element|
(1..n-1).map { |item| element * item }
end
expected: [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [2, 4, 6, 8, 10, 12, 14, 16, 18, 20], [3, 6, 9, 12, 15, 18, 21, 24,...56, 64, 72, 80], [9, 18, 27, 36, 45, 54, 63, 72, 81, 90], [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]]
got: [[1, 2, 3, 4, 5, 6, 7, 8, 9], [2, 4, 6, 8, 10, 12, 14, 16, 18], [3, 6, 9, 12, 15, 18, 21, 24, 27], [4... 32, 40, 48, 56, 64, 72], [9, 18, 27, 36, 45, 54, 63, 72, 81], [10, 20, 30, 40, 50, 60, 70, 80, 90]]
Cool! We are only counting one too low. That is because we were originally having a special case for the last number, and we simply removed that special case. As we can see now, there is actually no reason for the special case, so we can just incorporate the last number into our normal flow of the code:
(1..n).map do |element|
(1..n).map { |item| element * item }
end
Congratulations! The first of the two tests passes! We only have the second test to worry about now, which gives this error:
expected: [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [2, 4, 6, 8, 10, 12, 14, 16, 18, 20], [3, 6, 9, 12, 15, 18, 21, 24, 27, 30]]
got: [[1, 2, 3], [2, 4, 6], [3, 6, 9]]
Oh, I see what is going on here. In the first test, the multiplication table just happened to be square, so we implemented a square multiplication table. But actually, the rows are always supposed to be 10 columns wide, regardless of n, whereas we just assumed they were n wide because in the first test, n happened to be 10.
But that is easy to fix:
(1..n).map do |element|
(1..10).map { |item| element * item }
end
And now all of the tests pass!
You will notice that we get almost no output now. That is a standard programming philosophy: if everything is going well, do not print anything. Only print something when there is something wrong. If you print too much when things are going right, it will be hard to spot when something is going wrong. Also, you de-sensitize your users and they just start ignoring what is printed, because it isn't interesting to them.
That's why RSpec by default only prints a single green dot for every test that passes, and a single line summary at the end, and only prints large messages when things are going wrong:
..
Finished in 0.00701 seconds (files took 0.15231 seconds to load)
2 examples, 0 failures
There are, of course, many other ways to write this. What I wanted to show you, is how you could fix what you already had written, by doing nothing but blindly following RSpec's excellent error messages, and simply doing tiny little steps to change the error message into something different. You are not even trying to fix the error, you are only trying to change the error to something that is closer to your goal.
We didn't really have to think hard. The errors were pretty much telling us what to do. That is the hallmark of good error messages and good tests.
You can even do this when you have no code at all! There is a methodology called "Test-Driven Development" (TDD), which is centered around the idea that you write tests before you ever write the first line of code, and then develop your code by simply "listening to the errors".
In this case, it could look a little like this. We start with an empty file, and we get this error:
NameError:
uninitialized constant ArrayUtils
Remember, we are only trying to do the "simplest possible thing to change the error message". The error message says that the ArrayUtils constant is not initialized, so we just initialize it, nothing more:
ArrayUtils = nil
NoMethodError:
undefined method `tabuada' for nil:NilClass
Okay, now it is telling us that NilClass doesn't have a method called tabuada. All we do is the stupidest thing to change the error message: we add that method to NilClass:
class NilClass
def tabuada
end
end
Is this stupid? Yes, of course it is! But we're not trying to be clever here. We are, in fact, very much trying to be not clever. Debugging code is hard, harder than to write code. Which means that if you make your code as clever as you can, you are by definition not clever enough to debug it! So, let's stick with this for now.
ArgumentError:
wrong number of arguments (given 1, expected 0)
Okay, so let's just give it one:
def tabuada(_)
end
Now, we no longer get errors from Ruby but instead test failures from RSpec. That is already a good step:
expected: [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [2, 4, 6, 8, 10, 12, 14, 16, 18, 20], [3, 6, 9, 12, 15, 18, 21, 24,...56, 64, 72, 80], [9, 18, 27, 36, 45, 54, 63, 72, 81, 90], [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]]
got: nil
So, we're returning the wrong thing. We can easily fix that by returning what the test asks us to:
def tabuada(_)
[
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20],
[3, 6, 9, 12, 15, 18, 21, 24, 27, 30],
[4, 8, 12, 16, 20, 24, 28, 32, 36, 40],
[5, 10, 15, 20, 25, 30, 35, 40, 45, 50],
[6, 12, 18, 24, 30, 36, 42, 48, 54, 60],
[7, 14, 21, 28, 35, 42, 49, 56, 63, 70],
[8, 16, 24, 32, 40, 48, 56, 64, 72, 80],
[9, 18, 27, 36, 45, 54, 63, 72, 81, 90],
[10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
]
end
This passes the first test, obviously, but not the second. Now, we could add a conditional expression like this:
def tabuada(_)
if _ == 10
[
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20],
[3, 6, 9, 12, 15, 18, 21, 24, 27, 30],
[4, 8, 12, 16, 20, 24, 28, 32, 36, 40],
[5, 10, 15, 20, 25, 30, 35, 40, 45, 50],
[6, 12, 18, 24, 30, 36, 42, 48, 54, 60],
[7, 14, 21, 28, 35, 42, 49, 56, 63, 70],
[8, 16, 24, 32, 40, 48, 56, 64, 72, 80],
[9, 18, 27, 36, 45, 54, 63, 72, 81, 90],
[10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
]
else
[
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20],
[3, 6, 9, 12, 15, 18, 21, 24, 27, 30]
]
end
end
However, it should be clear that this is not the spirit of the problem. If there were more tests, we would have to add a new clause for every number. In particular, this should be working for infinitely many numbers.
So, we'll have rethink our approach. Let's go back. All we need is an array, so let's start with an array:
def tabuada(_)
[]
end
As a first step, our array needs to have as many rows as the argument. If you want to create an array with a certain number of elements, you can use Array::new:
def tabuada(_)
Array.new(_)
end
got: [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil]
The length is good. But the contents are not. The contents should also be an array:
def tabuada(_)
Array.new(_) { Array.new(10) }
end
got: [[nil, nil, nil, nil, nil, nil, nil, nil, nil, nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil, ni...l, nil, nil, nil, nil, nil, nil, nil, nil, nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil]]
That's great, the result array already has the shape we want, now it's just missing the content. Array::new yields the index to the block, so we can use that to create the content:
def tabuada(_)
Array.new(_) { |a| Array.new(10) { |b| a * b }}
end
got: [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [0, 2, 4, 6, 8, 10, 12, 14, 16, 18],...35, 42, 49, 56, 63], [0, 8, 16, 24, 32, 40, 48, 56, 64, 72], [0, 9, 18, 27, 36, 45, 54, 63, 72, 81]]
We are very close now! As I said above, Array::new yields the index, but Ruby array indices range from 0 to size-1, not 1 to size, so all that is left to do is add 1 to the index, or alternatively, use the successor:
def tabuada(_)
Array.new(_) { |a| Array.new(10) { |b| a.succ * b.succ }}
end
And now, all of our tests pass!
Now that we have passing tests comes a very important step: Refactoring. Refactoring means:
While the tests are passing,
in small, well-defined, reversible steps,
change the structure of the code without changing its behavior
to make it look like you had the whole design figured out from the beginning.
Our first refactoring is going to be the Rename Parameter Refactoring. When we introduced the parameter, we were ignoring it, so we gave it the standard name for an ignored parameter: _. But that is a terrible name, so we will rename it to n:
def tabuada(n)
Array.new(n) { |a| Array.new(10) { |b| a.succ * b.succ }}
end
And we run our tests again to make sure that we haven't broken anything.
Our next refactoring is going to be the Move Method Refactoring. We very stupidly followed the error message telling us that it couldn't find the method tabuada on nil, so we did the simplest possible thing and added tabuada to nil. But, that doesn't make much sense.
Instead, we want to add it to ArrayUtils, which at that point just happened to be nil.
Which means that first, we need to change ArrayUtils to something else. Most Ruby programmers would probably use a module, but for something that is not going to be mixed and only used as a container for singleton methods, I personally actually prefer an empty BasicObject. So, let's do that and then move the method to it:
ArrayUtils = BasicObject.new
def ArrayUtils.tabuada(n)
Array.new(n) {|a| Array.new(10) {|b| a.succ * b.succ } }
end
We can slightly shorten this to something like this:
def (ArrayUtils = BasicObject.new).tabuada(n)
Array.new(n) {|a| Array.new(10) {|b| a.succ * b.succ } }
end
Note that in both cases, in the first example where we started from your failing code and fixed it, and in the second example where we started from an empty file, we always worked in small simple steps:
Read the error message.
Understand the error.
Make the simples, smallest, stupidest change to the code to change just one aspect of the error message.
Read the new error message.
Understand the error.
Check that the new error is a step forward or at least sideward.
If not, revert the change and try something different.
Else, go to #3 and repeat until the tests pass.
Only when the tests pass: refactor to make it look as if we had known where we are going from the beginning.
The tests ensure that we are always moving forward. The small steps ensure that we always understand what we are doing, and when things go wrong, we know the problem can only be in the tiny piece of code we changed. They also ensure that when we have to revert, we are only losing a couple seconds worth of work.