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
```

## 1 comment:

Interesting post. Also, thanks for your answer on stackoverflow.

Post a Comment