Beefy Boxes and Bandwidth Generously Provided by pair Networks
Problems? Is your data what you think it is?

Re: The Definitive Unit Conversion Script

by princepawn (Parson)
on Dec 13, 2002 at 17:24 UTC ( #219655=note: print w/replies, xml ) Need Help??

in reply to The Definitive Unit Conversion Script

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

  • Comment on Re: The Definitive Unit Conversion Script

Replies are listed 'Best First'.
Re^2: The Definitive Unit Conversion Script
by Aristotle (Chancellor) on Dec 13, 2002 at 19:22 UTC

    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.

        Well, I'm definitely not going to go forward with the interface you described. :) That's entirely different from what I'm trying to achieve. Although I guess a ::Subs add-on module could handle that well enough. I don't see the point in a UK_2_METRIC routine if you still have to specify the units though.

        Makeshifts last the longest.

      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.

        Sounds like an interesting idea, but on second thought, that requires the module to be XML aware and network aware. I don't think these things belong into it; if I go there, I rather offer a clean interface for updating the @table and let the module user do the rest. Adding a sample script to the distribution that uses that interface to import an XML table might be a nice idea though.

        Update: Actually, that sounds like a very nice idea. The sample script could import currency data from somewhere. Of course, it should carry a disclaimer, since currency calculations should not be done using floating point math when actual money depends on them, but it would still be a very nifty demonstration of the module's design.

        Makeshifts last the longest.

Log In?

What's my password?
Create A New User
Node Status?
node history
Node Type: note [id://219655]
and all is quiet...

How do I use this? | Other CB clients
Other Users?
Others rifling through the Monastery: (10)
As of 2017-03-28 11:02 GMT
Find Nodes?
    Voting Booth?
    Should Pluto Get Its Planethood Back?

    Results (330 votes). Check out past polls.