This post originated from an RSS feed registered with Ruby Buzz
by Eric Hodel.
Original Post: Dynamic String#sprintf width
Feed Title: Segment7
Feed URL: http://blog.segment7.net/articles.rss
Feed Description: Posts about and around Ruby, MetaRuby, ruby2c, ZenTest and work at The Robot Co-op.
When outputting columns of data it's nice to have them all line up prettily. String#sprintf (I prefer it's alias String#%) is the best tool for this!
Here's a list of pets that we'd like to output prettily:
data = [
['cats', 5],
['dogs', 10],
['giraffes', 1_000],
]
We'd like the output to look much like this nicely-formatted Array. Here's a basic sprintf format string that will print this out:
puts "pet amount"
data.each do |record|
puts "%s %d" % record
end
Which, when run, looks like:
pet amount
cats 5
dogs 10
giraffes 1000
Not very pretty ☹. Fortunately sprintf knows about field widths. We can add some to the format string and space out our header a bit while we're at it:
puts "pet amount"
data.each do |record|
puts "%8s %4d" % record
end
So we now have:
pet amount
cats 5
dogs 10
giraffes 1000
The names would be a bit easier to read if the were flush-left. A negative width will take care of that:
puts "pet amount"
data.each do |record|
puts "%-8s %4d" % record
end
Now, let's buy another pet!
data << ['crocodiles', 10_001]
Not so nice, we've come out of alignment ☹.
pet amount
cats 5
dogs 10
giraffes 1000
crocodiles 10001
I know what you're thinking now! Let's calculate the width of each field and add it to the format string! Ok:
max_name_length = data.map { |n,| n.length }.max
max_count_length = data.map { |_,c| c }.max.to_s.length
puts "%-#{max_name_length}s %s" % ['Pet name', 'Amount']
data.each do |record|
puts "%-#{max_name_length}s %#{max_count_length}d" % record
end
While I was at it I made the header line up better too. The output is much better:
Pet name Amount
cats 5
dogs 10
giraffes 1000
crocodiles 10001
The format strings are a bit ugly now. The interpolation inside the format string is unreadable. Fortunately sprintf also supports retrieving widths from the data. The * flag for a field has sprintf use the next argument as the field width:
This is a bit confusing to read though. A record for the above format string would look like ['cats', 5, 10, 5] but the values are accessed from the record in the opposite order from relative positions.
With relative positions first the field width is accessed then the data to format. With absolute positions first the data to format is accessed then the field width.
The downside of the [digit]$ flag is that every argument must be absolutely specified. Adding a relatively positioned field to such a format string results in the error in `%': numbered(1) after unnumbered(1) (ArgumentError) or in `%': unnumbered(1) mixed with numbered (ArgumentError)