Code Sharing - Units (abandoned child)

G

Gavin Kistner

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 :eek:1, :eek:p, :eek:2
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
 
A

Ara.T.Howard

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

you might be interested in this:

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

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

cheers.

-a
--
===============================================================================
| 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
===============================================================================
 
J

Jeff Wood

--------------050506020903040405070603
Content-Type: text/plain; charset=ISO-8859-1; format=flowed
Content-Transfer-Encoding: 7bit

Gavin said:
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 :eek:1, :eek:p, :eek:2
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--
 
G

Gavin Kistner

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

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

Adam Sanderson

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
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
474,007
Messages
2,570,266
Members
46,863
Latest member
montyonthebonty

Latest Threads

Top