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 in a file called string_decorator_test.rb
:
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 =
symbols to put above and below the text is very error prone. It can make future maintainer's life worse. Just to think that the future maintainer can be you, it should trigger a desire to find a better way to write that.
Here Documents
Here we're using a "squiggly heredoc" so we can have indented content between the opening identifier (<<~BANNER
) and the closing one (BANNER
)
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 string_decorator.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
print, in a line, the
=
character the same amount of times as the length of the given stringprint the given string in a new line
print again the same amount of
=
characters in a new line
String length
Let's experiment in irb
:
Great, that's exactly what we need!
Moving on...
Loops and Blocks
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:
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. So, let's stick with the <<
operator.
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
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 code is working as expected, and then we're done with this feature.
Source Control
A bug report arrives
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 go 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.
A system must be continually adapted or it becomes progressively less satisfactory.
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
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
:
That looks promising! 🙂
Longest line
Let's use what we've learned to check the longest line. Going back to our string_decorator.rb
:
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:
Great, that's what we want.
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.
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
We learned a good amount of things for such a simple functionality, didn't we? Let's recap!
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
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