From: Jeff Wood Date: 2005-09-01T14:14:32+09:00 Subject: Re: Code Sharing - Units (abandoned child) --------------050506020903040405070603 Content-Type: text/plain; charset=ISO-8859-1; format=flowed Content-Transfer-Encoding: 7bit Gavin Kistner wrote: > 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 > > > ... You might want to look at the Mega modules ( http://mega.rubyforge.org ) ... Then you can see the similar functionality they've already built, and maybe where you can fit your stuff in. j. --------------050506020903040405070603--