Last night I was sleepily trying to calculate the sustained transfer  
rate my web server would need to maintain to reach my quoted quota of  
300GB/month transfter. It sparked an idea, and this morning I played  
around with some inferential unit conversion code. I don't have the  
energy to finish it off (it's more than just polish), but thought I'd  
share it anyhow. I like the syntax it allows :)

Example code usage:

Units.add_conversion( 60.seconds, 1.minute )
Units.add_conversion( 1.mile_per_hour, 1.46666667.feet_per_second )
puts ( 32.feet_per_second_second *  
1.5.minutes.in_seconds ).in_miles_per_hour
#=> 1963.63635917355 miles/hour

distance = 3.feet
time = 1.second
rate = distance / time
puts rate, rate / 3
#=> 3 feet/second
#=> 1 foot/second

puts 18.camels * 12.days / 4.cows + 89.widget_jobbers
#=> ((54 camel*day/cow) + (89 jobber*widget))




Things it doesn't do that it should, IMO:
1) More robust pluralization/singularization of nouns

2) Accept scalar factors/divisors.

3) Automatically search for a path between two conversions.
(For example, if it knows how to convert from GB/s to MB/s and from  
MB/s to kB/s, it should be smart enough to know how to find the path  
from GB/s to kB/s.)

4) Extend Numeric operators to turn the tables around if the right  
operand is a Quantity or Expression.

5) All sorts of fun symbolic math with Expressions

6) Use conversions to flatten Expressions with convertible-units.
(For example, (1.hour + 30.minutes) should be able to be  
automatically converted into a single Quantity using only hours or  
minutes.)

IMO it shouldn't know anything about any sort of units a priori, but  
instead require things like Unit.add_conversion( 1.mile_per_hour,  
1.mph ).





module Units
   def method_missing( name, *args )
     top_units, bot_units = Units.from_string( name, self!=1 )
     Quantity.new( self, top_units, bot_units )
   end

   def Units.add_conversion( q1, q2 )
     @conversions ||= {}
     (@conversions[ q1.units ] ||= {})[ q2.units ] = 1.0 * q2.value /  
q1.value
     (@conversions[ q2.units ] ||= {})[ q1.units ] = 1.0 * q1.value /  
q2.value
   end

   def Units.convert( q1, units )
     convert_to = ( @conversions ||= {} )[ q1.units ]
     if convert_to && factor = convert_to[ units ]
       Quantity.new( q1.value * factor, *units )
     else
       raise "I don't know how to convert from #{q1.units} to #{units}"
     end
   end

   def Units.from_string( description, singularize=true )
     top = []
     bot = []
     section = top
     description.to_s.split( '_' ).each{ |unit|
       case unit
         when 'in'
           raise "Cannot create 'in' units (reserved for conversion)"
         when 'per'
           section = bot
         else
           section << ( singularize ? unit.singular : unit )
       end
     }
     return [top, bot]
   end

   class Quantity
     attr_reader :value, :units, :top_units, :bot_units

     def initialize( value, top_units=[], bot_units=[] )
       @value = value
       @top_units = [].concat( top_units )
       @bot_units = [].concat( bot_units )
       @units = [ @top_units, @bot_units ]

       #Simplify
       removed = []
       @bot_units.each_with_index{ |divisor, div_i|
         if i = @top_units.index( divisor )
           @top_units.delete_at( i )
           removed << div_i
         end
       }
       unless removed.empty?
         removed.each{ |divisor_index|
           @bot_units.delete_at( divisor_index )
         }
       end

       @top_units.sort!
       @bot_units.sort!
     end

     def dup
       self.class.new( @value, @top_units, @bot_units )
     end

     def method_missing( name, *args )
       if ( name = name.to_s ) =~ /^in_/
         Units.convert( self, Units.from_string( name.sub( /^in_/,  
'' ) ) )
       else
         top, bot = Units.from_string( name )
         self.class.new( @value, name, @top_units + top, @bot_units +  
bot )
       end
     end

     def same_units_as?( other )
       return false unless other.respond_to? :units
       self.units == other.units
     end

     def combine_units( *quantities )
       quantities.each{ |q|
         @top_units.concat( q.top_units )
         @bot_units.concat( q.bot_units )
       }
     end

     def +( other )
       if self.same_units_as?( other )
         self.class.new( @value + other.value, @top_units, @bot_units )
       else
         Expression.new( self, :+, other )
       end
     end

     def -( other )
       if self.same_units_as?( other )
         self.class.new( @value - other.value, @top_units, @bot_units )
       else
         Expression.new( self, :-, other )
       end
     end

     def *( other )
       if other.respond_to? :units
         self.class.new( @value * other.value, @top_units +  
other.top_units, @bot_units + other.bot_units )
       else
         self.class.new( @value * other, @top_units, @bot_units )
       end
     end

     def /( other )
       if other.respond_to? :units
         self.class.new( @value / other.value, @top_units +  
other.bot_units, @bot_units + other.top_units )
       else
         self.class.new( @value / other, @top_units, @bot_units )
       end
     end

     def to_s
       wrap = @top_units.length > 1 or @bot_units.length > 0
       out = wrap ? '(' : ''
       out << "#@value "
       if @value != 1 && @top_units.length == 1
         out << @top_units.first.plural
       else
         out << @top_units.join( '*' )
       end
       unless @bot_units.empty?
         out << '/'
         out << @bot_units.join( '*' )
       end
       out << ')' if wrap
       out
     end
   end

   class Expression
     attr_reader :o1, :op, :o2
     def initialize( o1, op, o2 )
       @o1 = o1
       @op = op
       @o2 = o2
     end

     def to_s
       "(#@o1 #@op #@o2)"
     end
   end

end

class String
   def singular
     self.gsub( /(([hs])e)?s$/, '\2' ).gsub( 'feet', 'foot' )
   end
   def plural
     out = self + ( ( self =~ /[hs]$/ ) ? 'es' : 's' )
     out.gsub( 'foots', 'feet' )
   end
end

class Numeric
   include Units
end