Improved currency conversion card

Well, that wasn’t so hard. Earlier today, I mentioned that I thought my currency conversion program would be more useful to more people if it:

  1. were written in a more conventional language than PostScript, and
  2. generated a PDF file directly, rather than relying on another program to do the conversion later.

Since I had already worked out the logic in the PostScript program, and since the commands in the PDF::Writer module for Ruby are direct analogs of many PostScript commands, porting the program from PostScript to Ruby wasn’t very hard at all. And I was able to generalize a good portion of it, making it easier to modify if you want to convert between currencies other than dollars and euros.

I call it conversion-scales.rb, and it creates a PDF file that can be printed out on an index card. The PDF has two scales, one for converting between two currencies, and one for converting between Celsius and Fahrenheit. The output looks like this:

Here’s the source code:

#!/usr/bin/env ruby

# This program creates a PDF file for printing on a 3x5 index card
# in portrait orientation. The card contains two conversion scales:
# in the right half of the card is the temperature conversion
# between Celsius and Fahrenheit; in the left half of the card is a
# currency conversion.
# 
# The temperature scale ranges from -25 C to 45 C on the left side
# of the axis and from about -13 F to about 113 F on the right
# side.
# 
# The currency scale is more complicated because of its generality.
# Any currencies can be used; they are refered to as "alpha" and
# "beta" in the source code. The exchange rate is kept in a global
# variable, $exchange, that is defined near the top of the source
# code.
# 
# The left side of the currency scale runs from 0 alpha to 100
# alpha; the scale has minor hash marks at every unit value, medium
# hash marks every 5 units, and major hash marks every 10 units.
# The major hash marks are labeled. The right side of the currency
# scale runs from 0 beta to the largest beta that's worth no more
# than 100 alpha. The spacing of the major, medium, and minor hash
# marks are kept in the global variables $beta_maj, $beta_med, and
# $beta_min, which are defined near the top of the source code.
# 
# It's probably best to define alpha and beta such that the
# exchange rate is greater than 1--that way, the scale will have at
# least two orders of magnitude for each currency. The alpha scale
# gives you two digits of precision; a good choice of global
# variables will give you similar precision on the beta scale.

# Here are the global variables that control the exchange rate and
# the spacing of the hash marks.

$exchange = 1.3319  # 1 alpha is equal to $exchange beta
$beta_maj = 10      # large increment of beta
$beta_med = 5       # medium increment of beta
$beta_min = 1       # small increment of beta

# Casual users won't need to change anything below this line.

# The required module is available as a Ruby gem.
require "pdf/writer"

# Add a series of methods to the Numeric class to convert from
# various units to PostScript points.
class Numeric
  def inch
    return PDF::Writer.in2pts(self)
  end

  def alpha   # alpha is the unit of currency left of the axis
    return self.inch*4.5/100  # 100 alphas will be 4.5 inches long
  end

  def beta    # beta is the unit of currency right of the axis
    return (self/$exchange).alpha # alpha to beta
  end

  def degC
    return self.inch*4.5/70   # 70 degrees C will be 4.5 inches long
  end

  def degF
    return ((self - 32)/1.8).degC  # Fahrenheit to Celsius
  end

end

# Create the PDF and set document-wide parameters
pdf = PDF::Writer.new(:paper => 'letter')
thick = PDF::Writer::StrokeStyle.new(1)
thin = PDF::Writer::StrokeStyle.new(0.5)
pdf.select_font("Helvetica")

########## Left half has currency conversion ##########

# Center the axis in the left half.
pdf.save_state
pdf.translate_axis(3.50.inch, 6.25.inch)
pdf.stroke_style(thick)

# Draw the axis.
pdf.line(0, 0, 0, 100.alpha)
pdf.stroke

# The left side of the axis is marked in increments
# of alpha from # 0 to 100.

# 10-alpha hashmarks and labels
0.step(100,10) do |amt|
  y = amt.alpha
  label = amt.to_s
  pdf.line(0, y, -18, y)
  pdf.stroke
  pdf.add_text_wrap(-52, y-2.5, 30, label, 9, :right)
end
# 5-alpha hashmarks
5.step(95,10) do |amt|
  y = amt.alpha
  pdf.line(0, y, -14, y)
  pdf.stroke
end
# 1-alpha hashmarks
pdf.stroke_style(thin)
1.upto(99) do |amt|
  y = amt.alpha
  pdf.line(0, y, -10, y)
  pdf.stroke
end

# The right side of the axis is marked in increments of beta. The top
# hash positions depend hash increments and the exchange rate between
# alpha and beta.
top_maj = (100*$exchange/$beta_maj).to_i*$beta_maj
top_med = (100*$exchange/$beta_med).to_i*$beta_med
top_min = (100*$exchange/$beta_min).to_i*$beta_min

# major beta hashmarks and labels
pdf.stroke_style(thick)
0.step(top_maj, $beta_maj) do |amt|
  y = amt.beta
  label = amt.to_s
  pdf.line(0, y, 18, y)
  pdf.stroke
  pdf.add_text(21, y-2.5, label, 9)
end
# medium beta hashmarks
$beta_med.step(top_med, $beta_med) do |amt|
  y = amt.beta
  pdf.line(0, y, 14, y)
  pdf.stroke
end
# minor beta hashmarks
pdf.stroke_style(thin)
$beta_min.step(top_min, $beta_min) do |amt|
  y = amt.beta
  pdf.line(0, y, 10, y)
  pdf.stroke
end

# Undo the coordinate translation.
pdf.restore_state

########## Right half has temperature conversion ##########

# Center the axis in the left half.
pdf.save_state
pdf.translate_axis(5.00.inch, 6.25.inch)
pdf.stroke_style(thick)

# Shift the Celsius origin up 25 degrees.
pdf.translate_axis(0, 25.degC)

# Draw the axis.
pdf.line(0, -25.degC, 0, 45.degC)
pdf.stroke

# 10-degC hashmarks and labels
-20.step(40,10) do |amt|
  y = amt.degC
  label = amt.to_s
  pdf.line(0, y, -18, y)
  pdf.stroke
  pdf.add_text_wrap(-52, y-2.5, 30, label, 9, :right)
end
# 5-degC hashmarks
-25.step(45,10) do |amt|
  y = amt.degC
  pdf.line(0, y, -14, y)
  pdf.stroke
end
# 1-degC hashmarks
pdf.stroke_style(thin)
-24.upto(44) do |amt|
  y = amt.degC
  pdf.line(0, y, -10, y)
  pdf.stroke
end
# 10-degF hashmarks and labels
pdf.stroke_style(thick)
-10.step(110,10) do |amt|
  y = amt.degF
  label = amt.to_s
  pdf.line(0, y, 18, y)
  pdf.stroke
  pdf.add_text_wrap(20, y-2.5, 16, label, 9, :right)
end
# 5-degF hashmarks
-5.step(105,10) do |amt|
  y = amt.degF
  pdf.line(0, y, 14, y)
  pdf.stroke
end
# 1-degF hashmarks
pdf.stroke_style(thin)
-13.upto(113) do |amt|
  y = amt.degF
  pdf.line(0, y, 10, y)
  pdf.stroke
end

# Undo the coordinate translation.
pdf.restore_state

# Save the PDF to a file.
File.open("conversion-scales.pdf", "wb") { |f| f.write pdf.render }

It’s about 30 lines longer than the PostScript version, mainly because of the very long comment at the beginning (which I encourage you to read, as it explains how to customize the program for different exchange rates). There are twelve loops in the program for drawing the various hash marks, and I could have made the program distinctly shorter if I had created (as I did in the PostScript version) a hash-drawing function that would have turned each of those loops into a single line. But Ruby loops are so much easier to write—and read—than PostScript loops that I didn’t feel compelled to factor out the common code. (I’m not a programmer by trade, but I’m guessing that “factoring is the cure for the common code” must be a standard joke in the CS world.)

One of the things I like most about Ruby is that it allows you to add methods to the standard classes. In this program, I added several methods to the Numeric class that made it easy to scale the currencies and temperatures to PostScript points. So, for example, when I need to draw the temperature axis, I just say

pdf.line(0, -25.degC, 0, 45.degC)

and the second and fourth arguments get converted into points using a notation that seems pretty natural to me.

Beyond that, I don’t think there’s much to say about the program that isn’t said in the comments or the original post. Run the program from the command line, and you’ll find a new file in your working directory called conversion-scales.pdf. If you open the file in Preview or Acrobat Reader, you’ll see the scales are drawn at the top center of a letter-sized page. This may seem weird, but it turns out to be the most efficient layout for me. I just print the file with the Manual Feed option and stick an index card into the printer. Had I set the page size to 3 by 5 inches in the program, I’d have to use Page Setup… to change the paper size from its letter-sized default before issuing the print command—a extra step that gives me nothing in return.

My printers have guides in the manual feed tray that keep the index card centered:

If your printer feeds index cards through somewhere else—like along the left edge, for example—you’d have to change the two translate_axis commands to put the axes where your printer needs them.

Tags: