Ruby Units-System

Units of measure conversions for Ruby, using Ruby objects and Ruby syntax rather than text strings.

There are a number of Ruby units libraries, but I don't think they take this approach (I haven't done much research, though.)

There's a couple of caveats for using this module from Ruby 1.8:

  • To use unit names that start with uppercase letters, the UseBlocks module must be included in the scope (either global scope or a module or class definitions) where the expressions are goign to be used. Alternatively, expressions such as u{W} could be replaced by either u{W()}, u{self.W} (method invocation) or u{self::W} (qualified constant). Units defined in text form, such as u('W*h') will work regardless of whether UseBlocks is included or not.

  • UTF-8 characters are liberally used in identifiers, so the code must be executed with the -Ku option (or setting $KCODE='UTF8' before requiring this library.)

Usage examples

Ruby 1.9

For use with Ruby 1.9, this gem can be used simply by requiring:

require 'units-system'

For versions other than 1.9.1 this is needed as in Ruby 1.8:

include Units::UseBlocks  # allow access to capitalized unit names from units/u blocks

Ruby 1.8

This library has been designed for Ruby 1.9; when using it under older versions of Ruby there's a couple of precautions to be taken to use it (which can be used with Ruby 1.9 too):

$KCODE = 'UTF8'           # avoid errors when parsing the required library under Ruby 1.8
require 'units-system'
include Units::UseBlocks  # allow access to capitalized unit names from units/u blocks

Depending on you installation you may have to “require 'rubygems' first.

The following examples use UTF-8 code; so they can should be used with a “encoding: utf-8” comment at the top of the file for Ruby 1.9, and/or with the ruby command line “-Ku” option for Ruby 1.8.

To work with units a units block can be used. Beware: in it self is changed, so outer self methods or instance variables are not accessible, unless assigned to local variables.

require 'units-system'

Units.units do

  # In the units environment predefined variables are available for all units and they
  # can be combined arithmetically:
  x = 3*m/s
  puts x                           # => 3.0*m/s
  # Note that SI prefixes (k for kilo, etc.) can be used as part of the unit names:
  x += 17*km/h
  puts x                           # => 7.72222222222222*m/s
  puts x.to(km/h)                  # => 27.8*km/h
  puts x.magnitude                 # => 7.72222222222222

  # Let's use some unit powers: convert 3 cubic meters to litres:
  puts (3*m**3).to(l)              # => 3000.0*l

  # Now let's convert some imperial units to SI:
  puts (100*mi/h).to_si            # => 44.704*m/s

  # Note that +in+ is a Ruby keyword, so to use inches you must use +self.in+:
  puts (10*cm).to(self.in)         # => 3.93700787401575*in
  # ...or use the alternative nonstandard name +inch+
  puts (10*cm).to(inch)            # => 3.93700787401575*inch

  # Now let's use derived units, e.g. power units:
  x = 10*kW
  # show a verbose description of the measure:
  puts x.describe                  # => 10.0 kiloWatt
  # convert to base units
  puts x.base                      # => 10000.0*(m**2*kg)/s**3
  # a more natural notation can be used instead of the default Ruby syntax:
  puts x.base.abr                  # => 10000.0 (m^2 kg)/s^3

  # Note that unit names that start with uppercase letters are OK:
  # (but see the notes on UseBlocks above if this doesn't work)
  puts 11*W                        # => 11.0*W
  puts (2*Mg).to(kg)               # => 2000.0*kg

  # Let's use kilograms-force (kiloponds) (not a SI unit)
  x = 10*kgf
  puts x                           # => 10.0*kgf
  # conversion to SI units uses the SI unit of force the newton N (which is a derived unit)
  puts x.to_si                     # => 98.0665*N
  # conversion to base units substitutes derived units for base units
  puts x.base                      # => 98066.5*(g*m)/s**2
  # but g (gram) is not a base SI unit, to get SI base units we must:
  puts x.base.to_si                # => 98.0665*(kg*m)/s**2

  # And now, for some trigonometry fun! (note the use of unicode characters)
  x = 90*°
  puts x                           # => 90.0*°
  puts x.to(rad)                   # => 1.5707963267949*rad
  puts sin(x)                      # => 1.0

  puts sin(45*°+30*+10*)         # => 0.713284429355996

  puts asin(0.5)                   # => 0.523598775598299*rad
  puts asin(0.5).to(°)             # => 30.0*°
  puts asin(0.5).in(°)             # => 30.0

  puts atan2(10*cm, 0.1*m).to(°)   # => 45.0*°

  # Temperature conversions may be absolute (convert levels of temperature)
  # or relative (convert differences of temperature)
  # When a measure has a single unit of temperature, conversion is absolute:
  puts (20*°C).to(K)               # => 293.15*K
  puts (20*°C).to(°F)              # => 67.9999999999999*°F
  puts (20*mK).to(°C)              # => -273.13*°C
  # In other cases conversion is relative:
  puts (2*°C/h).to(K/h)            # => 2.0*K/h
  puts (2*°C/h).to(°F/h)           # => 3.6*°F/h
  # To force the relative conversion of a single temperature pass a second argument to to():
  puts (20*°C).to(K,:relative)     # => 20.0*K
  puts (20*°C).to(°F,:relative)    # => 36.0*°F
  puts (20*mK).to(°C,:relative)    # => 0.02*°C

end

For short expressions, the abbreviation Units.u can be used instead of Units.units

include Units
puts u{60*km + 10*mi}              # => 76.09344*km
puts u{sin(45*°)}                  # => 0.707106781186547
x = u{120*km/h}
puts x.to(u{mi/h})                 # => 74.5645430684801*mi/h

Text strings can also be used to define units:

puts Units.u('60*km + 10*mi')      # => 76.09344*km
puts Units.u('sin(45*°)')          # => 0.707106781186547
x = Units.u('120*km/h')
puts x.to(Units.u('mi/h'))         # => 74.5645430684801*mi/h

And also as the right operand of binary arithmetic operators:

puts Units.u('20*km')/'h'          # => 20.0*km/h

New units can be defined with Units.define

Units.define :kph, 1, Units.u{km/h}
puts Units.u{270*kph.to(m/s)}      # => 75.0*m/s

Constants

Constants could be define practically as units, but to avoid introducing too much noise in the units namespace, they can be defined separately with:

Units.constant :g, 'standard gravity', u{9.80665*m/s**2}

A constant can be used anywhere with the Units::Const prefix:

puts Units::Const.g                # => 9.80665*m/s**2
# gram-force:
puts u{g*Const.g}                  # => 9.80665*(g*m)/s**2

To avoid using the prefix, constants to be used unprefixed can be declared with a with_constants; Note in the first example, that by introducing a constant named g we're hiding the gram units and would not be able to use it in the block.

# kilopond:
puts Units.with_constants(:g){kg*g} # => 9.80665*(kg*m)/s**2
# 1 GeV mass
puts Units.with_constants(:c){1*GeV/c**2}.to(:kg)             # => 1.782661844855044e-27*kg
# Planck units
Units.with_constants :c, :G, :hbar do
  puts sqrt(hbar*G/c**3)                                      # => 1.6161992557033346e-35*m
  puts sqrt(hbar*c/G)                                         # => 2.176509252445312e-08*kg
  puts sqrt(hbar*G/c**5)                                      # => 5.391060423886096e-44*s
end

Caveat

Note that Ruby variable definition rules imply that this:

m = Units.u{m}

Results is a nil value (the outer m assignment defines a local m variable even before executing the block, so the m in the block refers to that, yet-unassigned, variable and not to the meter unit)

Note on Patches/Pull Requests

  • Fork the project.

  • Make your feature addition or bug fix.

  • Add tests for it. This is important so I don't break it in a future version unintentionally.

  • Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)

  • Send me a pull request. Bonus points for topic branches.

Copyright

Copyright © 2009 Javier Goizueta. See LICENSE for details.