I'm currently working on a project where I need to convert from things in one set of units to any other set of units ( eg centimeters to inches and so forth)
I had a bunch of small helper functions to convert from X to Y, but these kept growing every time we needed to handle something which hadn't been anticipated.
This kind of thing is also exponential, as if we have 4 'unit types' and we add a 5th one, we need to add 8 new methods to convert each other type to and from the new type
A few hours of refactoring later, I have this, which I think is kind of cool, and will enable me to delete dozens of small annoying meters_to_pts methods all over the place.
Disclaimer: This is definitely not good OO. A hash is not and never should be a unit converter. In the production code I will refactor this to build an actual Unit Converter class which stores a hash internally :-)
# Builds a unit converter object given the specified relationships
#
# converter = UnitConverter.create({
# # to convert FROM a TO B, multiply by C
# :pts => {:inches => 72},
# :inches => {:feet => 12},
# :cm => {:inches => 2.54,
# :meters => 100},
# :mm => {:cm => 10},
# })
#
# You can then do
#
# converter.convert(2, :feet, :inches)
# => 24
#
# The interesting part is, it will follow any links which can be inferred
# and also generate inverse relationships, so you can also (with the exact same hash) do
#
# converter.convert(2, :meters, :pts) # relationship inferred from meters => cm => inches => pts
# => 5669.29133858268
#
class UnitConverter < Hash
# Create a conversion hash, and populate with derivative and inverse conversions
def self.create( hsh )
returning new(hsh) do |h|
# build and merge the matching inverse conversions
h.recursive_merge! h.build_inverse_conversions
# build and merge implied conversions until we've merged them all
while (convs = h.build_implied_conversions) && convs.any?
h.recursive_merge!( convs )
end
end
end
# just create a simple conversion hash, don't build any implied or inverse conversions
def initialize( hsh )
merge!( hsh )
end
# Helper method which does self.inject but flattens the nested hashes so it yields with |memo, from, to, rate|
def inject_tuples(&block)
h = Hash.new{ |h, key| h[key] = {} }
self.inject(h) do |m, (from, x)|
x.each do |to, rate|
yield m, from, to, rate
end
m
end
end
# Builds any implied conversions and returns them in a new hash
# If no *new* conversions can be implied, will return an empty hash
# For example
# {:mm => {:cm => 10}, :cm => {:meters => 100}} implies {:mm => {:meters => 1000 }}
# so that will be returned
def build_implied_conversions
inject_tuples do |m, from, to, rate|
if link = self[to]
link.each do |link_to, link_rate|
# add the implied conversion to the 'to be added' list, unless it's already contained in +self+,
# or it's converting the same thing (inches to inches) which makes no sense
if (not self[from].include?(link_to)) and (from != link_to)
m[from][link_to] = rate * link_rate
end
end
end
m
end
end
# build inverse conversions
def build_inverse_conversions
inject_tuples do |m, from, to, rate|
m[to][from] = 1.0/rate
m
end
end
# do the actual conversion
def convert( value, from, to )
value * self[to][from]
end
end
I'm not sure if deriving it from Hash is the right way to go, but it basically is just a big hash full of all the inferred conversions, so I'll leave it at that.
Update
Woops, this code requires 'returning' which is part of rails' ActiveSupport, and an extension to the Hash class called recursive_merge!, which I found on an internet blog comment somewhere (so it's only fitting that I share back with this unitconverter)
Code for recursive_merge
class Hash
def recursive_merge(hsh)
self.merge(hsh) do |key, oldval, newval|
oldval.is_a?(Hash) ?
oldval.recursive_merge(newval) :
newval
end
end
def recursive_merge!(hsh)
self.merge!(hsh) do |key, oldval, newval|
oldval.is_a?(Hash) ?
oldval.recursive_merge!(newval) :
newval
end
end
end
Code for returning
class Object
def returning( x )
yield x
x
end
end