Does anyone remember when the
Well, of course it's funny. It gives us a warm feeling inside to know that even the most intelligent people around - literately rocket scientists - make the same mistakes we do. You bet however it wasn't funny for the scientists who lost $125 million dollars worth of hardware, and set back the space programme.
However, when we - as programmers - have to do the same kind of work we won't be laughing. Or will we? I will. I've got Data::Dimensions handy and it does all my unit conversion and unit checking for me, so I don't have these problems.
I live in the UK. We use the metric system for most things (which is why it gets my goat when people refer to imperial measurement as English measurements.) So I can tell you the size of a room in meters, I weigh things in kilograms, I measure what I eat in kilocalories and my Sprite comes in litres. It's the way my brain works.
Of course, that's not strictly true. I measure how tall and how heavy people are in feet and stone, long distances in miles and speed in miles per hour, and (of course) beer in (uk) pints.
This isn't much of a problem until I need to convert one to the other. For example, when I'm trying to work out how far away the pub is. For this I need to use Perl.
#!/usr/bin/perl
# turn on perl's safety feature use strict; use warnings;
# conversion utility use Math::Units qw(convert);
# it's about 200m to the bus stop from my house my $distance = 200;
# it's one and a half miles into town $distance += convert(1.5,"miles","m");
# it's another twenty paces to the pub $distance += convert(20,"yd","m");
# now how many miles is that? $distance = convert($distance,"m","miles");
printf "It's %0.2f miles to the pub\n",$distance;
This prints out
bash-2.05b$ perl ~/to_the_pub It's 1.64 miles to the pub
Excellent. I keep all the figures in SI units by converting immedialty to meters, and only convert to miles (the format I use for long distances) at the very last minute. So we now know how far away the pub is. Maybe I'll just pop to my local instead.
Though this code works, The problem is that I'm just storing a single value in my variables, and doing it this way it's easy to make a mistake. How about if I don't like the pub when I get there and I want to go another thirty meters onto the next pub. Let's add that onto the bottom of the script.
# it's another thirty meters to the next pub (I like London) $distance += 30;
printf "It's %0.2f miles to the 2nd pub\n",$distance;
Whoops! What did I do wrong here. Oh yes, I'd converted distance to miles already. It's certainly not 31.64 miles to the pub. I'd die of thirst first.
What we need to do is store the metadata - what units we're using - in
with the figures themselves so that the units themselves can take care
on any conversion that needs doing. This is where Data::Dimensions
comes in. It exports a function units
that allows us to create
unit objects by passing two arguments. The first argument is a hash
of types this unit uses, and the second argument is the value for that
unit. For example:
# distance, 10 feet my $distance = units({ feet => 1 }, 10) # speed, 30 miles per hour. my $speed = units({ mile => 1, hour => -1 }, 30);
Things are combined by using the set keyword and normal mathematical functions. Things that are in different units (like meters and feet) are automatically converted.
# increase the distance set $distance = $distance + units({ meter => 1 }, 10);
# work out how fast we're accelerating my $time = units({ second => 10 }, 10); my $accl = units({ meter => 1, second => -1}); set $accl = $speed / $time;
The units have to match on one side of the equation to the other - for example, you can't add speed and time together as it makes no sense.
# meters per second per second != meters per second + seconds set $accl = $speed + time;
And you can't mix up your units either.
# meters per second per second != meters set $accl = $speed * $time;
In both these situations Data::Dimensions will throw an exception at you. This is a good thing (tm). It tends to point out in testing something that you can easily overlook. Mistakes need to be big and loud so you can spot them, not silent and hard to find.
So without further ado, we convert our above script to use Data::Dimensions.
#!/usr/bin/perl
# turn on perl's safety feature use strict; use warnings;
use Data::Dimensions qw(&units extended);
# declare that we're measuring distances to our destination # in miles my $distance = units({ mile => 1 }, 0);
# two hundred meters to the bus stop set $distance = $distance + units({ meter => 1 }, 200);
# it's one and a half miles into town set $distance = $distance + units({ mile => 1 }, 1.5);
# it's another twenty paces to the pub set $distance = $distance + units({ yd => 1 }, 20);
printf "It's %0.2f miles to the pub\n", $distance;
# it's another thirty meters to the next pub (I like London) $distance = $distance + units({ meter => 1 }, 30);
printf "It's %0.2f miles to the 2nd pub\n", $distance;
This prints out:
bash-2.05b$ perl ~/to_pub_dd It's 1.64 miles to the pub It's 1.65 miles to the 2nd pub
Note how in this version we don't have to worry about converting our distance into miles. It's always in miles no matter what operation we perform on it, as that's what it's defined as at the top of the program. And because of this, we can't make the same mistake we did previously.
Firstly, you don't have do anything to simply define your own units. It's quite possible for you to make up new units on the fly, and as long as you don't ever need to convert this unit into another format Data::Dimensions will do the right thing.
use Data::Dimensions qw(&units extended);
# new units my $camels = units( { camels => 1 }, 20 ); my $money = units( { pounds => 1 }, 400 );
# how much? my $price = units({ camels => 1, pounds => -1 }); set $price = $camels / $money;
# throws an error, can't mix snakes with camels set $camels = $camels + units({ snake => 1 }, 3);
All the units that Data::Dimensions understands are located in the
#!/usr/bin/perl
# turn on perl's safety feature use strict; use warnings;
use Data::Dimensions qw(&units extended);
# define S.A. units (standard armadillo) # based on 'Dasypus novemcinctus' nine banded armadillo
# 1 S.A. length is 573mm, maximum length of the species $Data::Dimensions::Map::units{armadillo_length} = [ 0.573, { m => 1 }];
# 1 S.A. mass is 10kg, maximum mass of the species $Data::Dimensions::Map::units{armadillo_mass} = [ 10, { kg => 1 }];
# 1 S.A. time is 9 years, estimated wild lifespan $Data::Dimensions::Map::units{armadillo_time} = [ 9*365*24*60*60, { s => 1 }];
# what's 30 miles per hour in armadillo_length per armadillo_time? my $speed = units({ armadillo_length => 1, armadillo_time => -1 }); set $speed = units({ mile => 1, hour => -1 }, 30); print "30mph is $speed armadillo_length per armadillo_time\n";
The units you use to define your new units must be those in the
%SI_base
hash in the Data::Dimensions::Map class.