Skip to content

Instantly share code, notes, and snippets.

@YingCGooi
Last active April 21, 2017 16:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save YingCGooi/c20e5282694e872b0765699e0a04e5ec to your computer and use it in GitHub Desktop.
Save YingCGooi/c20e5282694e872b0765699e0a04e5ec to your computer and use it in GitHub Desktop.

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. Since I wanted to test my knowledge, I took the challenge and wrote an original solution. I am also training my fluency in explaining a code line-by-line, which is encouraged by Launch School. Thus, I created a detailed analysis. Before we dive into the 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, a program that has ten or less lines of code can run the above strings without coming across any misalignments 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 = 76 # specifies the maximum content width in number of characters
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.')
+------------------------------------------------------------------------------+
|                                                                              |
| Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vulputate ves |
| tibulum nisi. Nam maximus hendrerit eros non mattis. Fusce a pretium elit. N |
| ulla ullamcorper turpis orci, eu accumsan tellus euismod suscipit. Pellentes |
| que convallis dolor dolor.                                                   |
|                                                                              |
| Nam vitae lectus mauris. Duis posuere quis massa quis auctor. Phasellus port |
| a fermentum lacus ac convallis. Nam id orci sit amet metus ornare sodales qu |
| is ac dolor.                                                                 |
|                                                                              |
| Donec auctor commodo ligula, id luctus ligula egestas at. Donec euismod ut t |
| ellus non scelerisque. Nulla ut elit leo. Aliquam molestie in tortor ac cong |
| ue. Fusce eget blandit velit.                                                |
|                                                                              |
+------------------------------------------------------------------------------+

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

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

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.

As 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

MAX_WIDTH = 76

First, we specify 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 (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 implementation:

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. The empty string will be used to create an entire new line for line break output.

Next, we need to think about transforming each paragraph in paragraphs into individual lines of strings. We can achieve this by calling Array#map on paragraphs:

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

  paragraphs.map do |paragraph|
    number_of_lines = (paragraph.size / MAX_WIDTH) + 1
    formatted_text = '' # initialize empty string
    # code that perform splitting of paragraphs into individual lines
    formatted_text # return formatted string
  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#[] (element reference) 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 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.'

Line Number(i) String Index Position Formula paragraph[MAX_WIDTH * (i - 1), MAX_WIDTH] current_line.size p current_line
1 0 to 75 paragraph[0, 76] 76 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vulputate ves'
2 76 to 151 paragraph[76, 76] 76 'tibulum nisi. Nam maximus hendrerit eros non mattis. Fusce a pretium elit. N'
3 152 to 227 paragraph[152, 76] 76 'ulla ullamcorper turpis orci, eu accumsan tellus euismod suscipit. Pellentes'
4 228 to 253 paragraph[228, 76] 26 'que 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.

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. An empty string will simply become an entire new line in our output. Note: every current_line that has an empty string '' will be converted to a full line with 76-character spaces as its content.

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

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

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 isseus, puts will correctly print out all the elements in our transformed paragraphs array.

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. The change to a MAX_WIDTH from 76 to 50 correctly outputs our box:

`MAX_WIDTH = 50`
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.')
+----------------------------------------------------+
|                                                    |
| Lorem ipsum dolor sit amet, consectetur adipiscing |
|  elit. Donec vulputate vestibulum nisi. Nam maximu |
| s hendrerit eros non mattis. Fusce a pretium elit. |
|  Nulla ullamcorper turpis orci, eu accumsan tellus |
|  euismod suscipit. Pellentesque convallis dolor do |
| lor.                                               |
|                                                    |
| Nam vitae lectus mauris. Duis posuere quis massa q |
| uis auctor. Phasellus porta fermentum lacus ac con |
| vallis. Nam id orci sit amet metus ornare sodales  |
| quis ac dolor.                                     |
|                                                    |
| Donec auctor commodo ligula, id luctus ligula eges |
| tas at. Donec euismod ut tellus non scelerisque. N |
| ulla ut elit leo. Aliquam molestie in tortor ac co |
| ngue. Fusce eget blandit velit.                    |
|                                                    |
+----------------------------------------------------+
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment