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.