A Flexible Bannerizer Program Written in Ruby

This is originally intended to be a further exploration attempt in one of the exercise problems in Launch School. As of time of writing, I am currently in the first course of Launch School (Programming Foundations).

Since I wanted to test my knowledge, I took the challenge and wrote an original solution. And since Launch School encourages students to break down a code into smaller parts, I want to practice doing it by writing my first post here. But before we dive into the step-by-step analysis, let’s do a quick summary of what this program does.

This bannerizer program takes a string as input, and outputs it within a box. For example:

print_in_box('')
+--+
| |
| |
| |
+--+
print_in_box('The quick brown fox jumps over the lazy dog')
+---------------------------------------------+
| |
| The quick brown fox jumps over the lazy dog |
| |
+---------------------------------------------+

The examples above look simple and straightforward. Put simply, we can do the above by writing ten or less lines of code, without coming across any misalignment in the output. So far we only dealt with rather short text inputs, but what about a longer string that exceeds our console width? How do we wrap lines around so that they don’t overflow or cause any misalignment issues? Take a look at the example below:

MAX_WIDTH = 50
def print_in_box(input_string)
# code goes here...
end
print_in_box('This bannerizer program takes a string as input, and outputs it within a box.
It also takes into account if the input string contains multiple paragraphs separated by one or more line breaks.
As a feature of this program, we can also change the maximum width of our box as desired. We can do this by changing the MAX_WIDTH constant variable to our desired content width.')
# what will the above code output?

The above example demonstrates the edge cases that needs to be considered if:

  • We need to expand/ shrink our box width as desired
  • Our input string contains multiple paragraphs separated by one or more line breaks

So that we can output the following:

+----------------------------------------------------+
| |
| This bannerizer program takes a string as input, a |
| nd outputs it within a box. |
| |
| It also takes into account if the input string con |
| tains multiple paragraphs separated by one or more |
| line breaks. |
| |
| As a feature of this program, we can also change t |
| he maximum width of our box as desired. We can do |
| this by changing the MAX_WIDTH constant variable t |
| o our desired content width. |
| |
+----------------------------------------------------+

I intend to walk you through a step-by-step process addressing the above edge cases based on my current knowledge of understanding. The remaining portion of this article will explain my thought process and why I chose to write my code the way I wrote.

Even if I have spent much time writing this post, it may not be perfect. So feel free to comment below if you notice any errors, or need additional clarification on certain parts of the analysis.

Breaking Down into Parts

We will use a different example here. Let’s say our console is only 80 columns wide.

MAX_WIDTH = 76

First, we initialize the constant variable MAX_WIDTH. This will be the maximum character width of the text string at any given line. We set this to be 76 (we choose a total box width of 80 minus 2 characters of padding on each side)

We then create our main method print_in_box that takes in a text input as an argument. The text input can be a simple line of string or multiple paragraphs. We then initialize a method local variable content_width to determine the width of our box:

def print_in_box(input_string) 
content_width = [input_string.size, MAX_WIDTH].min
end

The content_width resizes based on the character size of the input string, up to a width of 76 characters.

Wrapping the Input Text

Before moving on to draw the box and output the string, we need to split the input text into multiple paragraphs, and subsequently into individual lines before formatting them into a wrapped-string output. We create a sub-method wrapped_output. This sub-method aims to return an array of paragraphs containing appropriate line breaks to fulfil our wrapping criteria.

def wrapped_output(input_string, content_width) 
paragraphs = input_string.split("\n")
end

We create an array paragraphs within this method. We want paragraphs to contain each paragraph as elements. We can do this by calling String#split on the input text specifying \n as the delimiter. To simply demonstrate this:

input_string = 'Sample first paragraph.  Sample last paragraph...' input_string.split("\n") 
# => ["Sample first paragraph.", "", "Sample last paragraph..."]

By calling String#split on a string with two paragraphs, we have 3 elements returned in the array: the first paragraph, an empty string and the last paragraph.

Next, we need to think about splitting each paragraph in paragraphs into individual lines and formatting the lines before returning our transformed paragraphs. We can achieve this by first calling Array#map on paragraphs:

def wrapped_output(input_string, content_width) 
paragraphs = input_string.split("\n")
  paragraphs.map do |paragraph| 
formatted_text = ''
number_of_lines = (paragraph.size / MAX_WIDTH) + 1
# splits each paragraph into multiple lines...
# adds "|"s to each line
# concatenates formatted line into formatted_text
    formatted_text
end
end

Each paragraph or empty string is passed into the map block and in turn assigned to the local variable paragraph. One way to transform each paragraph into individual lines of strings is to add \ns into the paragraph string whenever we need a line break. We can put the formatted paragraph into a new variable formatted_text before returning it as a block return value.

To determine how many lines we have in each paragraph, we use the formula (paragraph.size / MAX_WIDTH) + 1. This formula determines how many 76-character lines can occur in a single paragraph. It then adds 1 to account for the last line which, in many cases is less than 76 characters. For example, a 254-character paragraph will have 3 full lines and 1 additional line with spaces at the end.

Splitting Paragraphs into Multiple Formatted Lines

This is perhaps the trickiest part of the problem. Essentially we want to perform a line-wrap and adding ‘|’ on each end of each line. In our Array#map block we will add a loop specifying i as the counter.

paragraphs.map do |paragraph| 
formatted_text = ''
number_of_lines = (paragraph.size / MAX_WIDTH) + 1
  for i in (1..number_of_lines) 
current_line = paragraph[MAX_WIDTH * (i - 1), MAX_WIDTH]
formatted_text += "| " + current_line.ljust(content_width) + " |\n"
end
  formatted_text 
end

The trick here is to extract 76 characters at a time from each paragraph to form a full line. We can achieve this by using the formula paragraph[MAX_WIDTH * (i - 1), MAX_WIDTH].

Loop Breakdown

The question here is perhaps, how do we start with extracting the first 76 characters of each paragraph? And how can we continuously extract the next 76 characters at subsequent iterations?

The easiest approach here is perhaps calling String#slice on paragraph. At the first iteration, the first 76 characters of paragraph string is simply paragraph[0, 76], it means that we return 76 characters starting from index 0 (first character) of paragraph. An example table below shows how each line is being extracted:

We use an example paragraph string with 254 characters:

‘Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vulputate vestibulum nisi. Nam maximus hendrerit eros non mattis. Fusce a pretium elit. Nulla ullamcorper turpis orci, eu accumsan tellus euismod suscipit. Pellentesque convallis dolor dolor.’
Note: MAX_WIDTH = 76

Formatting the current_line is relatively simple, however, we need to consider when the current_line has less than 76 characters especially in the last iteration, or when the current_line is an empty string '' . If we add the lines "|" on both ends without considering this, the "|" at the right end will be misaligned.

current_line.ljust(content_width)

To fix this, we call String#ljust on current_line, passing in the content_width as the argument. That way, the last line will expand and match our designated maximum box width, while keeping its alignment to the left. Note: every current_line that has an empty string '' will be converted to a full line with 76-character spaces as its content.

'| '+ current_line.ljust(content_width) + ' |\n'

Lastly, we add the lines “|” to both ends of current_line while adding a newline character \n at the end. Each current_line is then concatenated to the string variable formatted_text

After the last iteration, formatted_text will be our output paragraph string. This will also be the block return value of map, which will be used by map to return a new array containing all of the formatted paragraph strings.

Our new paragraphs array’s structure will look like this:

[
"| First paragraph...\n...second line...\n...third line... |",
"| [76-character spacing] |",
"| Second paragraph...\n...second line...\n...third line... |",
"| [76-character spacing] |",
"| Third parapgraph...\n...last line.[spacing until line end] |"
]

Putting It All Together

Now our wrapped_output method returns an array of paragraphs and within it contains \ns and |s at designated line breaks. We will now write the remaining code to our main method:

def print_in_box(input_string)
content_width = [input_string.size, MAX_WIDTH].min
horizontal_rule = "+-#{'-' * content_width}-+"
top_bottom_padding = "| #{' ' * content_width} |"
  puts horizontal_rule
puts top_bottom_padding
puts wrapped_output(input_string, content_width)
puts top_bottom_padding
puts horizontal_rule
end

In our main method print_in_box, we 'draw' the horizontal lines and padding and call puts on them. We will also call puts instead of other printing methods to our wrapped_output transformed array. Since we have already took care of the formatting and wrapping issues, puts will correctly print out all the elements in our transformed paragraphs array.


Summary

Final inputs:

MAX_WIDTH = 76 
...
print_in_box('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vulputate vestibulum nisi. Nam maximus hendrerit eros non mattis. Fusce a pretium elit. Nulla ullamcorper turpis orci, eu accumsan tellus euismod suscipit. Pellentesque convallis dolor dolor.

Nam vitae lectus mauris. Duis posuere quis massa quis auctor. Phasellus porta fermentum lacus ac convallis. Nam id orci sit amet metus ornare sodales quis ac dolor.

Donec auctor commodo ligula, id luctus ligula egestas at. Donec euismod ut tellus non scelerisque. Nulla ut elit leo. Aliquam molestie in tortor ac congue. Fusce eget blandit velit.')

After splitting up and formatting our original string, we return a new, transformed paragraphs array containing individual formatted paragraphs as elements:

[
“| Lorem ipsum dolor sit amet, consectetur adipiscing |\n| elit. Donec vulputate vestibulum nisi. Nam maximu |\n| s hendrerit eros non mattis. Fusce a pretium elit. |\n| Nulla ullamcorper turpis orci, eu accumsan tellus |\n| euismod suscipit. Pellentesque convallis dolor do |\n| lor. |\n”,
“| |\n”,
“| Nam vitae lectus mauris. Duis posuere quis massa q |\n| uis auctor. Phasellus porta fermentum lacus ac con |\n| vallis. Nam id orci sit amet metus ornare sodales |\n| quis ac dolor. |\n”,
“| |\n”,
“| Donec auctor commodo ligula, id luctus ligula eges |\n| tas at. Donec euismod ut tellus non scelerisque. N |\n| ulla ut elit leo. Aliquam molestie in tortor ac co |\n| ngue. Fusce eget blandit velit. |\n”
]

And we can puts the array above after ‘drawing’ the box to yield our…

Final Output:

A bannerizer output limited to an 80-character wide box

I hope that this post will give an insight to people who just started out Ruby to articulate their code more confidently. Nevertheless, I feel that there is still room for improvement. Any critiques/comments/ suggestions are welcomed!


Originally published at gist.github.com.