Loops, Blocks & Strings
When we want to do something repeatedly, we use loops. There are many ways to create loops in Ruby, in this chapter we're going to see a couple of them.
Text Banner
We're going to write a method that adds a decoration to a string to make it look like it's in a banner, like this:
Starting the banner
project:
Then let's start with the test.
Write the test first
Here's our first test:
Maybe you felt a little uncomfortable with that hard-to-read value assigned to expected
. Yeah, me too.
I think that mentally counting the amount of =
s to put above and below the text is very error prone. It can make future maintainer's life worse (keep in mind that the future maintainer can be you). Let's find a better way to write that.
Here Documents
A here document (aka heredoc) is a more comfortable way to read a multilinear block of text. In our case, as we want to check if the borders of the banner have the proper length in a quick glance, we would use a heredoc like this:
Here we're using a "squiggly heredoc" so we can have indented content between the opening identifier (<<~BANNER
) and the closing one (BANNER
)
Check the official documentation if you need more info about heredocs.
Write the minimal amount of code for the test to run
Running the test:
As expected, we got an error (not a failure).
Keep the discipline! You don't need to know anything new to make the test fail properly.
All you need to do right now is write enough code to make the test fail with no errors.
If you let the error messages drive the development you'll end up with a file named banner.rb
with this content:
Run the test and now it should fail with no errors.
Write enough code to make the test pass
In order to create the banner we need:
check how many characters there are in the given string
put the
=
character the same amount of times as the length of the given string, above and below the string
String length
For our first goal, we can check the String class documentation and look if it has a method to give us the length of a string. Turns out that such method is the String#length.
Let's experiment in irb
:
Great, that's exactly what we need!
Moving on...
Loops and Blocks
One of the simplest ways to create a loop in Ruby is by using the Integer#times method. Here's an example:
Run this code in irb
and you'll see that it prints Learn Ruby with TDD
5 times.
Although that code is easy to read, almost like natural language, the way it's written can be new for us, programmers.
In that code we're using the #times
method and also a block.
Blocks are frequently used in Ruby. It's like a way of bundling up a set of instructions for use elsewhere.
In our code above, the block starts with the keyword do
and ends with the end
. The block is being passed to the #times
method to be executed.
We're going to talk more about blocks, but for now that's enough.
Concatenating Strings
The simplistic way to concatenate strings is by using the +
plus sign, like this:
If we want to append content to a variable we could use the +=
operator, like this:
Although it's possible to append contents to a string variable with the +=
operator, the most usual Rubyist way to do it is by using the shovel operator: <<
The final result can look like the same, but there are subtle differences between +=
and <<
. The most notable one is that using <<
has a better performance, specially when used in a loop, which is our case. Let's stick with the <<
.
This StackOverflow Q&A has some good answers about the differences between
+=
and<<
. Including some code where you can prove the difference in performance.
First implementation
Let's recap what we've just learned:
Get the string length with
text.length
Repeat instructions in a loop
x
times bybundling up a set of instructions in a block
passing the block to
x.times
Append contents to a string variable with
var << 'more content'
Combining these techniques we can create a text banner function like this:
I've put some comments just to explain what we're doing now. In the refactor phase we're going to clean them up.
Maybe the only thing that require an extra explanation is the text.length.times
expression. We can use that because String#length
returns an Integer, and then we can use Integer#times
, which is what creates the loop
Let's run the test:
Great! A successful test triggers our brain to make that decision: refactor or commit?
Let's refactor!
Refactor
I'd like to use this refactoring session to talk about blocks again...
Bracket Blocks
When a block contains just a single instruction, it's usual to use {
brackets }
to delimiter the block, like this:
Run this 👆 in irb
and confirm.
With this new knowledge we can make our code shorter:
Run the test and you'll see that it's still working fine.
String multiplication
One interesting thing in String documentation is that we can use string * integer
to get a new string containing integer
copies of the original string
. Like this:
This is an example of how expressive the Ruby language can be.
With this knowledge we can refactor our banner function so it doesn't even need a loop:
Run the test to confirm this is working as expected, and then we're done with this feature.
Source Control
New requirements
Let's imagine we released our software to the world. This is when the "real work" begins: maintaining our software. We start to see our code being used in ways we didn't anticipate, and bugs start to appear.
A few days after publishing our code a user submitted a bug report saying that it doesn't work with multiline strings.
When we receive a bug report the very first thing to do is to reproduce it. Let's do it in irb
(be sure to be in the same directory as your code):
Indeed, that's not what we would expect from the banner code. We didn't envisaged this use case when we were developing. Such situation is one of the most common things in the day-to-day work of a professional developer, we must get used to it.
In fact this situation is so common that more than fifty years ago a computer scientist named Manny Lehman already noticed that and said:
A system must be continually adapted or it becomes progressively less satisfactory.
This is the first of the eight Lehman's laws of software evolution
Once we confirmed that the bug report is valid. Then let's fix it reproduce it in a test case.
Write the test first
We expect the borders to be as long as the longest line, then let's write a test for it:
Run the test to see the error/failure message:
Good thing that the test failed with no errors, but we still need to sort the failure.
The output shows exactly the difference between the expected and the actual borders, while the text in between remains the same.
Now that we have a good test, we can work on our fix.
Write enough code to make the test pass
If our border needs to be as long as the longest line in the string, we need a way to check each line length.
Iterating over each line of a String
In the String class page we can see a method called #each_line
.
When we call String#each_line
giving a block to it, it creates substrings that are the result of splitting the original string at each occurrence of a new line. The new thing for us here is that #each_line
needs a place (a variable) to where it puts the created substring. In this case we need to use a block with a parameter.
Block Parameters
When the instructions within our block need to reference the value they're currently working with, we specify a block parameter. Let's see an example in irb
:
In each iteration the substring is stored in the line
variable and we can use it however we want. In the example above we're just inspecting it with the Kernel#p
method.
That looks promising! 🙂
Longest line
Let's use what we've learned to check the longest line.
In the loop above we're checking if the current line length is longer than max_length
. If that's true, then we update the value of max_length
.
Running the test:
Whoops! Looks like there's an extra =
character in the border.
The reason for this is that String#each_line
creates substrings by splitting the original string at the occurrences of a new line, but it preserves the newline character.
We could see that in our irb
session:
So, in order to accurately get the length of a line we must ignore the trailing newline character. Fortunately we ha method for that: String#chomp.
This time you check by yourself on irb
. Try it with a string like "meleu\n"
.
Here's the new version of our code using #chomp
:
Run the tests and they should pass now.
Time for refactoring.
Refactor
I think our banner code is starting to accumulate too much logic in it. It should be a function that just puts borders above and below a given text.
Unfortunately real world is not that simple. We found a need to handle the case where the input text has multiple lines. Our banner
is handling this but its complexity increased. This situation reminds another one of the Lehman's laws:
As a system evolves, its complexity increases unless work is done to maintain or reduce it.
We need to proactively fight against this increasing complexity. In our case here we're going to achieve this by delegating the longest-line-detection logic to a separate function
Run the tests and they should pass.
For now I'm happy with this version. So, let's move on.
Source Control
Key Concepts
Ruby
Heredocs
Loops with
Integer#times
Blocks
with
do
/end
with
{
brackets}
with a parameter
String methods
concatenating with
<<
(aka shovel operator)string "multiplication" with
*
length
each_line
chomp
Testing
Reinforced TDD practices (write test first!)
After receiving a bug report:
first reproduce it in a test case
then start working on the fix
Software Engineering
The two first Lehman's laws of software evolution.
Continuing Change: A system must be continually adapted or it becomes progressively less satisfactory.
Increasing Complexity: As a system evolves, its complexity increases unless work is done to maintain or reduce it.
Last updated