Formatting tables in the terminal
Widths from the data
A table is three problems: how wide each column is, how to pad a cell, and where the borders go. The first one is a pass over the rows, so the widths do not depend on a literal nobody updates when a name gets longer:
rows = [["Name", "Count"], ["Ada", 3], ["Bob", 12]]
widths = [0, 0]
rows.each { |row| row.each_with_index { |cell, i| widths[i] = [widths[i], cell.to_s.length].max } }
ljust, rjust and center take that width and an optional fill character, so the padding is one call per cell. A number column is right aligned and a text column is left aligned, which is the whole visual grammar of a table.
The bordered table
Build the border from the widths, print it, the rows, and the border again. map plus join is the line:
border = "+" + widths.map { |w| "-" * (w + 2) }.join("+") + "+"
puts border
rows.each do |row|
puts "| " + row.each_with_index.map { |cell, i| cell.to_s.ljust(widths[i]) }.join(" | ") + " |"
end
puts border
ruby table.rb
+------+-------+ | Name | Count | | Ada | 3 | | Bob | 12 | +------+-------+
The two spaces around a cell are the "| " and " | " in the join, which is why the border adds two to each width. The table is plain text on purpose: it survives a terminal, a log file and a diff without a dependency.
The same rows as CSV
The same array is the input for a machine-readable version. CSV escapes the separators and the quotes, which string concatenation does not. The csv library is installed later, in lesson 132; the command below assumes it.
ruby -e 'require "csv"; rows = [["Name", "Count"], ["Ada", 3]]; print CSV.generate { |c| rows.each { |r| c << r } }'
Name,Count Ada,3
One script can print both: the table to the terminal and the CSV to a file, chosen by an option. Printing the table and calling it data is the mistake; a caller that parses columns by position breaks on the first cell with a space in it.
Only columns and a format
For two columns, a format string is shorter than a width calculation. it is the implicit block parameter, and the splat passes the two cells to the format:
ruby -e 'rows = [["Ada", 3], ["Bob", 12]]; rows.each { puts format("%-10s %3d", *it) }'
Ada 3 Bob 12
Fixed widths are fine when the values are known to be short: a name under ten characters, a count under a thousand. A name that grows past the width pushes the column instead of breaking, which is the failure mode to accept knowingly.
What to watch for
length counts characters and a terminal measures
display columns. A wide character or an emoji makes the border shorter
than the text; the fix is a width library, not a wider guess.
A table written to a file and then read back is data only by accident.
If another program reads it, write CSV or JSON; keep
the bordered table
for a person.
Next: Encodings
Article author: Arthur Isaev