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