Beefy Boxes and Bandwidth Generously Provided by pair Networks
Think about Loose Coupling
 
PerlMonks  

The Definitive Unit Conversion Script

by Aristotle (Chancellor)
on Dec 13, 2002 at 15:28 UTC ( [id://219603]=sourcecode: print w/replies, xml ) Need Help??
Category: Miscellaneous
Author/Contact Info /msg Aristotle
Description:

Just what you'd expect from the title. :-)

New conversions are very easy to add; add a new row to the table, containing the source and destination unit and the factor to multiply by to get one from the other. If it's not a simple factor, add an array with two subrefs, one to convert from source to destination unit and another to convert backwards.

It will automatically keep applying conversions and reverse conversions to all the results it generates until it has calculated all related units from any given one.

Run it and it will tell you how to use it.

#!/usr/bin/perl -w
use strict;
use Getopt::Std;

sub conv {
    my ($val, $fac) = @_;
    ref $fac ?
        $fac->[0]->($val) :
        $val * $fac;
}

sub reverse_conv {
    my ($val, $fac) = @_;
    ref $fac ?
        $fac->[1]->($val) :
        $val / $fac;
}

use constant SRC => 0;
use constant DST => 1;
use constant FAC => 2;

my @table = (
    #### temperature
    [ c => f => [
        sub { ($_[0] * 9/5) + 32 },
        sub { ($_[0] - 32) * 5/9 },
    ]],
    [ c => k => [
        sub { $_[0] + 273.16 },
        sub { $_[0] - 273.16 },
    ]],

    #### mass/weight
    [ kg   =>  g =>   1000 ],
    [ floz =>  g =>     30 ],
    [ lbs  => kg => 0.4536 ],

    #### distance
    [ ft   => m  =>   0.3048 ],
    [ ft   => in =>       12 ],
    [ in   => cm =>     2.54 ],
    [ m    => cm =>      100 ],
    [ yd   => m  =>   0.9144 ],
    [ km   => m  =>     1000 ],
    [ mile => km => 1.609347 ],
);

my @unit = sort keys %{{
    map +($_->[SRC] => 1, $_->[DST] => 1), @table
}};

die << "END_USAGE" unless getopts('ap:', \my %opt) and @ARGV and (0 ==
+ @ARGV % 2);
usage:  unitconv [-a] [-p precision] <val> <unit> [<val> <unit> [..]]
    -p     the number of decimal places [default: 2]

    -a     force all possible conversions to be shown, even if
           insufficient display precision truncates the value to 0

    <unit> can be one of: @unit

    Results will be printed on one line per input val/unit pair.
END_USAGE

my $prec = $opt{p} || 2;

while(@ARGV) {
    my (%val, $keys_before) = reverse splice @ARGV, 0, 2;

    do {
        $keys_before = keys %val;
        for(@table) {
            $val{$_->[DST]} = conv($val{$_->[SRC]}, $_->[FAC])
                if exists $val{$_->[SRC]}
                and not exists $val{$_->[DST]};

            $val{$_->[SRC]} = reverse_conv($val{$_->[DST]}, $_->[FAC])
                if exists $val{$_->[DST]}
                and not exists $val{$_->[SRC]};
        }
    } until ($keys_before == keys %val);

    unless($opt{a}) {
        sprintf("%.*f", $prec, $val{$_}) == 0 and delete $val{$_}
            for keys %val;
    }

    printf "% 8.${prec}f %s " x keys(%val) . "\n",
        map +($val{$_} => $_), sort keys %val;
}
Replies are listed 'Best First'.
(tye)Re: The Definitive Unit Conversion Script
by tye (Sage) on Dec 13, 2002 at 17:18 UTC

    I tried units of "kg/m/m" and "kg/m^2" and it didn't work. I think you have a bug.   ; )

            - tye

      Actually, I am looking for a good (read: high precision values) comprehensive table of units and conversion factors to add to this. Before I add new units however I need to add a filter that rejects converted values if they get too large (ie if I ask for 1500 miles I don't want to know how much that is in milimeters).

      But that's on the wishlist - for now, I needed to scratch an itch, and most every unit conversion CGI page I found out there was at least mildly painful. The too large filter is actually a bit more complex than the nobrainer too small filter. I'll update the script and add more many units when I have a few spare brain cycles (time is plentiful, but my mind is always elsewhere :-) ).

      Makeshifts last the longest.

        You don't need to add a conversion factor for what I gave (though supporting such to increase precision is fine). You already handle going from 'f' to 'k' via f=>c then c=>k. It'd be nice (with a name including "Definitive") to also handle conversions between m^3 and ft^3, ft/sec and mi/hour, kg/m^2 and lb/ft^2, etc. using the data you already have in your code (except you don't have 60sec=min, 60min=hr, and ft..mi yet).

                - tye
Re: The Definitive Unit Conversion Script
by mojotoad (Monsignor) on Dec 13, 2002 at 19:48 UTC
    I was interested in tye's quip about dealing with unit specifications such as 'kg/m/m' vs 'kg/m^2'...so I started digging around on CPAN for tokenizers that might assist in normalizing unit representations. There's some infix/postfix stuff, as well as various tokenizers such as Math::Expr that might be of assistance here.

    Perhaps more importantly, there's some unit conversion modules already out there that might bear further scrutiny:

    Math::Units
    Convert-Units
    Math::Calc::Units

    Of these, the Convert-Units bundle seems to be on the money, since in the author's words:

    It is intentionally distinct from the Math::Units module. Why? The Math::Units module implies that unit conversions are exact, with one-to-one relationships. This is fine for scientific work. It's even fine for some general purpose/real-world uses (such as converting Fehrenheight to Celcius).

    Real-world measurement systems are conflicting. For instance, a "point" in typography is equivalent to 1/72 inch, according to PostScript specs and common usage. Other type systems consider it 1/72.27 inch, or 0.01383 inches, or 0.0148 inches. Outside of that context, a point may be 1/120 or 1/144 inch.

    Common notations and abbreviations also depend on context. Does "12 pt" mean "12 point", "12 parts" or "12 pints"?

    Even without conflicts in the definition of a particular unit, there's no need to convert point sizes for fonts into miles or light-years. Typesetters, surveyors and astronomers user different scales as well as systems.

    Hope these are of assistance. At the very least, you can mine the modules for conversion tables!

    Matt

      Thanks for doing my digging work for me. :-) These were all quite interesting finds. Neither seems to aim for what I do.

      Math::Calc::Units and Convert::Units both attempt to offer extremly generic parsing of input and to produce human readable output to the extend of taking (in Math::Calc::Units) specifications such as 10MB / 384kbps and returning a result expressed in minutes. That's far more work than I ever intend to do, and IMHO should be done in separate stages (and therefor modules) - one to calculate a straight result, on to apply a set of heuristics to convert a given value into a more human readable specification. The latter kind of module would be useful in different environments as well.

      Math::Units is much closer to my goals, so I took a closer look. I found it only does one-shot conversions. To do so it expends considerable effort to deduce the shortest path between two indirectly convertible units. Trying to do broadside conversions with that module would be costly, and since it provides no querying interface, also quite an annoyance.

      In contrast, even though brute force, the "flood fill" approach I used naturally takes shortest path between two indirectly related units. It also does broadside conversions at no extra cost.

      My final gripe with Math::Units is that its internal data structure layout is quite complex and opaque. It doesn't look as though adding conversions is anywhere as simple as doing so in the table setup I used. Granted the module does try to be a lot more intelligent than my code, but it does lead to signficant complexity.

      Makeshifts last the longest.

Re: The Definitive Unit Conversion Script
by princepawn (Parson) on Dec 13, 2002 at 17:24 UTC
    Why don't you put this on CPAN as Convert::Units or Units::Convert or something? This is useful.

    Carter's compass: I know I'm on the right track when by deleting something, I'm adding functionality

      Thanks for the compliment :)

      Anyone have suggestions how I should go about modularizing this?

      At first I didn't see any way to meaningfully transform it into a module since it has to have a useful interface. Then I began thinking.. the following is a brainstorming transcript if you will, so bear with me and give me feedback.

      I guess I should swap out the inner loop into of the script into a subroutine and package it with the table. The rest of the hoohah for calling the script from the commandline could ship as a script like the GET, HEAD etc ones that come with LWP. The inner loop alone probably offers too little control - extra routines to allow to ask for specific conversions should be useful. So I should probably add an interface to ask whether a unit is known. Actually, the best way to do that is probably to have an interface that returns all possible conversion targets for a given unit, which would obviously return nothing (= false in scalar context) if the unit is unknown.

      The question of whether there should be an interface to add new conversions at runtime and how it should look repeatedly crossed my mind during all this, but I don't feel like it's a direction I'd want to take the module in. It's probably better if it remains something a.. "metaconstant" module, a bunch of oft needed calculations you just drop in your script and don't think about. It almost makes me think the "introspection" interface (requesting what conversion targets exist for a given unit) is overkill, but then it is probably useful even for static uses for things like maybe dynamically populating dropdowns or such.

      If I'm gonna go there, maybe there should plaintext names and/or descriptions for the units supported.. on second thought, that would require translation and all the countless headaches it brings along, which is definitely farther out than I want to go. It would require an extra interface for the language selection and querying the available languages too, and people will probably still have to reimplement those themselves if it doesn't happen to support their language. If could include a lot of languages - but neither do I know who I'd ask for translations, nor would I be willing to put in the effort to maintain all of that. And it would probably be useless bloat for the vast majority of users. Maybe as an addon module ::Descriptions or something, should the interest in this module ever warrant that amount of work.

      So I have a module containing the conversion table, a routine for one-shot conversions, one for broadside salvo conversions (calculate any, all and every related unit you can get to), and one to ask whether a unit is known and what conversion targets it has, if so.

      Then the query routine should probably differentiate between all available direct conversion targets that can be reached via the one-shot routine and the full list of related units you can get via the broadside converter.

      Maybe there should be a single unit to single unit conversion routine which does not care whether a direct conversion is possible or intermediate conversions have to be done. But that would be complex - choosing the conversion steps such that you get from the source to the destination in the shortest possible way - or even at all - is far from trivial. It is simpler to just bruteforce a broadside conversion and pluck the result out of it. But the user can do that too, esp if the broadside conversion function returns its results in a convenient format. There's no point in adding garden decoration to the module.

      The most convenient format is probably to return either a hashref or flat list according to the context.

      ...

      Ok, I'm done. Suggestions, anyone?

      Makeshifts last the longest.

        The interface I would want would be something like:     my @new= ConvertTo( $toUnits, $fromUnits, @old ); which would convert the numbers in @old from $fromUnits to $toUnits and return the results. So:

        # <--------- inches ---------><----- ft -----> my @inches= ConvertTo( 'in', 'ft', 1, 2, 3 ); # @inches is now ( 12, 24, 36 )
        Note that it is "to" followed by "from" so the unit name is closest to the numbers that are/will be in those units.

        If I want to do a lot of conversions but not all at once, then:

        my $toInFromFt= ConvertTo( 'in', 'ft' ); while( <IN> ) { chomp; print OUT $toInFromFt->($_), $/; }
        And I'd probably support aliases for units and include a "long alias" for every unit: "inch", "feet", "meter", "centimeter", "Centigrade", "Farenheit", "Kelvin", "second", "hour", etc. just to avoid confusion.

        I'd probably put the unit data after __DATA__ so you could just append more units to be able to support them.

        The "all possible conversions" is interesting for interactive exploration, but I don't see nearly as much use for it as requesting a specific conversion.

        For finding the conversion, I'd look for a direct conversion, but if there isn't one, form the list of all possible conversions and then repeat for each of those:

        my @table = ( #### temperature [ c => f => sub { ($_[0] * 9/5) + 32 }, sub { ($_[0] - 32) * 5/9 }, ], [ c => k => sub { $_[0] + 273.16 }, sub { $_[0] - 273.16 }, ], #### mass/weight [ kg => g => 1000 ], [ floz => g => 30 ], [ lbs => kg => 0.4536 ], #### distance [ ft => m => 0.3048 ], [ ft => in => 12 ], [ in => cm => 2.54 ], [ m => cm => 100 ], [ yd => m => 0.9144 ], [ km => m => 1000 ], [ mile => km => 1.609347 ], ); my %table; for my $conv ( @table ) { my( $from, $to, $conv, $rev )= @$conv; if( exists $table{$from}{$to} ) { warn "Duplicate conversions to $to from $from.\n"; } $table{$from}{$to}= $conv; if( $rev ) { if( exists $table{$from}{$to} ) { warn "Duplicate conversions to $from from $to.\n"; } $table{$to}{$from}= $rev; } } # Handle reverse conversions when a better one isn't provided: for my $conv ( @table ) { my( $from, $to, $conv )= @$conv; if( ! ref($conv) && ! exists $table{$to}{$from} ) { $table{$to}{$from}= sub { $_[0] / $conv }; } } sub FindConversionPathTo { my( $dest, $source )= @_; my( @sol ); $source= { $source => "$source " }; while( ! @sol ) { for my $src ( keys %sources ) { if( exists $table{$src}{$dest} ) { $source{$src} .= "$dest "; push @sol, $src; } else { for my $dest ( keys %{$table{$src} ) { if( ! exists $source{$dest} ) { $source{$dest}= $source{$src} . "$dest "; } } } } } # Pick one of the solutions at random: (: return split ' ', (@source{@sol})[rand @sol]; }
        Untested (I hope you don't mind).

                - tye

        I haven't looked at your code in detail yet, nor tried to use the interface you've got already, specifically so that I would not be influenced.

        My first thought on how I would like to use a conversions module is that I would pass the source and destination units and it would create a named sub in my package namespace (like use constant does).

        In use, it might look something like this:

        # if (units) specified after text unit description # (which should understand most normal abbrevs.) # then if the input is a string it is inspected for units, # and the conversion done in the appropriate direction # If the input is purely numeric (if ONLY Perl_looks_like_number() was + accessible!) # then the conversion is in the direction specified by the order of de +claration time parameters. use Units::Convert FT_IN_2_MTRS => q[ft(')inches(") meters(m)]; print FT_IN_2_MTRS q{5'10"}; # prints '1.7773m' print FT_IN_2_MTRS 5.8333; # prints 1.77773 # No (units) specified on delclaration, input must be numeric, convers +ion works in 1 direction only. use Units::Convert MPH_2_KPH => q[mph kph]; print MPH_2_KPH 70; # prints 112 print MPH_2_KPH '70mph'; # Causes warn or die my @limits = qw(30 40 50 60 70); print "@{[ MPH_2_KPH @limits ]}"; # prints 50 64 80 96 112 # An extension would be for the user to supply a sprintf-style format +string # that is used for the formating/precision of the output. # Once we get string .v. numeric contexts, the sub could determine whe +n to append the units or not use Units::Convert CI_2_CC => 'inch^3(%.2f ci) cm^3(%.f cc)'; print CI_2_CC 500; # prints 8183 print CI_2_CC '500 ci'; # prints '8183 cc' # If an itermediate conversion is required, this could be specified on + the declaration # I'm not sure this is a good example, but it's the one that came to m +ind. use Units::Convert UK_2_METRIC_WEIGHTS => 'stones(st)_pounds(lbs) lbs +kilograms(%.1f kilos)'; print UK_2_METRIC_WEIGHTS '11st 7lbs'; # prints '73.2 kilos' print UK_2_METRIC_WEIGHTS 11.5; # prints 73.2 print UK_2_METRIC_WEIGHTS '11.5_'; # prints '73.2 kilos' maybe? # The presence of an underscore forces output formattting (if supplied +)?

        Final thought on the precision and under/overflow thing. Perhaps, if a flag is set, the routines could return BigInt/Floats if the standrad precisions will cause accuracy loss? I haven't thought that through, so I don't know what the implications are.

        Now I'll read your code and see if I'm completly off-base, but I like to look at things from my own perspective first when I can :^).

        If you decide not to go ahead with teh module, let me know and I will.


        Examine what is said, not who speaks.

        How about putting the info from @tables into an XML format and then read it in when the module is loaded? You could also add an option to update the XML tables from an internet site, if needed.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: sourcecode [id://219603]
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others cooling their heels in the Monastery: (5)
As of 2024-03-19 05:45 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found