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 :slight_smile:

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 be interested in this:

   http://raa.ruby-lang.org/project/quanty/

you're code looks very interesting too though...

cheers.

-a

路路路

On Thu, 1 Sep 2005, 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 :slight_smile:

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

--

email :: ara [dot] t [dot] howard [at] noaa [dot] gov
phone :: 303.497.6469
Your life dwells amoung the causes of death
Like a lamp standing in a strong breeze. --Nagarjuna

===============================================================================

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 :slight_smile:

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.

Damnit, ruby is too much fun. I was just thinking I was going to
need to do some simple unit conversions, and behold.

聽聽I have seen some other modules that do this, but your code looks very
natural to use. For the singular and plural issue, check out the way
they do it in Rails, it works pretty well. I've used it a few times.

I might play with this a little today.
聽聽.adam sanderson

Er, I must have missed it...where is there a module in that collection that does anything like this?

路路路

On Aug 31, 2005, at 11:14 PM, Jeff Wood wrote:

Gavin Kistner wrote:

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

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