In Progress
Unit 1, Lesson 1
In Progress

String Justify

Video transcript & code

When it comes to outputting text to the console, a little bit of extra formatting can make the difference between information and noise. Today I thought we'd talk about some of Ruby's built-in tools for laying out tabular data.

In episode #349, we modified the formatting of a Logger object to make the log lines simple and clean. Instead of all the usual fields, we just have the severity level and the message.

require "logger"
logger = Logger.new($stderr)
logger.formatter = ->(severity, datetime, progname, message) {
  "#{severity} #{message}\n"
}

logger.info "Ready for launch"
logger.error "Launch clamp malfunction"
logger.info "Resetting launch clamp sensor"
logger.info "Igniting engines"
logger.error "Launch clamps still engaged"
logger.fatal "Aborting launch"

# !> INFO Ready for launch
# !> ERROR Launch clamp malfunction
# !> INFO Resetting launch clamp sensor
# !> INFO Igniting engines
# !> ERROR Launch clamps still engaged
# !> FATAL Aborting launch

Unfortunately, the resulting output isn't quite as attractive as we'd intended. Instead of neatly lined-up columns, the log messages have inconsistent indentation because some severity names are longer than others.

Now, one way to do tackle this issue is with string formats. As we saw in episodes #194 and #195, with string formats we can achieve virtually any tabular layout we could possibly dream up. In this case, we can set up a format that puts the severity in a left-aligned 5-character field, and then appends the message after that field.

require "logger"
logger = Logger.new($stderr)
logger.formatter = ->(severity, datetime, progname, message) {
  "%-5s %s\n" % [severity, message]
}

logger.info "Ready for launch"
logger.error "Launch clamp malfunction"
logger.info "Resetting launch clamp sensor"
logger.info "Igniting engines"
logger.error "Launch clamps still engaged"
logger.fatal "Aborting launch"

# !> INFO  Ready for launch
# !> ERROR Launch clamp malfunction
# !> INFO  Resetting launch clamp sensor
# !> INFO  Igniting engines
# !> ERROR Launch clamps still engaged
# !> FATAL Aborting launch

This works. But let's be honest: every time we decide to use string formats, we wind up having to go look up the specifier syntax all over again. For simple problems like this, it would be nice if there was a way to do it that was easier to remember.

Fortunately, there is. Let's revert back to our original code. Then, rather than substituting a string format, we'll just use the #ljust method. This method name is short for "left justify", and we give it an integer representing the size of the field the text should be justified within.

When we execute this code, we get nicely aligned columns.

Of course, there's more than just #ljust. If we wanted to right-align the severity field up against the messages, we could do it with #rjust.

require "logger"
logger = Logger.new($stderr)
logger.formatter = ->(severity, datetime, progname, message) {
  "#{severity.to_s.rjust(5)} #{message}\n"
}

logger.info "Ready for launch"
logger.error "Launch clamp malfunction"
logger.info "Resetting launch clamp sensor"
logger.info "Igniting engines"
logger.error "Launch clamps still engaged"
logger.fatal "Aborting launch"

# !>  INFO Ready for launch
# !> ERROR Launch clamp malfunction
# !>  INFO Resetting launch clamp sensor
# !>  INFO Igniting engines
# !> ERROR Launch clamps still engaged
# !> FATAL Aborting launch

There's also a method for centering text in a field. Let's just use all these methods together and see what we can achieve.

We have a train schedule that we want to print in an attractive tabular form. To do this, we start by calculating the maximum field width of each column. We map over the string length of each field value, and then take the maximum from that list.

Then we print out a header line, with each column header string centered within its field, and a space in between them.

Then we print out a dividing line of dashes.

Finally, we loop through the train schedule. For each train, we print the name justified to the left, a space, and then the arrival time justified to the right.

schedule = [
  {name: "Northeast Zephyr", time: "8:36 AM"},
  {name: "Western Cannonball", time: "12:18 PM"},
  {name: "Southern Greased Pig", time: "4:27 PM"},
]

name_width = schedule.map{ |t| t[:name].size }.max
time_width = schedule.map{ |t| t[:time].size }.max

puts "Name".center(name_width) + " " + "Time".center(time_width)
puts "-" * (name_width + time_width + 1)
schedule.each do |train|
  puts train[:name].ljust(name_width) + " " + train[:time].rjust(time_width)
end

# >>         Name           Time
# >> -----------------------------
# >> Northeast Zephyr      8:36 AM
# >> Western Cannonball   12:18 PM
# >> Southern Greased Pig  4:27 PM

The result is a nice readable table.

We don't have to use spaces as the fill-in character. If we wanted, we could replace the spaces with dots, using a second argument passed to the justification methods.

schedule = [
  {name: "Northeast Zephyr", time: "8:36 AM"},
  {name: "Western Cannonball", time: "12:18 PM"},
  {name: "Southern Greased Pig", time: "4:27 PM"},
]

name_width = schedule.map{ |t| t[:name].size }.max
time_width = schedule.map{ |t| t[:time].size }.max

puts "Name".center(name_width) + " " + "Time".center(time_width)
puts "-" * (name_width + time_width + 1)
schedule.each do |train|
  puts train[:name].ljust(name_width, ".") +
       "." +
       train[:time].rjust(time_width, ".")
end

# >>         Name           Time
# >> -----------------------------
# >> Northeast Zephyr......8:36 AM
# >> Western Cannonball...12:18 PM
# >> Southern Greased Pig..4:27 PM

The code to achieve this table isn't as concise as the equivalent string-format version would have been. But we didn't have to remember any cryptic format specifiers. If we're just laying out a few strings and not worried about numeric formatting, this is probably an easier way to go about it.

Happy hacking!

Responses