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 1, p, 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
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 1, p, 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