File Coverage

blib/lib/Math/Units.pm
Criterion Covered Total %
statement 362 401 90.2
branch 230 328 70.1
condition 10 14 71.4
subroutine 20 22 90.9
pod 0 19 0.0
total 622 784 79.3


line stmt bran cond sub pod time code
1             package Math::Units;
2              
3             # Copyright 1997, 1998 Ken Fox
4              
5             # This program is free software; you can redistribute it and/or modify
6             # it under the terms of either:
7             #
8             # a) the GNU General Public License as published by the Free
9             # Software Foundation; either version 1, or (at your option) any
10             # later version, or
11             #
12             # b) the "Artistic License," the text of which is distributed with
13             # Perl 5. If you need a copy of this license, please write to
14             # me at <fox@vulpes.com> and I will be happy to send one.
15             #
16             # This program is distributed in the hope that it will be useful,
17             # but WITHOUT ANY WARRANTY; without even the implied warranty of
18             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See either
19             # the GNU General Public License or the Artistic License for more
20             # details.
21              
22             =head1 NAME
23              
24             Math::Units - Unit conversion
25              
26             =head1 SYNOPSIS
27              
28             use Math::Units qw(convert);
29              
30             my $out_value = convert($in_value, 'in unit', 'out unit');
31              
32             =head1 DESCRIPTION
33              
34             The Math::Units module converts a numeric value in one unit of measurement
35             to some other unit. The units must be compatible, i.e. length can not be
36             converted to volume. If a conversion can not be made an exception is thrown.
37              
38             A combination chaining and reduction algorithm is used to perform the most
39             direct unit conversion possible. Units may be written in several different
40             styles. An abbreviation table is used to convert from common long-form unit
41             names to the (more or less) standard abbreviations that the units module uses
42             internally. All multiplicative unit conversions are cached so that future
43             conversions can be performed very quickly.
44              
45             Too many units, prefixes and abbreviations are supported to list here. See
46             the source code for a complete listing.
47              
48             =head1 TODO
49              
50             I beleive this module has great potential, if you have any ideas or patches feel free to submit them to rt.cpan.org.
51              
52             'units' program test like 'gunits'
53              
54             other tests
55              
56             POD about what units/abbr/etc can be used with the function
57              
58             general cleanup
59              
60             Mr. Fox's original TODO:
61              
62             1. There should be a set of routines for adding new unit formulas,
63             reductions and conversions.
64              
65             2. Some conversions can be automatically generated from a reduction. (This
66             has to be done carefully because conversions are bi-directional while
67             reductions *must* be consistently uni-directional.)
68              
69             3. It would be nice to simplify the default conversions using the
70             yet-to-be-written solution to #2.
71              
72             4. There are many units (several in the GNU unit program for example) that
73             aren't defined here. Since I was (un)fortunately born in the U.S., I
74             have a(n) (in)correct belief of what the standard units are. Please let
75             me know if I've messed anything up!
76              
77             =head1 EXAMPLES
78              
79             print "5 mm == ", convert(5, 'mm', 'in'), " inches\n";
80             print "72 degrees Farenheit == ", convert(72, 'F', 'C'), " degrees Celsius\n";
81             print "1 gallon == ", convert(1, 'gallon', 'cm^3'), " cubic centimeters\n";
82             print "4500 rpm == ", convert(4500, 'rpm', 'Hz'), " Hertz\n";
83              
84             =cut
85              
86 2     2   43932 use strict;
  2         5  
  2         85  
87 2     2   9 use vars qw($VERSION @ISA @EXPORT_OK);
  2         4  
  2         207  
88              
89             $VERSION = 1.3;
90              
91             require Exporter;
92             @ISA = qw(Exporter);
93             @EXPORT_OK = qw(convert print_conversion);
94              
95 2     2   10 use Carp;
  2         8  
  2         8408  
96              
97             # Prefixes are used to alter the magnitude of a unit. They
98             # can *not* be chained together to form compound prefixes.
99             # (For special cases of compound prefixes, you can enter an
100             # abbreviation that rewrites the compound prefix to a single
101             # prefix of the right magnitude.)
102              
103             my %prefixes = (
104             'T' => 1e12,
105             'G' => 1e9,
106             'M' => 1e6,
107             'k' => 1000,
108             'h' => 100,
109             'da' => 10,
110             'd' => .1,
111             'c' => .01,
112             'm' => .001,
113             'µ' => 1e-6,
114             'n' => 1e-9,
115             'dn' => 1e-10,
116             'p' => 1e-12,
117             'f' => 1e-15
118             );
119              
120             # Formulas and reductions are carefully chosen expressions that
121             # define a unit in terms of other units (and constants). The
122             # unit conversion algorithm always applies a formula definition,
123             # but only uses a reduction as a last ditch effort to make the
124             # conversion. The reason for this is that reductions can lead
125             # to very long chains of unit conversions. However, in most
126             # conversions a single factor can be used which will skip the
127             # entire reduction process (and improve accuracy besides).
128             #
129             # Always express a unit in terms of more fundamental units.
130             # Loops are not detected and will cause the conversion algorithm
131             # to hang. (Adding units is intended to be easy, but not
132             # trivial.)
133             #
134             # See below for conversion examples.
135              
136             my %formulas = (
137             'are' => '100 m^2', # as in hectare
138             'l' => 'm^3/1000', # liter
139             'tonne' => '1000 kg', # metric ton
140             'N' => 'kg m/s^2', # newton
141             'dyn' => 'cm gram/s^2',
142             'Pa' => 'N/m^2', # pascal
143             'bar' => '1e5 Pa',
144             'barye' => 'dyne/cm^2',
145             'kine' => 'cm/s',
146             'bole' => 'g kine',
147             'pond' => 'gram gee',
148             'glug' => 'pond s^2/cm',
149             'J' => 'N m', # joule
150             'W' => 'J/s', # watt
151             'gee' => '9.80665 m/s^2', # Earth gravity
152             'atm' => '101325 Pa', # Earth atmosphere
153             'Hg' => '13.5951 pond/cm^3', # mercury (used in: inches Hg)
154             'water' => 'pond/cm^3', # water (used in: inches water)
155             'mach' => '331.46 m/s', # speed of sound
156             'coulomb' => 'A s',
157             'V' => 'W/A', # volt
158             'ohm' => 'V/A',
159             'siemens' => 'A/V',
160             'farad' => 'coulomb/V',
161             'Wb' => 'V s', # weber
162             'henry' => 'Wb/A',
163             'tesla' => 'Wb/m^2',
164             'Hz' => 'cycle/s', # hertz
165              
166             'lbf' => 'lb gee', # pounds of force
167             'tonf' => 'ton gee', # tons of force
168              
169             'duty' => 'ft lbf',
170             'celo' => 'ft/s^2',
171             'jerk' => 'ft/s^3',
172              
173             'slug' => 'lbf s^2/ft',
174             'reyn' => 'psi sec', # viscosity
175              
176             'psi' => 'lbf/in^2', # pounds per square inch
177             'tsi' => 'tonf/in^2', # tons per square inch
178              
179             'ouncedal' => 'oz ft/s^2', # force which accelerates an ounce at 1 ft/s^2
180             'poundal' => 'lb ft/s^2', # same thing for a pound
181             'tondal' => 'ton ft/s^2', # and for a ton
182              
183             'hp' => '550 ft lbf/s', # horse power
184             'nauticalmile' => '1852 m',
185             'mil' => '.001 in'
186             );
187              
188             # The base units are:
189             #
190             # m .............. meter (length) meter^2 (area) meter^3 (volume)
191             # g .............. gram (mass)
192             # s .............. second (time)
193             # deg ............ degree (angular measure)
194             # A .............. ampere (current)
195             # C .............. degrees Celsius (temperature)
196             # Cd ............. Celsius degrees (temperature change)
197              
198             my %reductions = (
199             'in' => '0.0254 m', # inches
200             'pnt' => 'in/72', # PostScript points
201             'ft' => '12 in', # feet
202             'yd' => '3 ft', # yards
203             'mi' => '5280 ft', # miles
204             'kip' => '1000 lbf', # kilo pounds
205              
206             'barrel' => '42 gal', # barrels
207             'gal' => '231 in^3', # gallons
208             'qt' => 'gal/4', # quarts
209             'pt' => 'qt/2', # pints
210             'gill' => 'pt/4', # gills
211             'floz' => 'pt/16', # fluid ounces
212              
213             'Fd' => '1.8 Cd', # Farenheit degrees (change)
214             'Kd' => 'Cd', # Kelvins (change)
215              
216             'min' => '60 s', # minutes
217             'hr' => '60 min', # hours
218             'day' => '24 hr', # days
219             'wk' => '7 day', # weeks
220              
221             'lb' => '453.59237 g', # pounds
222             'oz' => 'lb/16', # ounces
223             'dr' => 'oz/16', # drams
224             'gr' => 'lb/7000', # grains
225             'ton' => '2000 lb', # tons
226              
227             'cycle' => '360 deg', # complete revolution = 1 cycle
228             'rad' => '180 deg/3.14159265358979323846', # radians
229             'grad' => '9 deg/10', # gradians
230              
231             'troypound' => '5760 gr', # troy pound
232             'troyounce' => 'troypound/12', # troy ounce
233             'pennyweight' => 'troyounce/20', # penny weight
234              
235             'carat' => '0.2 gm' # carat
236             );
237              
238             # Abbreviations are simple text conversions that convert a pattern
239             # expression (i.e. a Perl regular expression) into a different form.
240             # Usually these convert from the long, spelled out form of a unit
241             # to the unit's abbreviated form. Plural forms are also eliminated.
242             # A few small bows to standard spoken units are also available.
243             #
244             # Examples:
245             #
246             # meters => m
247             # kilometers => k-meters => k-m
248             # grams/cc => grams/cm^3 => g/cm^3
249             # meters per second => m/s
250             # cubic inches => cu-in
251             # feet squared => ft^2
252             # hectares => h-are
253             #
254             # Abbreviation substitutions are applied IN THE GIVEN ORDER to the unit
255             # until no more abbreviations match. As in the formula and
256             # reduction expressions, be careful to avoid rewriting loops. Also,
257             # be aware that longer abbreviations should appear first to avoid
258             # the possibility of an unintended rewrite.
259              
260             my @abbreviations = (
261             '\bper\b' => '\/',
262             '\bsq(uare)?\s+' => 'sq,',
263             '\bcu(bic)?\s+' => 'cu,',
264             '\s+squared\b' => '^2',
265             '\s+cubed\b' => '^3',
266              
267             '\bmicrons?\b' => 'µ,m',
268              
269             '\bdecinano-?' => 'dn,',
270             '\btera-?' => 'T,',
271             '\bgiga-?' => 'G,',
272             '\bmega-?' => 'M,',
273             '\bkilo-?' => 'k,',
274             '\bhecto-?' => 'h,',
275             '\bdeka-?' => 'da,',
276             '\bdeca-?' => 'da,',
277             '\bdeci-?' => 'd,',
278             '\bcenti-?' => 'c,',
279             '\bmilli-?' => 'm,',
280             '\bmicro-?' => 'µ,',
281             '\bnano-?' => 'n,',
282             '\bpico-?' => 'p,',
283             '\bfemto-?' => 'f,',
284              
285             '\bdn-' => 'dn,',
286             '\bT-' => 'T,',
287             '\bG-' => 'G,',
288             '\bM-' => 'M,',
289             '\bk-' => 'k,',
290             '\bh-' => 'h,',
291             '\bda-' => 'da,',
292             '\bda-' => 'da,',
293             '\bd-' => 'd,',
294             '\bc-' => 'c,',
295             '\bm-' => 'm,',
296             '\bµ-' => 'µ,',
297             '\bn-' => 'n,',
298             '\bp-' => 'p,',
299             '\bf-' => 'f,',
300              
301             '\b[Rr][Pp][Mm]\b' => 'cycle\/min',
302             '\bhz\b' => 'Hz',
303              
304             '\b[Cc]elsius\b' => 'C',
305             '\b[Ff]arenheit\b' => 'F',
306             '\b[Kk]elvins?\b' => 'K',
307             '\bdegs?\s+C\b' => 'C',
308             '\bdegs?\s+F\b' => 'F',
309             '\bC\s+change\b' => 'Cd',
310             '\bF\s+change\b' => 'Fd',
311             '\bK\s+change\b' => 'Kd',
312              
313             '\bdegs\b' => 'deg',
314             '\bdegrees?\b' => 'deg',
315             '\brads\b' => 'rad',
316             '\bradians?\b' => 'rad',
317             '\bgrads\b' => 'grad',
318             '\bgradians?\b' => 'grad',
319              
320             '\bangstroms?\b' => 'dn,m',
321             '\bcc\b' => 'cm^3',
322             '\bhectares?\b' => 'h,are',
323             '\bmils?\b' => 'm,in',
324             'amperes?\b' => 'A',
325             'amps?\b' => 'A',
326             'days\b' => 'day',
327             'drams?\b' => 'dr',
328             'dynes?\b' => 'dyn',
329             'feet\b' => 'ft',
330             'foot\b' => 'ft',
331             'gallons?\b' => 'gal',
332             'gm\b' => 'g',
333             'grams?\b' => 'g',
334             'grains?\b' => 'gr',
335             'hours?\b' => 'hr',
336             'inch(es)?\b' => 'in',
337             'joules?\b' => 'J',
338             'lbs\b' => 'lb',
339             'lbm\b' => 'lb',
340             'liters?\b' => 'l',
341             'meters?\b' => 'm',
342             'miles?\b' => 'mi',
343             'minutes?\b' => 'min',
344             'newtons?\b' => 'N',
345             'ounces?\b' => 'oz',
346             'pascals?\b' => 'Pa',
347             'pints?\b' => 'pt',
348             'points?\b' => 'pnt',
349             'pounds?\b' => 'lb',
350             'quarts?\b' => 'qt',
351             'seconds?\b' => 's',
352             'secs?\b' => 's',
353             'watts?\b' => 'W',
354             'weeks?\b' => 'wk',
355             'yards?\b' => 'yd'
356             );
357              
358             # The conversion table *must* define unit conversion in terms
359             # of the base units, not in terms of units with prefixes. This
360             # table will be used to generate the initial conversion factors
361             # used in simple unit to unit conversion. Inverse factors will
362             # be automatically generated where possible. As new unit
363             # conversion paths are discovered, the combined conversion
364             # factors will be added to the table. No conversion factors
365             # should be entered for units that are defined in the formula
366             # table. (Many or all of the reductions will be redundantly
367             # defined in the conversions table. The reductions table uses
368             # a more general format which makes automatic conversion a
369             # bit tricky.)
370             #
371             # The entire purpose of the conversion table is to allow a
372             # more direct unit conversion path. The reduction algorithm
373             # will always find a conversion (if one exists) but it may
374             # use many more multiplies than if the conversion table is
375             # used directly.
376             #
377             # Here is an example contrasting the two approaches. Given
378             # the following base facts:
379             #
380             # reductions: in -> m, ft -> in, yd -> ft
381             # conversions: in <-> m, ft <-> in, yd <-> ft
382             #
383             # convert feet to yards:
384             #
385             # by reduction: ft -> in -> m <- in <- ft <- yd
386             # by conversion: ft -> yd
387             #
388             # This demonstrates that fewer intermediate multiplies are
389             # performed in the direct conversion approach over the reduction
390             # approach. However, the following problem can not be easily
391             # solved in the direct conversion approach:
392             #
393             # convert square meters to inch * feet:
394             #
395             # by reduction: m^2 -> area <- m m <- m feet <- inch feet
396             # by conversion: m^2 -> no match!
397             #
398             # Conversion can't solve this problem unless it first breaks up
399             # square meters into meter * meter. Simple in this case, but very
400             # hard to generalize.
401             #
402             # In summary, the direct conversion system uses fewer intermediate
403             # conversions for better accuracy (and possibly performance but
404             # that isn't really an issue). The reduction system is more
405             # general in that it can solve conversion problems that the direct
406             # conversion system can't.
407             #
408             # Examples:
409             #
410             # m -> in is solved by m -> in
411             # in -> m is solved by in -> m (inverses are automatically generated)
412             # qt -> ft^3 is solved by qt -> gal -> in^3 -> ft^3
413             # l -> ft^3 is solved by l -> m^3 -> in^3 -> ft^3
414             # K -> F is solved by K -> C -> F
415              
416             my %conversions = (
417             'in,m' => 0.0254,
418             'in,pnt' => 72,
419             'ft,in' => 12,
420             'yd,ft' => 3,
421             'mi,ft' => 5280,
422              
423             'barrel,gal' => 42,
424             'gal,in^3' => 231,
425             'gal,qt' => 4,
426             'qt,pt' => 2,
427             'pt,floz' => 16,
428             'pt,gill' => 4,
429              
430             'C,F' => sub { $_[0] * 1.8 + 32 },
431             'F,C' => sub { ( $_[0] - 32 ) / 1.8 },
432             'K,C' => sub { $_[0] - 273.15 },
433             'C,K' => sub { $_[0] + 273.15 },
434              
435             'Cd,Fd' => 1.8,
436             'Kd,Cd' => 1,
437              
438             'wk,day' => 7,
439             'day,hr' => 24,
440             'hr,min' => 60,
441             'min,s' => 60,
442              
443             'dollar,cent' => 100,
444              
445             'lb,g' => 453.59237,
446             'lb,oz' => 16,
447             'lb,gr' => 7000,
448             'oz,dr' => 16,
449             'ton,lb' => 2000,
450              
451             'cycle,deg' => 360,
452             'rad,deg' => 180 / 3.14159265358979323846,
453             'grad,deg' => 9 / 10,
454              
455             'troypound,gr' => 5760,
456             'troypound,troyounce' => 12,
457             'troyounce,pennyweight' => 20,
458              
459             'carat,gm' => .2
460             );
461              
462             my $factors_computed = 0; # have the base conversion factors been computed?
463             my %factor = (); # conversion factors for base units
464             my %conversion_history = (); # history of conversion factors for raw unit strings
465              
466             sub register_factor {
467 45     45 0 63 my ( $u1, $u2, $f ) = @_;
468              
469 45         98 $factor{$u1}{$u2} = $f;
470 45 100       200 $factor{$u2}{$u1} = 1 / $f if ( ref($f) ne "CODE" );
471             }
472              
473             sub print_unit($\%) {
474 0     0 0 0 my ( $prefix, $u_group ) = @_;
475 0         0 my ( $num_str, $den_str, $u, $dim );
476              
477 0         0 $num_str = "";
478 0         0 $den_str = "";
479              
480 0         0 while ( ( $u, $dim ) = each %{$u_group} ) {
  0         0  
481 0 0       0 if ( $u eq "1" ) { $prefix *= $dim }
  0 0       0  
    0          
    0          
    0          
482 0         0 elsif ( $dim > 1 ) { $num_str .= "$u^$dim " }
483 0         0 elsif ( $dim == 1 ) { $num_str .= "$u " }
484 0         0 elsif ( $dim == -1 ) { $den_str .= "$u " }
485 0         0 elsif ( $dim < -1 ) { $den_str .= join( "", $u, "^", -$dim, " " ) }
486             }
487              
488 0 0       0 $num_str .= "$prefix " if ( $prefix != 1 );
489              
490 0         0 chop $num_str;
491 0         0 chop $den_str;
492              
493 0 0       0 $num_str = "1" if ( !$num_str );
494              
495 0         0 print $num_str;
496 0 0       0 print "/", $den_str if ($den_str);
497 0         0 print "\n";
498             }
499              
500             my $current_prefix;
501             my %current_group;
502              
503             sub merge_simple_unit {
504 305     305 0 400 my ( $prefix, $u, $dim ) = @_;
505              
506 305 100       563 if ( $dim > 1 ) { $current_prefix *= $prefix**$dim }
  36         1176  
507 305 100       586 if ( $dim == 1 ) { $current_prefix *= $prefix }
  184 100       249  
    100          
508 49         75 elsif ( $dim == -1 ) { $current_prefix /= $prefix }
509 36         63 elsif ( $dim < -1 ) { $current_prefix /= $prefix**-$dim }
510              
511 305 100       679 if ( $u ne "1" ) {
512 266 100       585 if ( defined( $current_group{$u} ) ) { $current_group{$u} += $dim }
  19         26  
513 247         378 else { $current_group{$u} = $dim }
514              
515 266 100       857 delete $current_group{$u} if ( $current_group{$u} == 0 );
516             }
517             }
518              
519             sub reduce_simple_unit {
520 330     330 0 412 my ( $u, $dim, $apply_reductions ) = @_;
521 330         273 my ($p);
522              
523 330 100       649 if ( defined( $formulas{$u} ) ) {
524 45         93 reduce_unit( $formulas{$u}, $dim, $apply_reductions );
525 45         126 return;
526             }
527              
528 285 100 100     977 if ( $apply_reductions && defined( $reductions{$u} ) ) {
    100          
529 16         27 reduce_unit( $reductions{$u}, $dim, $apply_reductions );
530 16         36 return;
531             }
532             elsif ( defined( $factor{$u} ) ) {
533 236         403 merge_simple_unit( 1, $u, $dim );
534 236         597 return;
535             }
536              
537 33         123 foreach $p ( keys %prefixes ) {
538 370 100       3998 if ( $u =~ /^$p,?(.+)/ ) {
539 35 100       115 if ( defined( $formulas{$1} ) ) {
540 3         11 merge_simple_unit( $prefixes{$p}, "1", $dim );
541 3         8 reduce_unit( $formulas{$1}, $dim, $apply_reductions );
542 3         13 return;
543             }
544 32 50 33     127 if ( $apply_reductions && defined( $reductions{$1} ) ) {
    100          
545 0         0 merge_simple_unit( $prefixes{$p}, "1", $dim );
546 0         0 reduce_unit( $reductions{$1}, $dim, $apply_reductions );
547 0         0 return;
548             }
549             elsif ( defined( $factor{$1} ) ) {
550 30         94 merge_simple_unit( $prefixes{$p}, $1, $dim );
551 30         144 return;
552             }
553             }
554             }
555              
556 0         0 Carp::croak "unknown unit '$u' used";
557             }
558              
559             sub reduce_unit {
560 228     228 0 270 my ( $u_group, $dim, $apply_reductions ) = @_;
561 228         234 my ($u);
562              
563 228         218 foreach $u ( keys %{$u_group} ) {
  228         577  
564 366 100       574 if ( $u eq "1" ) {
565 36         73 merge_simple_unit( $u_group->{$u}, $u, $dim );
566             }
567             else {
568 330         763 reduce_simple_unit( $u, $dim * $u_group->{$u}, $apply_reductions );
569             }
570             }
571             }
572              
573             sub canonicalize_unit_list (\@$$) {
574 300     300 0 378 my ( $units, $u_group, $denomenator ) = @_;
575 300         262 my ( $u, $dim );
576              
577 300         271 foreach $u ( @{$units} ) {
  300         534  
578 369 100       633 next if ( !$u );
579              
580 367 100       1113 if ( $u =~ s/\^(.+)$// ) { # unit of higher dimension, e.g. "cm^3"
    50          
    100          
581 57         99 $dim = $1;
582             }
583             elsif ( $u =~ /^sq,(.+)/ ) { # square unit, e.g. "sq-in"
584 0         0 $u = $1;
585 0         0 $dim = 2;
586             }
587             elsif ( $u =~ /^cu,(.+)/ ) { # cubic unit, e.g. "cu-in"
588 2         6 $u = $1;
589 2         3 $dim = 3;
590             }
591             else {
592 308         575 $dim = 1;
593             }
594              
595 367 100       635 $dim = -$dim if ($denomenator);
596              
597 367 100       1176 if ( $u =~ /^-?\d+(?:\.\d+)?(?:e-?\d+)?$/ ) {
598 44 100       100 if ( $dim == 1 ) { $dim = $u }
  30 100       35  
599 13         25 elsif ( $dim == -1 ) { $dim = 1 / $u }
600 1         3 else { $dim = $u**$dim }
601 44         56 $u = "1";
602             }
603              
604 367 100       676 if ( defined( $u_group->{$u} ) ) {
605 11 100       23 if ( $u eq "1" ) { $u_group->{$u} *= $dim }
  2         10  
606 9         30 else { $u_group->{$u} += $dim }
607             }
608             else {
609 356         1321 $u_group->{$u} = $dim;
610             }
611             }
612             }
613              
614             sub canonicalize_unit_string ($$) {
615 225     225 0 290 my ( $units, $u_group ) = @_;
616 225         230 my ( $num, $den, $u, @units );
617              
618 225         6584 substitute_abbreviations( \$units );
619 225         438 $units =~ tr [*][ ];
620 225         485 $units =~ s/\s*\^\s*/\^/g;
621 225         284 $units =~ s/-\s*(\D)/ $1/g;
622              
623 225 100       601 if ( $units =~ m|^([^/]*)/(.*)| ) {
624 75         147 $num = $1;
625 75         110 $den = $2;
626 75         81 $den =~ tr [/][ ];
627             }
628             else {
629 150         215 $num = $units;
630 150         176 $den = "";
631             }
632              
633 225         616 @units = split( /\s+/, $num );
634 225 50       461 if ( scalar @units ) {
635 225         494 canonicalize_unit_list( @units, $u_group, 0 );
636             }
637              
638 225         570 @units = split( /\s+/, $den );
639 225 100       462 if ( scalar @units ) {
640 75         135 canonicalize_unit_list( @units, $u_group, 1 );
641             }
642              
643 225         495 $u_group;
644             }
645              
646             sub reduce_toplevel_unit ($\%) {
647 152     152 0 212 my ( $units, $u_group ) = @_;
648              
649 152         270 canonicalize_unit_string( $units, $u_group );
650              
651 152         165 $current_prefix = 1;
652 152         275 %current_group = ();
653              
654 152         278 reduce_unit( $u_group, 1, 0 );
655              
656 152         313 %{$u_group} = %current_group;
  152         460  
657              
658 152         297 $current_prefix;
659             }
660              
661             sub finish_reducing_toplevel_unit (\%) {
662 12     12 0 17 my ($u_group) = @_;
663              
664 12         11 $current_prefix = 1;
665 12         19 %current_group = ();
666              
667 12         18 reduce_unit( $u_group, 1, 1 );
668              
669 12         22 %{$u_group} = %current_group;
  12         27  
670              
671 12         22 $current_prefix;
672             }
673              
674             sub get_factor {
675 200     200 0 275 my ( $u1, $u2 ) = @_;
676              
677 200 100       539 ( $u1 eq $u2 ) ? 1 : $factor{$u1}{$u2};
678             }
679              
680             my $combined_f;
681             my $combined_f_useless;
682              
683             sub attempt_direct_conversion {
684 165     165 0 232 my ( $value, $u1, $u1_dim, $u2, $u2_dim ) = @_;
685 165         159 my ($f);
686              
687 165 100       283 if ( $u1_dim != $u2_dim ) {
688 48 100       119 $u1 = "$u1^$u1_dim" if ( $u1_dim != 1 );
689 48 100       106 $u2 = "$u2^$u2_dim" if ( $u2_dim != 1 );
690 48         57 $u1_dim = 1;
691             }
692              
693 165 100       243 if ( $u1_dim < 0 ) {
694 35         36 $u1_dim = -$u1_dim;
695 35         53 $f = get_factor( $u2, $u1 );
696             }
697             else {
698 130         215 $f = get_factor( $u1, $u2 );
699             }
700              
701 165 100       361 if ( defined($f) ) {
702 100 100       287 if ( ref($f) eq "CODE" ) {
    100          
703 18         37 $value = &$f( $value, $u1_dim );
704 18         30 $combined_f_useless = 1;
705             }
706             elsif ( $f != 1 ) {
707 38 100       88 $f = $f**$u1_dim if ( $u1_dim > 1 ); # $u1_dim is non-negative
708 38         36 $value *= $f;
709 38         45 $combined_f *= $f;
710             }
711              
712 100         195 return $value;
713             }
714              
715 65         86 undef;
716             }
717              
718             my %tmp_u_history;
719             my @tmp_u_path;
720             my @tmp_dim_path;
721              
722             my $tmp_value;
723             my $tmp_uX;
724             my $tmp_uX_dim;
725              
726             sub apply_factor_chain {
727 14     14 0 18 my $chained_f = 1.0;
728 14         15 my $chained_f_useless = 0;
729              
730 14         19 push @tmp_u_path, $tmp_uX;
731 14         22 my $final = scalar(@tmp_u_path) - 1;
732 14         20 my $original_value = $tmp_value;
733              
734 14         18 my ( $i, $f, $dim );
735              
736 14         36 for ( $i = 0; $i < $final; ++$i ) {
737 35         42 $dim = $tmp_dim_path[$i];
738              
739 35         69 $f = get_factor( $tmp_u_path[$i], $tmp_u_path[ $i + 1 ] );
740              
741 35 100       71 if ( defined($f) ) {
742 31 100       87 if ( ref($f) eq "CODE" ) {
    50          
743 6 50       14 if ( $dim < 0 ) {
744 0         0 $dim = -$dim;
745 0         0 $f = get_factor( $tmp_u_path[ $i + 1 ], $tmp_u_path[$i] );
746             }
747 6         26 $tmp_value = &$f( $tmp_value, $dim );
748 6         15 $chained_f_useless = 1;
749             }
750             elsif ( $f != 1 ) {
751 25 100       72 $f = $f**$dim if ( $dim != 1 ); # $dim can be either negative or positive
752 25         26 $tmp_value *= $f;
753 25         61 $chained_f *= $f;
754             }
755             }
756             }
757              
758 14 100       25 if ($chained_f_useless) {
759 3         6 $combined_f_useless = 1;
760             }
761             else {
762 11         16 my $u1 = $tmp_u_path[0];
763 11 50 33     65 if ( exists( $factor{$u1} ) && exists( $factor{$tmp_uX} ) ) {
764 11         17 my $u1_dim = $tmp_dim_path[0];
765              
766 11 100       28 $u1 = "$u1^$u1_dim" if ( $u1_dim != 1 );
767 11 100       22 $tmp_uX = "$tmp_uX^$tmp_uX_dim" if ( $tmp_uX_dim != 1 );
768              
769 11         27 register_factor( $u1, $tmp_uX, $chained_f );
770 11         17 $combined_f *= $chained_f;
771             }
772             }
773              
774 14         81 die "OK\n";
775             }
776              
777             sub breadth_first_factor_search {
778 236     236 0 296 my ( $level, $u, $dim ) = @_;
779 236         214 my $attempts = 0;
780              
781 331         495 SEARCH:
782             {
783 236         213 $tmp_u_history{$u} = 1;
784              
785 331         308 ++$attempts;
786              
787 331         381 push @tmp_u_path, $u;
788 331         357 push @tmp_dim_path, $dim;
789              
790 331 100       485 if ( $level == 0 ) {
791 188 100 100     555 if ( $dim == $tmp_uX_dim && defined( $factor{$u}{$tmp_uX} ) ) {
792 14         29 apply_factor_chain();
793             }
794             }
795             else {
796 143         151 my $child;
797 143         126 foreach $child ( keys %{ $factor{$u} } ) {
  143         385  
798 291 100       572 if ( !defined( $tmp_u_history{$child} ) ) {
799 165         293 breadth_first_factor_search( $level - 1, $child, $dim );
800             }
801             }
802             }
803              
804 300 100       561 if ( $attempts < 2 ) {
805 209 100       263 if ( $dim == 1 ) {
806 115 100       229 if ( $u =~ /^([^^]+)\^(.+)/ ) {
807 1         4 $u = $1;
808 1         2 $dim = $2;
809              
810 1 50       5 redo SEARCH if ( !defined( $tmp_u_history{$u} ) );
811             }
812             }
813             else {
814 94         157 $u = "$u^$dim";
815 94         107 $dim = 1;
816              
817 94 50       208 redo SEARCH if ( !defined( $tmp_u_history{$u} ) );
818             }
819             }
820             }
821              
822 205         381 while ( $attempts-- > 0 ) {
823 296         262 pop @tmp_u_path;
824 296         806 pop @tmp_dim_path;
825             }
826             }
827              
828             sub attempt_indirect_conversion {
829 24     24 0 39 my ( $input_value, $u1, $u1_dim, $uX, $uX_dim ) = @_;
830              
831 24         24 $tmp_value = $input_value;
832 24         32 $tmp_uX = $uX;
833 24         28 $tmp_uX_dim = $uX_dim;
834              
835 24         25 eval {
836 24         23 my $level;
837 24         67 for ( $level = 0; $level < 4; ++$level ) {
838 71         125 %tmp_u_history = ();
839 71         94 @tmp_u_path = ();
840 71         79 @tmp_dim_path = ();
841              
842 71         126 breadth_first_factor_search( $level, $u1, $u1_dim );
843             }
844             };
845              
846 24 100       72 return undef if ( $@ ne "OK\n" );
847              
848 14         23 return $tmp_value;
849             }
850              
851             sub perform_unit_conversion ($\%\%) {
852 82     82 0 101 my ( $value, $u1_group, $u2_group ) = @_;
853 82         101 my ( $u1, $u1_dim );
854 0         0 my ( $u2, $u2_dim );
855 0         0 my ($new_value);
856              
857 82         183 DIRECT_UNIT_CONVERSION:
858 82         83 foreach $u1 ( keys %{$u1_group} ) {
859 122         157 $u1_dim = $u1_group->{$u1};
860              
861 122         109 foreach $u2 ( keys %{$u2_group} ) {
  122         206  
862 165         194 $u2_dim = $u2_group->{$u2};
863              
864 165         285 $new_value = attempt_direct_conversion( $value, $u1, $u1_dim, $u2, $u2_dim );
865              
866 165 100       350 if ( defined($new_value) ) {
867 100         104 $value = $new_value;
868 100         149 delete $u1_group->{$u1};
869 100         110 delete $u2_group->{$u2};
870 100         250 next DIRECT_UNIT_CONVERSION;
871             }
872             }
873             }
874              
875             INDIRECT_UNIT_CONVERSION:
876 82         131 foreach $u1 ( keys %{$u1_group} ) {
  82         183  
877 22         28 $u1_dim = $u1_group->{$u1};
878              
879 22         24 foreach $u2 ( keys %{$u2_group} ) {
  22         33  
880 24         33 $u2_dim = $u2_group->{$u2};
881              
882 24         61 $new_value = attempt_indirect_conversion( $value, $u1, $u1_dim, $u2, $u2_dim );
883              
884 24 100       68 if ( defined($new_value) ) {
885 14         21 $value = $new_value;
886 14         28 delete $u1_group->{$u1};
887 14         15 delete $u2_group->{$u2};
888 14         43 next INDIRECT_UNIT_CONVERSION;
889             }
890             }
891             }
892              
893 82 100 100     114 if ( scalar keys %{$u1_group} || scalar keys %{$u2_group} ) {
  82         233  
  77         159  
894 6         7 $tmp_value = $value;
895 6         26 die "REDUCE\n";
896             }
897              
898 76         175 $value;
899             }
900              
901             sub compute_base_factors {
902              
903             # register all of the direct unit-to-unit conversion factors
904              
905 1     1 0 2 my ( $pair, $f, $u1, $u2 );
906 1         8 while ( ( $pair, $f ) = each %conversions ) {
907 34         65 ( $u1, $u2 ) = split( /,/, $pair );
908 34         54 register_factor( $u1, $u2, $f );
909             }
910              
911             # build a fast pattern substitution function by eval'ing a
912             # subroutine generated by concatenating all the abbreviation
913             # substitution commands together.
914              
915 1         3 my $code = "sub substitute_abbreviations { my(\$units) = \@_; SUBST: {\n";
916 1         3 my ( $pattern, $subst );
917              
918 1         2 my $i = 0;
919 1         4 while ( $i < scalar @abbreviations ) {
920 88         100 $pattern = $abbreviations[ $i++ ];
921 88         103 $subst = $abbreviations[ $i++ ];
922              
923 88         226 $code .= " redo SUBST if (\$\$units =~ s/$pattern/$subst/g);\n";
924             }
925              
926 1         2 $code .= "} }";
927              
928 1 100   225 0 2172 eval $code;
  225 50       306  
  225 100       233  
  266 100       663  
  264 50       1007  
  264 100       566  
  262 50       569  
  261 50       470  
  261 50       454  
  256 50       455  
  256 50       501  
  256 50       455  
  256 50       439  
  256 50       542  
  256 50       4840  
  256 50       649  
  256 100       593  
  256 50       408  
  256 50       447  
  256 50       487  
  254 50       413  
  254 50       602  
  254 50       425  
  254 50       1078  
  254 50       415  
  254 50       428  
  254 50       458  
  254 50       415  
  254 50       397  
  254 50       538  
  254 50       612  
  254 100       503  
  254 50       417  
  254 50       621  
  254 50       782  
  253 50       446  
  253 100       636  
  253 50       469  
  253 50       399  
  253 50       619  
  250 50       616  
  250 50       394  
  250 50       454  
  250 50       426  
  250 50       415  
  250 50       395  
  250 50       409  
  250 50       469  
  250 50       609  
  250 50       461  
  250 50       468  
  250 50       425  
  250 100       411  
  250 100       415  
  250 100       430  
  250 50       441  
  248 50       484  
  246 50       401  
  245 50       409  
  245 50       408  
  245 100       450  
  245 100       463  
  245 50       404  
  245 50       420  
  244 100       393  
  243 100       396  
  243 50       591  
  243 50       452  
  242 100       449  
  240 50       425  
  240 50       383  
  240 50       440  
  237 50       371  
  237 100       402  
  237 50       466  
  237 50       398  
  237 50       419  
  236 100       389  
  236 50       350  
  236 50       389  
  236 50       372  
  235 100       374  
  235 50       394  
  235 100       407  
  235 100       384  
  234 50       368  
  234 50       370  
  232 50       413  
  225         386  
  225         541  
  225         714  
929              
930             # simplify all the formulas and reductions up front so that
931             # multiple rewrite passes aren't required during unit expansion
932              
933 1         30 foreach $u1 ( keys %formulas ) {
934 43         101 $formulas{$u1} = canonicalize_unit_string( $formulas{$u1}, {} );
935             }
936              
937 1         13 foreach $u1 ( keys %reductions ) {
938 30         62 $reductions{$u1} = canonicalize_unit_string( $reductions{$u1}, {} );
939             }
940              
941             # mark this function completed because it only runs once
942              
943 1         6 $factors_computed = 1;
944             }
945              
946             sub print_conversion {
947 0     0 0 0 my ( $value, $u1, $u2 ) = @_;
948 0         0 my $my_result = Convert( $value, $u1, $u2 );
949              
950 0         0 print "$value $u1 == $my_result $u2\n";
951 0         0 $my_result;
952             }
953              
954             sub convert {
955 213     213 0 109671 my ( $value, $u1, $u2 ) = @_;
956 213         261 my ( %u1_group, %u2_group );
957 0         0 my ( $u1_prefix, $u2_prefix );
958 0         0 my ($f);
959              
960 213 50       497 return ($value) if ( $u1 eq $u2 );
961 213 100       919 if ( defined( $f = $conversion_history{$u1}{$u2} ) ) {
962 137         535 return ( $value * $f );
963             }
964              
965 76 100       254 if ( !$factors_computed ) {
966 1         4 compute_base_factors();
967             }
968              
969 76         174 $u1_prefix = reduce_toplevel_unit( $u1, %u1_group );
970 76         143 $u2_prefix = reduce_toplevel_unit( $u2, %u2_group );
971              
972 76         191 $combined_f = $u1_prefix / $u2_prefix;
973 76         84 $combined_f_useless = 0;
974 76         95 $value *= $combined_f;
975              
976 76         94 eval { $value = perform_unit_conversion( $value, %u1_group, %u2_group ); };
  76         189  
977              
978 76 100       159 if ($@) {
979 6 50       16 if ( $@ eq "REDUCE\n" ) {
980 6         13 $u1_prefix = finish_reducing_toplevel_unit(%u1_group);
981 6         12 $u2_prefix = finish_reducing_toplevel_unit(%u2_group);
982              
983 6         9 $f = $u1_prefix / $u2_prefix;
984              
985 6 50       13 if ( !$combined_f_useless ) {
986 6         7 $combined_f *= $f;
987             }
988              
989 6         8 $value = $tmp_value * $f;
990              
991 6         5 eval { $value = perform_unit_conversion( $value, %u1_group, %u2_group ); };
  6         24  
992              
993 6 50       16 if ($@) {
994 0 0       0 if ( $@ eq "REDUCE\n" ) {
995 0         0 Carp::croak "conversion of unit '$u1' to '$u2' failed (incompatible units?)";
996             }
997             else {
998 0         0 Carp::croak $@;
999             }
1000             }
1001             }
1002             else {
1003 0         0 Carp::croak "impossible! $@";
1004             }
1005             }
1006              
1007 76 100       140 if ( !$combined_f_useless ) {
1008 55         185 $conversion_history{$u1}{$u2} = $combined_f;
1009             }
1010              
1011 76         395 $value;
1012             }
1013              
1014             1;