This is a lot of code to demonstrate something simple. The module is
a convenient place to group defintions which work together. In this
case, to perform unit conversions. The next
file is a short driver which uses this module.
#
# This module provides unit conversion. The main public name is
# the method ratio, which gives the ratio of two dimensioned expressions,
# or throws an exception if they do not conform. The exceptions are also
# public, as are the BaseUnit and RelatedUnit classes, which can be used
# to add conversion information to the module.
#
# Units.ratio(from, to)
# Will tell you how many to you need to equal from. From and to are strings
# of the form
# [ n ] { [ / ] unitname[ ^r ] }+
# Where n is the number of whatever units, defaulting to 1.0. The unitname
# is a unit or alias created with BaseUnit or RelatedUnit. Unitnames may
# be separated by spaces or dashes. Power on the unit defaults to 1.
module Units
public
# Two new exceptions just for us to pitch.
class UnitsException < Exception
# Base class for exceptions invented for this module.
end
# A unit expression string couldn't be parsed.
class UnitParseError < UnitsException
def initialize(m = "Cannot parse measurement expression")
super(m)
end
end
# Conversion of units which are not conformable (feet to liters or like
# that).
class UnitConformability < UnitsException
def initialize(m = "Units not conformable")
super(m)
end
end
# Abstract base class for units.
class Unit
# A list of all the units we know.
@@units = { }
# Access to @@units.
def Unit.exists(n)
return @@units.has_key?(n)
end
def Unit.named(n)
return @@units[n]
end
# If this were Java, I'd define an abstract function isbase() which tells
# if this object is a BaseUnit or not.
def initialize(name)
@name = name
@@units[name] = self
@@units[name + 's'] = self
end
attr_reader :name
def alias(*names)
names.each { |n| @@units[n] = self }
end
end
# This is the base unit class. There is one base unit for each
# dimension. Any unit in the right dimension could be used.
# The class is just a name and a dimension name.
class BaseUnit < Unit
def isbase()
return true
end
def initialize(name, dname)
super(name)
@dimension = dname
end
attr_reader :dimension
# This orders the unit objects arbitrarily, but that is sufficient to
# sort them and compare lists.
def <=> (u)
return object_id <=> u.object_id
end
end
private
# Here are the base units for each dimension.
BaseUnit.new("meter", "length").alias("m", "metre")
BaseUnit.new("gram", "mass").alias("g")
BaseUnit.new("second", "time").alias("s", "sec")
# A measurement is a number and some numerator and denominator units.
# The unit lists are kept in lowest terms of base units, though the object
# may be initialized with any units.
class Measurement
def initialize(m, num, denom)
@mult = m.to_f # The multiplier. to_f in case you send an integer.
@num = num # The numerator units
@denom = denom # The denominator units.
normalize # Convert to lowest terms of base units.
end
attr_reader :mult, :num, :denom
# Return the ratio. Throws conformability.
def ratio(divby)
raise UnitConformability.new \
if @num != divby.num || @denom != divby.denom
return @mult / divby.mult
end
private
# Convert to base units only, in lowest terms.
def normalize
# Convert the lists to just base units.
newnum = []
newdenom = []
basify(@num, false, newnum, newdenom)
basify(@denom, true, newdenom, newnum)
# Now eliminate units which appear in both places. This depends on
# an arbitrary ordering of the base units which allows us to compare
# them in a merge order.
newnum.sort!
newdenom.sort!
@num = [ ]
@denom = [ ]
while newnum.length > 0 && newdenom.length > 0
rel = newnum[0] <=> newdenom[0]
if rel < 0
# They are different and the num comes first. Must be kept.
@num.push(newnum.shift)
elsif rel > 0
# They are different and the denom comes first. Must be kept.
@denom.push(newdenom.shift)
else
# A match. Eliminate.
newnum.shift
newdenom.shift
end
end
@num.concat(newnum)
@denom.concat(newdenom)
end
# Convert the source list to base, adding the components to ndest and
# ddest, and multiplying or dividing the measure's number. The
# src is a unit list. These may not be base units, but, if not, the
# measures they contain will be.
def basify(src, divide, ndest, ddest)
for unit in src
if unit.isbase
ndest.push(unit)
else
@mult = if divide then
@mult / unit.related.mult
else
@mult * unit.related.mult
end
ndest.concat(unit.related.num)
ddest.concat(unit.related.denom)
end
end
end
end
# This can be called as (qty, unitex) or just (unitex), where m is
# taken as 1.0. Units expression is name[^pwr] [ / name... ]
# unit names are always alpha. - is a separator like a space, but not
# between ^ and pwr.
def Units.qty(m, s=nil)
m,s = 1.0,m if s == nil
# See if there's a number at the front of the expression.
if s.sub!(/^\s*(\-?(\d+(\.\d*)?|\d*\.\d+))\s*/, '')
m *= $1.to_f
end
# Collect the stuff needed for the units part.
num = []
denom = []
# What's the next thing?
puthere = num
otherone = denom
while true
# Strip leading crud, which is spaces or dashes
s.sub!(/^(\s|\-)*/, '')
break if s == ''
# Find the next "thing", which is a unit name or a /.
s.sub!(%r=^([a-zA-Z]+|/)=, '') or
raise UnitParseError.new('Expected unit name or slash at "' + s + '"' )
thing = $1
if thing == '/'
# Swap which list the units go into.
puthere, otherone = otherone, puthere
else
# Unit name.
Unit.exists(thing) or
raise UnitParseError.new('Unknown unit name "' + thing + '"')
unit = Unit.named(thing)
# See if there's a ^n
ct = 1
top = true
if s.sub!(/^\s*\^\s*(\-?)(\d+)/, '')
ct = $2.to_i
top = false if $1 != ''
end
if top
ct.times { puthere.push(unit) }
else
ct.times { otherone.push(unit) }
end
end
end
return Measurement.new(m, num, denom)
end
public
# Return the ratio.
def Units.ratio(top, bot)
return Units.qty(top).ratio(Units.qty(bot))
end
# The units that are not basic
class RelatedUnit < Unit
def isbase()
return false
end
def initialize(name, measure)
super(name)
measure = Units.qty(measure) if measure.kind_of?(String)
@related = measure
end
attr_reader :related
end
private
# Here are all the rest of the units we know about.
RelatedUnit.new("kilometer", "1000 meter").alias("km")
RelatedUnit.new("centimeter", "0.01 meter").alias("cm")
RelatedUnit.new("milimeter", "0.01 meter").alias("mm")
RelatedUnit.new("inch", "2.54 cm").alias("in")
RelatedUnit.new("foot", "12 in").alias("ft", "feet")
RelatedUnit.new("mile", "5280 ft").alias("mi")
RelatedUnit.new("yard", "3 ft").alias("yd")
RelatedUnit.new("furlong", "660 ft")
RelatedUnit.new("milliliter", "cm^3").alias("ml", "cc")
RelatedUnit.new("liter", "1000 ml").alias("l")
RelatedUnit.new("gallon", "3.785412 liter").alias("gal")
RelatedUnit.new("quart", "0.25 gal").alias("qt")
RelatedUnit.new("pint", "0.5 quart").alias("pt")
RelatedUnit.new("cup", "0.25 quart")
RelatedUnit.new("acre", "43560 ft^2")
RelatedUnit.new("hectare", "10000 m^2")
RelatedUnit.new("minute", "60 sec").alias("min")
RelatedUnit.new("hour", "60 min").alias("hr")
RelatedUnit.new("day", "24 hr")
RelatedUnit.new("week", "7 day").alias("wk")
RelatedUnit.new("fortnight", "14 day")
RelatedUnit.new("year", "365.25 day").alias("yr")
RelatedUnit.new("kilogram", "1000 gram").alias("kg")
RelatedUnit.new("slug", "14.593903 kg")
RelatedUnit.new("newton", "kg-m/s^2").alias("N")
RelatedUnit.new("pound", "4.448222 N").alias("lb")
RelatedUnit.new("joule", "N-m").alias("J")
RelatedUnit.new("calorie", "0.238846 J").alias("cal")
RelatedUnit.new("kcal", "1000 cal")
RelatedUnit.new("BTU", "1055.055853 J")
RelatedUnit.new("watt", "J/s")
RelatedUnit.new("kilowatt", "1000 watt").alias("kw")
RelatedUnit.new("horsepower", "746 watt")
RelatedUnit.new("knot", "1.68781 ft/sec")
end