# Forum: Ruby Unit Conversion (#183)

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
on 2008-11-14 16:53
```-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

The three rules of Ruby Quiz 2:

1.  Please do not post any solutions or spoiler discussion for this
quiz until 48 hours have passed from the time on this message.

2.  Support Ruby Quiz 2 by submitting ideas as often as you can!
Visit <http://splatbang.com/rubyquiz/>.

3.  Enjoy!

Suggestion:  A [QUIZ] in the subject of emails about the problem
the original quiz message, if you can.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

## Unit Conversion (#183)

Google added a calculator to its search engine a while back. Enter
"convert 50 miles to kilometers", or even just "50 mi to km", and the
first "search" result will tell you that 50 miles is 80.4672
kilometers. This works for units other than length. Try "33 ml to
gal", "6 hours to minutes", and"50 stones to lbs", and you'll see that
Google's calculator knows a lot of different units and how to convert
between them all.

Your task is to write a units converter script. The input to the
script must be three arguments: the quantity, the source units, and
the destination units. The first example above would be run like this:

\$ ruby convert.rb 50 miles kilometers

Or, using abbreviations:

\$ ruby convert.rb 50 mi km

Support as many units and categories of units (i.e. volume, length,
weight, etc.) as you can, along with appropriate abbreviations for
each unit.```
on 2008-11-14 19:59
```On Nov 14, 10:49 am, Matthew Moss <m...@moss.name> wrote:
> 3.  Enjoy!
> "convert 50 miles to kilometers", or even just "50 mi to km", and the
>      \$ ruby convert.rb 50 miles kilometers
>
> Or, using abbreviations:
>
>      \$ ruby convert.rb 50 mi km
>
> Support as many units and categories of units (i.e. volume, length,
> weight, etc.) as you can, along with appropriate abbreviations for
> each unit.

This will be interesting. I'm not going to endeavor into myself b/c I
help maintain Stick (http://stick.rubyforge.org) which already does
this (and there's another lib out there that does it too). But it will
be interesting to see how others approach it.

Thanks,
T.```
on 2008-11-14 20:24
```>>      \$ ruby convert.rb 50 mi km
>>
>> Support as many units and categories of units (i.e. volume, length,
>> weight, etc.) as you can, along with appropriate abbreviations for
>> each unit.
>
> This will be interesting. I'm not going to endeavor into myself b/c I
> help maintain Stick (http://stick.rubyforge.org) which already does
> this (and there's another lib out there that does it too). But it will
> be interesting to see how others approach it.

Note to everyone else: using another lib, such as stick, is considered
cheating for this quiz.  :D```
on 2008-11-14 20:29
```Matthew Moss wrote:
> Note to everyone else: using another lib, such as stick, is considered
> cheating for this quiz.

That's "another lib which does unit conversion" not "any other lib",
right?```
on 2008-11-14 22:20
```On Nov 14, 2008, at 1:25 PM, Sebastian Hungerecker wrote:

> Matthew Moss wrote:
>> Note to everyone else: using another lib, such as stick, is
>> considered
>> cheating for this quiz.
>
> That's "another lib which does unit conversion" not "any other lib",
> right?

Yes.
Although, if it's wholly self-contained, that's worth some awesome
points.```
on 2008-11-15 01:16
```On Nov 14, 2008, at 11:20 , Matthew Moss wrote:

>>> Note to everyone else: using another lib, such as stick, is
>>> considered cheating for this quiz.  :D

so... `units` isn't a library... is this cheating?

>> def units(n, i, o) `units "#{n} #{i}" #{o}`[/[\d\.]+/].to_f; end
=> nil
>> units 50, :mi, :km
=> 80.4672

:D

I assume it is... but I also assume it isn't a real contender for the
intent of the quiz in the first place (which is a shame, because this
is much better reuse).```
on 2008-11-15 01:33
```On Nov 14, 2008, at 6:12 PM, Ryan Davis wrote:

> >> units 50, :mi, :km
> => 80.4672
>
> :D
>
> I assume it is... but I also assume it isn't a real contender for
> the intent of the quiz in the first place (which is a shame, because
> this is much better reuse).

Well, there are a number of past quizzes that would be rather boring
if reuse were always the primary goal.  :D

Don't be boring.  ;)```
on 2008-11-15 03:30
```On Fri, Nov 14, 2008 at 6:12 PM, Ryan Davis <ryand-ruby@zenspider.com>
wrote:
>>> units 50, :mi, :km
> => 80.4672
>
> :D
>
> I assume it is... but I also assume it isn't a real contender for the intent
> of the quiz in the first place (which is a shame, because this is much
> better reuse).

Don't forget mass can be converted to energy :)

Excellent quiz!  Waiting for some good answers!

Todd```
on 2008-11-15 10:31
```On 2008.11.14., at 19:55, Trans wrote:

>> 2.  Support Ruby Quiz 2 by submitting ideas as often as you can!
>> ## Unit Conversion (#183)
>> Your task is to write a units converter script. The input to the
>> script must be three arguments: the quantity, the source units, and
>> the destination units. The first example above would be run like
>> this:
>>
>>      \$ ruby convert.rb 50 miles kilometers
>>
>> Or, using abbreviations:
>>
>>      \$ ruby convert.rb 50 mi km

ruby convert.rb 100 mile per minute per second foot per second squared

(read: 100 mile per minute per second *to* foot per second squared)

Maybe we should allow

ruby convert.rb x unit1 to unit2

or

ruby convert.rb x long_unit_name even_longer_unit_name

to avoid unnecessary complicated (or impossible) parameter parsing?

Cheers,
Peter
___
http://www.rubyrailways.com```
on 2008-11-15 11:06
```Peter Szinek wrote:
> ruby convert.rb x unit1 to unit2
>
> or
>
> ruby convert.rb x long_unit_name even_longer_unit_name

Or
ruby convert.rb x "long unit name" "even longer unit name"
i.e. just assume that ARGV[1] and ARGV[2] are the unit names and let the
user
take care of unit names with spaces in them. Seems the most
uncomplicated
option to me.

HTH,
Sebastian```
on 2008-11-15 12:01
```> Or
> ruby convert.rb x "long unit name" "even longer unit name"
> i.e. just assume that ARGV[1] and ARGV[2] are the unit names and let
> the user
> take care of unit names with spaces in them. Seems the most
> uncomplicated
> option to me.

D'oh! :-)

Cheers,
Peter
___
http://www.rubyrailways.com```
on 2008-11-15 15:40
```>
> Don't forget mass can be converted to energy :)
And patrol to dollars.
Not to mention distance to time. Well it seems at least, whenever I
ask how far is it away I get answers like 1 hour. ;)
>
> Excellent quiz!  Waiting for some good answers!
1+
Robert```
on 2008-11-16 03:10
```On Fri, 14 Nov 2008 14:20:42 -0500, Matthew Moss wrote:

>>> Support as many units and categories of units (i.e. volume, length,
>>> weight, etc.) as you can, along with appropriate abbreviations for
>>> each unit.
>>
>> This will be interesting. I'm not going to endeavor into myself b/c I
>> help maintain Stick (http://stick.rubyforge.org) which already does
>> this (and there's another lib out there that does it too). But it will
>> be interesting to see how others approach it.
>
> Note to everyone else: using another lib, such as stick, is considered
> cheating for this quiz.  :D

I disagree, and think that if he actually has a useful lib, he should
show it off. When I wrote Quiz #95 (Code to S-Exp), it was useful to me
to see the rubynode and ParseTree solutions, even if some would have
considered that cheating. I wound up using the rubynode version for the
Ruby 1.8 version of the library I was writing.

(Or maybe if you say it's cheating, that means he's allowed to talk
it during the spoiler period.)

--Ken```
on 2008-11-16 06:17
```On Nov 15, 2008, at 8:07 PM, Ken Bloom wrote:

>>>> Or, using abbreviations:
>>> this (and there's another lib out there that does it too). But it
>>> will
>>> be interesting to see how others approach it.
>>
>> Note to everyone else: using another lib, such as stick, is
>> considered
>> cheating for this quiz.  :D
>
> I disagree, and think that if he actually has a useful lib, he should
> show it off.

It is cheating.

However, I never told him not to do it.  ;)

Seriously, this is Ruby Quiz. I'm not going to call the cops on you
because you did something other than what I expected.```
on 2008-11-16 06:20
```On Nov 15, 2008, at 8:07 PM, Ken Bloom wrote:

>>>> Or, using abbreviations:
>>> this (and there's another lib out there that does it too). But it
> to see the rubynode and ParseTree solutions, even if some would have
> considered that cheating. I wound up using the rubynode version for
> the
> Ruby 1.8 version of the library I was writing.
>
> (Or maybe if you say it's cheating, that means he's allowed to talk
> it during the spoiler period.)

For what it's worth, I always invited people to submit whatever they
wanted when I ran the quiz.  The more the merrier, I figure.  Beside,
I was just going to summarize whatever the heck I wanted to anyway.  ;)

James Edward Gray II```
on 2008-11-16 07:34
```> For what it's worth, I always invited people to submit whatever they
> wanted when I ran the quiz.  The more the merrier, I figure.
> Beside, I was just going to summarize whatever the heck I wanted to
> anyway.  ;)

Agreed. I said it was cheating, but I didn't say "don't cheat."  :D```
on 2008-11-16 18:35
```It would be much more fun to include some non static conversion
variables. Like:
ruby convert.rb 30 euros dollars
# Just making things more interesting```
on 2008-11-16 20:38
```On Nov 16, 2008, at 11:31 AM, Rubén Medellín wrote:

> It would be much more fun to include some non static conversion
> variables. Like:
> ruby convert.rb 30 euros dollars
> # Just making things more interesting
>

Go for it!  :D```
on 2008-11-17 11:31
```> Agreed. I said it was cheating, but I didn't say "don't cheat."  :D
>
>
Sorry for nitpicking, but it is *not* cheating. It might not be what
you want and it is important to know what you consider a valid
submission or not. But cheating induces fraud, deceit or dishonesty. I
guess any solution having a proud
require 'mylib'
at the top does not have any of these negative properties.

Not that I want to make your job harder than it is already but
solutions that are submitted here might be interesting for the humble
reader even if they do not hit the spirit of the quiz on its
sweet-spot, and you might not review them. Would you tend to agree on
this?

Cheers
Robert
--
We are all in the gutter, but some of us are looking at the stars.

Oscar Wilde```
on 2008-11-17 16:00
Attachment: convert.rb (2 KB)
```My converter converts alcoholic beverages by effect *). In addition to
the source and destination beverage it converts the used containers,
too. Google can't do this yet ;-)

Example:

\$ ruby convert.rb 1 bottle of wine into glasses of beer
1 bottle (0.75l) of wine (VOL 12%) effects equal to 6.06 glasses (2.00l)
of beer (VOL 4%)

\$ ruby convert.rb 1 bucket of beer into cups of martini
1 bucket (10.00l) of beer (VOL 4%) effects equal to 26.47 cups (2.65l)
of martini (VOL 17%)

Martin

*)
- Please use results with care!
- It can't turn water into wine!```
on 2008-11-17 16:26
```On Nov 17, 2008, at 4:27 AM, Robert Dober wrote:

>> Agreed. I said it was cheating, but I didn't say "don't cheat."  :D
>>
>>
> Sorry for nitpicking, but it is *not* cheating.

Provide a simple, better word for "going against intended guidelines"
and I'll consider it.

In reality, I think most people understand that it is being used
tongue-in-cheek. And, as I stated, "cheating" is acceptable.

> It might not be what
> you want and it is important to know what you consider a valid
> submission or not.

Basically, anything within reason that solves the quiz problem is
valid, "cheating" or not.

> Not that I want to make your job harder than it is already but

> solutions that are submitted here might be interesting for the humble
> reader even if they do not hit the spirit of the quiz on its
> sweet-spot, and you might not review them. Would you tend to agree on
> this?

How about just writing a solution that solves the quiz -- "cheating"
if you like, not "cheating" if you don't like -- and leave the spirits
to go haunt some Perl coder. I never have time to fully review all the
solutions, "cheating" or not. But I generally give everything a once-
over. Just because you do something that I've called cheating will not
automatically send your submission to the trash bin. On the contrary,
I'll probably point it out as "the right way" to do things.

If your main issue is that you think "cheat" implies dishonesty, then
understand I mean no such thing. gsub(/cheat/, "reuse") and you'll be
happier.```
on 2008-11-17 21:14
```> If your main issue is that you think "cheat" implies dishonesty, then
> understand I mean no such thing. gsub(/cheat/, "reuse") and you'll be
> happier.
No problem at all, I have however taken some time to explain why I was
somehow shocked OL.
I apologize if this is kind of wasting bandwidth on the Quiz Thread.```
on 2008-11-18 18:54
```I just wanted to submit my first Ruby-quiz solution ever - but I was
waiting for the other solutions, being unsure when does the 48-hour
deadline end - but by now I am pretty sure it's over. So what's going
on?

Cheers,
Peter
___
http://www.rubyrailways.com
http://scrubyt.org```
on 2008-11-18 19:56
```On Nov 18, 2008, at 11:50 AM, Peter Szinek wrote:

> I just wanted to submit my first Ruby-quiz solution ever - but I was
> waiting for the other solutions, being unsure when does the 48-hour
> deadline end - but by now I am pretty sure it's over. So what's
> going on?

Yes, the quiz is (usually) posted Friday, so submissions typically
start rolling in Sunday. And Martin did provide his alcohol submission
yesterday. So feel free to post.```
on 2008-11-18 20:33
```> Yes, the quiz is (usually) posted Friday, so submissions typically
> start rolling in Sunday. And Martin did provide his alcohol
> submission yesterday. So feel free to post.

OK. <drumroll> my first submission to Ruby-quiz:

(After all those discussion on cheating, I am not sure if this
solution is not considered cheating of some sort ;-)

===============================================================
require 'rubygems'
require 'cgi'
require 'scrubyt'

begin
+#{CGI::escape(ARGV[1])}+to+#{CGI::escape(ARGV[2])}"

final_result(/= (.+) /)
end
end
rescue
puts "Sorry, even *google* can't translate that!"
end
===============================================================

ex:
ruby converter.rb 10 "meter per second" "mile per hour"
22.3693629

ruby converter.rb 10 USD EUR
7.91201836

ruby converter.rb 7 "ruby gems" "python eggs"
Sorry, even *google* can't translate that!
etc.

disadvantage: you need to be online
advantage: it's quite rich, robust and up-to date (e.g. currency
conversions)

I am wondering if this solution is (at least somewhat) OK - I have a
similar one, though that doesn't require you to be on-line. However, I
am not going to implement it if the above solution is BS :-)

Cheers,
Peter
___
http://www.rubyrailways.com
http://scrubyt.org```
on 2008-11-19 02:19
```On Nov 18, 2:28 pm, Peter Szinek <pe...@rubyrailways.com> wrote:
> require 'rubygems'
>    end
>
>
> I am wondering if this solution is (at least somewhat) OK - I have a
> similar one, though that doesn't require you to be on-line. However, I
> am not going to implement it if the above solution is BS :-)

I think this is an interesting solution b/c it is prescient of a
future we are likely to see. When average broadband connections
approach 1gbs or so this kind of thing will be the norm. We are seeing
this evolution now, with software not being installed by physical
media any longer, but via online package management. And now with
things like RubyGems it is becoming even more fine grained.

Ultimately "usr/lib" will be on the wire.

So what is your similar solution? Taking a guess... is it this with a
cache?

T.```
on 2008-11-19 02:31
```From: Trans [mailto:transfire@gmail.com]
...
# Ultimately "usr/lib" will be on the wire.

totally.

i would wish something like,

require "http://some.site/some.rb"

or
gems that's requireable (no need for require rubygems)

require "http://some.site/some.gem"

or
or just plain require a zipful of rbs/gems/othergz...

require "http://some.site/some.gz"```
on 2008-11-19 03:41
```Given the conversation about "cheating", and what I know about the
potential complexities of a good solution, having written the original
prototype of units.rb, I asked Peter Vanbroekhoven, who is the master
mind behind Stick's current units system, if he'd be willing to write
up a brief overview of how it works. Thankfully he has obliged us.

Also, I just switched the Stick repo to Git. So the code can be
browsed here:

http://stick.rubyforge.org/git?p=stick.git;a=tree

The main units code part being here:

http://stick.rubyforge.org/git?p=stick.git;a=tree;...

Especially look at base.rb.

The notation this uses is not exacty like that requested by the quiz,
rather it would look like:

50.mi.to(km)

But I think that's close enough --it wouldn't be very hard to add a
cli front-end to translate.

Anyway, cheating or not, I hope this proves helpful to anyone
interested in the subject.

Here's Peter's overview:

== The Basics

There are two types of units: base units, which are not expressed as a
function of other units, and derived units which are expressed as a
function of other units. The base units are represented by the
Stick::Units::BaseUnit class. Derived units are implemented by
Stick::Units::Unit. Derived units are represented as the product of
powers of other units that can  be base units or derived units. These
powers are kept in a Hash. BaseUnit is never exposed to the user; if
you need a base unit, is represented as a Unit that's the product of a
single BaseUnit to the power 1. To be able to work with units, it's
often necessary to normalize them in some way, i.e., express them in
function of base units only. This is done by the Unit#simplify method.
This normalization is not performed automatically; rather we have
chosen to have the user of units.rb initiate any conversion so he or
she can have more control over rounding errors and such. Units can be
multiplied, divided and exponentiated.

Units by itself are not very interesting unless they are combined with
some numeric value. This is what the Stick::Units::Value class does.
It holds a Unit, and a value which can be integers, float,
BigDecimals, complex numbers, etc. Values can be multiplied, divided,
and added and subtracted if the units are compatible. This is checked
by normalizing the units and then transforming the Value with the
larger unit to the smaller unit. For instance, adding inches and feet
will result in inches because an inch is the smaller unit. Value
supports most other numeric operators.

== Converters

units.rb has the notion of converters. This notion has been introduced
because sometimes units have the same name but differ in value
depending on location (e.g., a hundredweight in the UK and the US),
they can have the same symbol though they are different, or in the
case of currency you might want to use different services with
possibly slightly different exchange rates. A unit belongs
unambiguously to a single converter. Units from different converters
can be used together, so it is possible to use US and UK
hundredweights in the same expression. What happens here is that a
unit is not only determined by a name but also by a converter. That's
all there is to it really.

There's always the notion of a current converter. When constructing a
unit, the current converter is used by default unless specified
otherwise. This current converter can be changed with
Stick::Units.with_unit_converter which takes a block. The current
converter is stored in a thread-local variable for the duration of the
block and is taken from there. This gives the user means to specify
what unit systems to use in a more granular way. Converter can include
other converter. This allows a converter to extend another converter
and override some of the names. This is actually what
Stick::Units.with_unit_converter does: internally it creates an
anonymous converter that includes both the previous "current
converter" and the new one which gets stacked on top and thus takes
precedence.

== Syntactic Sugar

Units are specified using symbols like :mile and :in, but units.rb
offers a lot of syntactic sugar to make it easier to use. After
including Stick::Units, you can use mile and in directly, or even do
1.in and 2.miles. This is implemented through method_missing and

units.rb uses a DSL to specify converters and units. These DSLs are
conveniently used in the config files to preload a large number of
units.```
on 2008-11-19 05:36
```> Anyway, cheating or not, I hope this proves helpful to anyone
> interested in the subject.

Ack... Fine, nothing is cheating; forget I ever mentioned it. Since
the word seems to be problematic all of a sudden, I'll refrain from
using it in the future.

In any case, thanks for the overview of stick/units. I was unaware of
the library before this week, and it's always good to know some the
the quality libs out there.```
on 2008-11-19 07:58
```Martin Boese wrote:
> \$ ruby convert.rb 1 bucket of beer into cups of martini
> 1 bucket (10.00l) of beer (VOL 4%) effects equal to 26.47 cups (2.65l)
> of martini (VOL 17%)
>

Either your glasses and cups are way bigger than mine, there seems to be
a bug in your output code.

Regards,

Michael```
on 2008-11-19 08:02
```Michael Ulm wrote:
>>
>> \$ ruby convert.rb 1 bucket of beer into cups of martini
>> 1 bucket (10.00l) of beer (VOL 4%) effects equal to 26.47 cups (2.65l)
>> of martini (VOL 17%)
>>
>
> Either your glasses and cups are way bigger than mine, there seems to be
> a bug in your output code.
>

Oops, I understand the output now, the amount in the brackets is the
total
amount in liters, not the amount per container. Sorry for the noise.

Regards,

Michael```
on 2008-11-19 16:23
```On Nov 18, 11:32 pm, Matthew Moss <m...@moss.name> wrote:
> > Anyway, cheating or not, I hope this proves helpful to anyone
> > interested in the subject.
>
> Ack... Fine, nothing is cheating; forget I ever mentioned it. Since
> the word seems to be problematic all of a sudden, I'll refrain from
> using it in the future.

Don't sweat it. I thought of it as "cheating" myself at first.
Thankfully, some wise members of this list made me realize, whether it
technically qualifies as a quiz solution or not, it could at least
contribute to the conversation.

> In any case, thanks for the overview of stick/units. I was unaware of
> the library before this week, and it's always good to know some the
> the quality libs out there.

Your welcome, and I'll extend that to Peter.

T.```
on 2008-11-20 00:40
Attachment: 183-robert-dober.tgz (2 KB)
```Well time is difficult to get but I just had to supply a Ruby Quiz
submission *again*.
Was so much fun. Tried to implemnt Tom's syntax driven by a data file
(tiny :(, see above)

# file: units.txt
#
# Units data file for Ruby Quiz #183
#
# -----------------------------------
#
1 in = 0.0254 m
1 l  = 0.001 m3
use SI prefixes for m g l m3
_____________________________________________

# file: units.rb

module Conversion
#
# A primitive parser to create the conversion data structure, syntax
checks are
# omitted to shorten the solution ;).
#
class LineParser
SIUnits = { "Y" => 10**24, "Z" => 10**21, "E" => 10**18, "P" =>
10**15, "T" => 10**12,
"G" => 10**9, "M" => 1_000_000, "k" => 1000, "h" => 100, "D" =>
10,
"d" => 0.1, "c" => 0.01, "m" => 0.001, "Âµ" => 0.000_001, "n" =>
10**-9, "p" => 10**-12,
"f" => 10**-15, "a" => 10**-18, "z"=> 10**-21, "y" => 10**-24 }

def parse_line line
return if /^\s*#/ === line || line.strip.empty?
line = line.strip.sub /#.*/, ""
case line
when /^use SI prefixes for /
else
end
end

def traverse &blk
@c.each do | unit, conversion_hash |
_traverse unit, conversion_hash, [ unit ], &blk
end
end
private
def add_conversion lhs_value, lhs_unit, equal_dummy, rhs_value,
rhs_unit
@c[ lhs_unit ][ rhs_unit ] = Float( rhs_value ) / Float( lhs_value
)
@c[ rhs_unit ][ lhs_unit ] = Float( lhs_value ) / Float( rhs_value
)
end

units.each do |unit|
end
end

SIUnits.each do | prefix, conversion |
@c[ prefix + unit ][ unit ] = conversion
@c[ unit ][ prefix + unit ] = 1 / conversion
end
end

def initialize
@c = Hash::new{ | h, k | h[ k ] = { } }
end

def _traverse src_unit, unit_conversions, traversed_units, f=1.0,
&blk
unit_conversions.each do | new_unit, conversion |
next if traversed_units.include? new_unit
blk.call src_unit, new_unit, f * conversion
_traverse src_unit, @c[ new_unit ], traversed_units + [ new_unit ], f
* conversion, &blk
end
end
end
#
#  Will be subclassed for each unit.
#  Each such sublcass will than get all conversion methods defined
dynamically
#  by means of exploring "units.txt"
#
class ProxyClass
# syntactic sugar
def to; self end
alias_method :as, :to

private
def initialize value
@value = value
end
end

ProxyClasses = {} # maps units to their proxy classes
def method_missing unit_name
pc = ProxyClasses[ unit_name.to_s ] || super( unit_name )
pc::new self
end

conversions = LineParser::new
File::open "units.txt" do | f |
f.each do | line |
conversions.parse_line line
end
end
conversions.traverse do | src_unit, tgt_unit, conversion |
( ProxyClasses[ src_unit ] ||= Class::new ProxyClass ).module_eval
do
define_method tgt_unit do (@value * conversion).to_s + tgt_unit
end
end
end

end

class Integer
include Conversion
end

class Float
include Conversion
end
_________________________________________________________________

# file: test-solution.rb

require 'units'
require 'test/unit'

class Test183_01 < Test::Unit::TestCase
def test_001_simple
assert_equal "25.4mm", 1.0.in.to.mm
assert_equal "1.0mm", 0.001.m.to.mm
assert_equal "0.0393700787401575in", 1.mm.in
assert_equal "39370.0787401575in", 1.km.as.in
assert_equal "0.001m3", 1.0.l.as.m3
end
end

Thx for the quiz.
--
Ne baisse jamais la tÃªte, tu ne verrais plus les Ã©toiles.

Robert Dober ;)```
on 2008-11-20 19:16
```On Nov 19, 6:36 pm, "Robert Dober" <robert.do...@gmail.com> wrote:
> Well time is difficult to get but I just had to supply a Ruby Quiz
> submission *again*.
> Was so much fun. Tried to implemnt Tom's syntax driven by a data file
> (tiny :(, see above)

If this were a a contest, looks like you get the blue ribbon :)

> # file: units.txt
> #
> # Units data file for Ruby Quiz #183
> #
> # -----------------------------------

Impressed you got that much functionality from just this little bit of
code.

What up with "µ" => 0.000_001 ;-)

T.```
on 2008-11-20 21:58
```> ## Unit Conversion (#183)

Summary will be posted tomorrow; new quiz on Saturday. Apologies...```
on 2008-11-20 23:08
```On Thu, Nov 20, 2008 at 6:22 PM, Trans <transfire@gmail.com> wrote:
>
>
> T.
>
>
I do not really merit this praise, though I appreciate it;). Actually
the metaprogramming part is about ok, but the
datastructure is quite primitive, well if only I had more time (or
were smarter LOL).
Cheers
R.```
on 2008-11-21 20:49
```> ## Unit Conversion (#183)

The right way, generally, to do a task such as unit conversion is to
see if someone has already done all the hard work for you. As was
pointed out, there are several options in this respect:

* The [Stick] library for Ruby; a [brief summary] was provided.
Stick provides a value class (i.e. quantity with units), conversions,
syntactic sugar and more.
* Google's search engine can act as a calculator, including unit
conversions. Using Google's API is one option; another is screen-
scraping, as was done by _Peter Szinek_. (Of course, as noted, you
must have an activate Internet connection to use this solution.)
* As was pointed out by _Ryan Davis_, there is a BSD/Un*x command
and library called `units` which does this same task. Transform the
arguments, pass them to a shell, and capture the output.

Many thanks to _Martin Boese_, whose solution had to be empirically
confirmed. Repeatedly.

But I'm going to look at the solution from _Robert Dober_. While it is
limited, as posted, his data driven approach could be expanded to
include more conversions.

To understand how the expression `1.0.in.to.mm` will generate the
string "25.4mm", I'll trace it a step at a time, looking at the
relevant bits of code.

First, we have the float value `1.0`, but where does the method `in`
come from? Clearly, class `Float` gets something by way of extension:

class Float
include Conversion
end

Module `Conversion` only defines one method that will extend `Float`
(with the rest of `Conversion` being helper classes and code executed
when `Conversion` is first evaluated). That method is `method_missing`:

def method_missing unit_name
pc = ProxyClasses[ unit_name.to_s ] || super( unit_name )
pc::new self
end

So we will look for `ProxyClasses["in"]` and, if not found, we just
call to the parent class and hope it knows what to do with method call
`in`. But in this case, we're expecting to find something in
`ProxyClasses`... a Class, in fact, which we attempt to instantiate
immediately using `new`. But where does we fill `ProxyClasses`?

Ah, that would be the code right below `method_missing` in his
solution: the code that makes use of `LineParser`.

conversions = LineParser::new
File::open "units.txt" do | f |
f.each do | line |
conversions.parse_line line
end
end

Robert provided a minimal `units.txt` data file to show how the code
works. (Note that the line beginning "use SI" is part of the data file
and not a mistake; see `parse_line` for how that is handled.)

1 in = 0.0254 m
1 l  = 0.001 m3
use SI prefixes for m g l m3

It could be expanded greatly to support many more units. As each line
is read, the `LineParser` object parses them, keeping track of the
conversion rules -- I'll come back to that later. What I want to look
at first is what gets done with those rules:

conversions.traverse do | src_unit, tgt_unit, conversion |
( ProxyClasses[ src_unit ] ||= Class::new ProxyClass ).module_eval
do
define_method tgt_unit do (@value * conversion).to_s + tgt_unit
end
end
end

`traverse` is going to enumerate over a number of valid conversions --
source units, target units, and the conversion factor. And here we see
from where the `ProxyClasses` originate... New `ProxyClass` objects
are created through the code `Class::new ProxyClass` (but only if one
didn't exist already for the particular source unit... note the use of
the `||=` operator which only evaluates the right side and assigns
left if the left was initially nil).

After ensuring that the `ProxyClass` corresponding to the source units
exists, we call `module_eval` in order to add methods to the anonymous
class just created. The method name will be the target units, and the
method multiplies in the conversion factor, converts to a string, and
appends the targets units.

So, getting back to our example `1.0.in.to.mm`, we've now found the
`ProxyClass` corresponding to `1.0.in`. And we know that `ProxyClass`
also has methods named by target units, which includes one that
corresponds to the last part of the example: `.mm`.

If you're wondering about `to`, every `ProxyClass` defines that method
to return self: essentially a useless function (in the sense that it
does nothing more than `1.0.in.mm`). It's existence mimics other
libraries, and the point is readability. (An alternative would be a
more traditional call, such as 1.0.convert(:in, :mm) or similar.)

So once these proxy classes exist, there's very little effort going on
to evaluate calls such as our example. And creating the proxy classes
isn't much more difficult, assuming you have a proper conversion
table. Now we come back to `LineParser` and what happens beyond its
`parse_line` method. (I'll skip `parse_line` itself, since it is a
few, simple regular expressions.)

Most of `units.txt` that defines our conversions is going to be
split line of the data file. The conversion table (stored in `@c`) is
two-layered hash -- a hash of hashes -- and is setup with this code:

def add_conversion lhs_value, lhs_unit, equal_dummy, rhs_value,
rhs_unit
@c[ lhs_unit ][ rhs_unit ] = Float( rhs_value ) / Float( lhs_value )
@c[ rhs_unit ][ lhs_unit ] = Float( lhs_value ) / Float( rhs_value )
end

The conversion ratio (and the inverse conversion ratio) are stored in
two places based on the indexing order. By storing both ratios/orders,
we can convert in "both directions". That is, for our example, not
only can we convert inches to millimeters, but millimeters to inches.

The last bit of file parsing is adding appropriate metric prefixes (SI
units). One line in the file indicates which units are worthy of
metric prefixes. In the data file provided, we see that meters can
accept metric prefixes (such as "kilo" and "milli"), but inches will
not. These prefixes are handed by `add_si_unit_for`:

SIUnits.each do | prefix, conversion |
@c[ prefix + unit ][ unit ] = conversion
@c[ unit ][ prefix + unit ] = 1 / conversion
end
end

Here, `unit` is the particular unit we want to support metric
prefixes. `SIUnits` is the hash containing the metric prefixes as
characters and the corresponding orders of magnitude. For every unit
and metric prefix, two more conversions are added, each the inverse of
the other: conversion between the naked unit and the adorned unit
(e.g. between meters and millimeters, and vice-versa).

Finally, `traverse` is an enumerator that will yield (via `blk.call`)
every valid combination of units and the appropriate conversion
factor. It manages this without storing every conversion (e.g. we
store the inches to meters conversion, and the meters to millimeters
conversion, but don't explicitly store inches to millimeters).
Enumerating every possible, valid conversion is done in the private
method `_traverse`:

def _traverse src_unit, unit_conversions, traversed_units, f=1.0, &blk
unit_conversions.each do | new_unit, conversion |
next if traversed_units.include? new_unit
blk.call src_unit, new_unit, f * conversion
_traverse src_unit, @c[ new_unit ], traversed_units + [ new_unit ],
f * conversion, &blk
end
end

The final, recursive step here is what allows us to build a transitive
closure of all units. `src_unit` is, of course, the source unit (e.g.
inches). `unit_conversion` contains all possible immediate conversions
from the source and is the hash of units and conversion factors. And,
you can see, we enumerate those into `new_unit` and `conversion`.

We skip a target unit if it's already been visited (i.e. in
`traversed_units`). Otherwise, we yield to the caller (`blk.call`) and
recurse, now converting the source unit to everything the target unit
can also be converted, making sure to update `traversed_units` so as
to terminate eventually.

[1]: http://stick.rubyforge.org/