String substitutions in ruby and coding style

Posted on

Problem

I wrote a short ruby script to generate various screenshots, and event though I am not completely new to ruby I rarely use it.

Full script (41 LOC): https://gist.github.com/1229115

The string substitutions irritate me. They don’t look quite right – much too ugly for a beautiful and concise language like ruby. Am I overdoing it? What alternatives are there?

Here is the lines I am concerned about (for context, please take a look at the gist):

# assemble file name
fname = "#{directory}/#{prefix}-#{timestamp}-#{commit}-#{size[:width]}x#{size[:height]}.png"

# create command line stuff
call = "#{macruby_exe} #{snapper} 
        --width #{size[:width]} --height #{size[:height]} #{url} #{fname}"

# now call it
system call

# just to show the progress (and the file sizes)
puts "#{fname} (#{File.size?(fname) / 1024}kB)"

Just to illustrate the script, the output usually looks like:

screenies/screenshot-1316535022-0b5c7967887481deb17bcd9039cd3fe0ac4540d4-640x960.png (95kB)
screenies/screenshot-1316535022-0b5c7967887481deb17bcd9039cd3fe0ac4540d4-800x480.png (62kB)
screenies/screenshot-1316535022-0b5c7967887481deb17bcd9039cd3fe0ac4540d4-800x600.png (78kB)
   ^          ^          ^                          ^                       ^
   |          |          |                          |               ________|______
   |          |          |                          |              |               |
directory   prefix     timestamp                  commit    size[:width] x size[:height]

Solution

There are other ways you could write this but I’m not sure if you’d find any of them cleaner. One option is plain old sprintf. For example:

# assemble file name
fname = sprintf("%s/%s-%i-%s-%ix%i.png", directory, prefix, timestamp,
          commit, size[:width], size[:height])

You could shorten it a little by giving size its own to_s method:

class << size; def to_s; "#{self[:width]}x#{self[:height]}" end end

fname = sprintf("%s/%s-%i-%s-%s.png", directory, prefix, timestamp,
          commit, size)

…and perhaps create other convenience methods along the same lines. But still, very small gains.

Another option would be to make your own super-small template engine, something like this:

def tiny_subst template, bndg
  param_exp   = /:(w+)[^w:]+/i
  tmpl_params = template.scan(param_exp).flatten

  proc {
    out_str = template.clone

    tmpl_params.reduce(out_str) do |str, p|
      str.gsub ":#{p}", eval(p, bndg).to_s unless p.empty?
    end
  }
end

tmpl   = ':directory/:prefix-:timestamp-:commit-:size.png'
subber = tiny_subst(tmpl, binding)

directory = 'screenies'
prefix    = 'screenshot'
timestamp = Time.now.to_i
commit    = '0b5c7967887481deb17bcd9039cd3fe0ac4540d4'
size      = '640x960'

subber.yield # => screenies/screenshot-1316535022-0b5c7967887481deb17bcd9039cd3fe0ac4540d4-640x960.png

commit  = 'foobarbazquux'
size    = '9000x9000'

subber.yield # => screenies/screenshot-1316535022-foobarbazquux-9000x9000.png

So, that’s kinda nice, but is it really worth it? Not for me, but I guess if you really hate “#{this}” it might be.

Per your request (and drawing from your gist), more concise and readable is:

screensizes = [
#     width height
    [   640, 960  ], # iPhone 4
    [   800, 480  ], # wvga
    [   800, 600  ], # old school
    [  1024, 600  ], # netbook, WSVGA
    [  1024, 786  ], # old school, iPad
    [  1200, 800  ], # macbook
    [  1366, 786  ], # netbook
    [  1440, 900  ], # macbook pro
    [  1600, 768  ], # strange
    [  1680, 1050 ], # hires macbok
]

screensizes.each do |w,h|
# Assemble file name:
  f = "#{directory}/#{prefix}-#{timestamp}-#{commit}-#{w}x#{h}.png"

# Create command line stuff:
  s = "#{macruby_exe} #{snapper} --width #{w} --height #{h} #{url} #{f}"

# Now call it:
  system s

# Show progress and file sizes:
  puts "#{f} (#{File.size?(f) / 1024}kB)"
end

Leave a Reply

Your email address will not be published. Required fields are marked *