From: Gavin Kistner Date: 2005-09-01T12:41:51+09:00 Subject: Code Sharing - Units (abandoned child) 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