File Coverage

blib/lib/Math/Calc/Units/Rank.pm
Criterion Covered Total %
statement 147 161 91.3
branch 31 50 62.0
condition 6 9 66.6
subroutine 14 14 100.0
pod 0 8 0.0
total 198 242 81.8


line stmt bran cond sub pod time code
1             package Math::Calc::Units::Rank;
2 1     1   5 use base 'Exporter';
  1         2  
  1         73  
3 1     1   4 use vars qw(@EXPORT_OK);
  1         6  
  1         44  
4 1     1   24 BEGIN { @EXPORT_OK = qw(choose_juicy_ones render render_unit); }
5              
6 1     1   5 use Math::Calc::Units::Convert qw(convert canonical);
  1         2  
  1         63  
7 1     1   5 use Math::Calc::Units::Convert::Multi qw(variants major_variants major_pref pref_score range_score get_class);
  1         2  
  1         70  
8 1     1   4 use strict;
  1         3  
  1         1738  
9              
10             # choose_juicy_ones : value -> ( value )
11             #
12             # Pick the best-sounding units for the given value, and compute the
13             # resulting magnitude and score. The total number returned is based on
14             # a magical formula that examines the rates of decay of the scores.
15             #
16             sub choose_juicy_ones {
17 12     12 0 20 my ($v, $options) = @_;
18              
19             # Collect the variants of the value, together with their scores.
20 12         42 my @variants = rank_variants($v, $options); # ( < {old=>new}, score > )
21              
22             # Remove duplicates
23 12         31 my %variants; # To remove duplicates: { id => [ {old=>new}, score ] }
24 12         59 for my $variant (@variants) {
25 126         132 my $id = join(";;", values %{ $variant->[0] });
  126         319  
26 126         272 $variants{$id} = $variant;
27             }
28              
29 12         22 my @options;
30 12         38 for my $variant (values %variants) {
31 126         206 my ($map, $score) = @$variant;
32 126         160 my %copy;
33 126         179 my ($magnitude, $units) = @$v;
34 126         353 while (my ($unit, $count) = each %$units) {
35 140         593 $copy{$map->{$unit}} = $count;
36             }
37 126         361 push @options, [ $score, convert($v, \%copy) ];
38             }
39              
40             # Pick up to five of the highest scores. If any score is less than
41             # 1/10 of the previous score, or 1/25 of the highest score, then
42             # don't bother returning it (or anything worse than it.)
43 12         26 my @juicy;
44             my $first;
45 0         0 my $prev;
46 12         46 foreach (sort { $b->[0] <=> $a->[0] } @options) {
  335         409  
47 48         69 my ($score, $val) = @$_;
48 48 100 100     187 last if (defined $prev && ($prev / $score) > 8);
49 45 100 100     170 last if (defined $first && ($first / $score) > 25);
50 42         76 push @juicy, $val;
51 42 100       92 $first = $score unless defined $first;
52 42         41 $prev = $score;
53 42 100       116 last if @juicy == 5;
54             }
55              
56 12         208 return @juicy;
57             }
58              
59             # rank_variants : -> ( < map, score > )
60             # where map : {original unit => new unit}
61             #
62             sub rank_variants {
63 12     12 0 21 my ($v, $options) = @_;
64              
65 12         38 $v = canonical($v);
66              
67 12         29 my ($mag, $count) = @$v;
68              
69 12         39 my @rangeable = grep { $count->{$_} > 0 } keys %$count;
  16         55  
70 12 50       45 if (@rangeable == 0) {
71 0         0 @rangeable = keys %$count;
72             }
73              
74 12         75 return rank_power_variants($mag, \@rangeable, $count, $options);
75             }
76              
77             sub choose_major {
78 4     4 0 10 my (@possibilities) = @_;
79 4         10 my @majors = map { [ major_pref($_), $_ ] } @possibilities;
  9         31  
80 4         24 return (sort { $a->[0] <=> $b->[0] } @majors)[-1]->[1];
  6         27  
81             }
82              
83             # rank_power_variants : value x [unit] x {unit=>power} x options ->
84             # ( )
85             #
86             # $top is the set of units that should be range checked.
87             #
88             sub rank_power_variants {
89 26     26 0 58 my ($mag, $top, $power, $options) = @_;
90              
91             # Recursive case: we have multiple units left, so pick one to be
92             # the "major" unit and select the best combination of the other
93             # units for each major variant on the major unit.
94              
95 26 100       76 if (keys %$power > 1) {
96             # Choose the major unit class (this will return the best
97             # result for each of the major variants)
98 4         17 my $major = choose_major(keys %$power);
99 4         13 my $majorClass = get_class($major);
100              
101 4         16 my %powerless = %$power;
102 4         9 delete $powerless{$major};
103              
104 4         6 my @ranked; # ( )
105              
106             # Try every combination of each major variant and the other units
107 4         18 foreach my $variant (major_variants($major, $options)) {
108 14         60 my $mult = $majorClass->simple_convert($variant, $major);
109 14         52 my $cval = $mag / $mult ** $power->{$major};
110              
111 14 50       41 print "\n --- for $variant ---\n" if $options->{verbose};
112 14         55 my @r = rank_power_variants($cval, $top, \%powerless, $options);
113 14 50       68 next if @r == 0;
114              
115 14         27 my $best = $r[0];
116 14         32 $best->[0]->{$major} = $variant; # Augment map
117             # Replace score with major pref
118 14         39 $best->[1] = pref_score($variant);
119 14         125 push @ranked, $best;
120             }
121              
122 4         33 return @ranked;
123             }
124              
125             # Base case: have a single unit left. Go through all possible
126             # variants of that unit.
127              
128 22 50       58 if (keys %$power == 0) {
129             # Special case: we don't have any units at all
130 0         0 return [ {}, 1 ];
131             }
132              
133 22         58 my $unit = (keys %$power)[0];
134 22         43 $power = $power->{$unit}; # Now it's just the power of this unit
135 22         71 my $class = get_class($unit);
136 22         87 my (undef, $canon) = $class->to_canonical($unit);
137 22         79 my $mult = $class->simple_convert($unit, $canon);
138 22         56 $mag *= $mult ** $power;
139              
140 22         30 my @choices;
141 22         40 my @subtop = grep { $_ ne $canon } @$top;
  23         84  
142 22         42 my $add_variant = (@subtop == @$top); # Flag: add $variant to @$top?
143              
144 22         91 foreach my $variant (variants($canon)) {
145             # Convert from $canon to $variant
146             # Input: 4000 / sec ; (canon=sec)
147             # 1 ms -> .001 sec ; (variant=ms)
148             # 4000 / (.001 ** -1) = 4 / ms
149 282         974 my $mult = $class->simple_convert($variant, $canon);
150 282         550 my $minimag = $mag / $mult ** $power;
151              
152 282         494 my @vtop = @subtop;
153 282 50       523 push @vtop, $variant if $add_variant;
154              
155 282         641 my $score = score($minimag, $variant, \@vtop);
156 282 50       677 printf "($mag $unit) score %.6f:\t $minimag $variant\n", $score
157             if $options->{verbose};
158 282         873 push @choices, [ $score, $variant ];
159             }
160              
161 22         172 @choices = sort { $b->[0] <=> $a->[0] } @choices;
  756         953  
162 22 50       74 return () if @choices == 0;
163              
164 22         49 return map { [ {$unit => $_->[1]}, $_->[0] ] } @choices;
  282         1146  
165             }
166              
167             # Return a string representing a given set of units. The input is a
168             # map from unit names to their powers (eg lightyears/sec/sec would be
169             # represented as { lightyears => 1, sec => -2 }); the output is a
170             # corresponding string such as "lightyears / sec**2".
171             sub render_unit {
172 4     4 0 8 my ($units, $options) = @_;
173              
174             # Positive powers just get appended together with spaces between
175             # them.
176 4         7 my $str = '';
177 4         25 while (my ($name, $power) = each %$units) {
178 2 50       17 if ($power > 0) {
179 2         7 $str .= get_class($name)->render_unit($name, $power, $options);
180 2         10 $str .= " ";
181             }
182             }
183 4         5 chop($str);
184              
185             # Negative powers will be placed after a "/" character, because
186             # they're in the denominator.
187 4         7 my $botstr = '';
188 4         13 while (my ($name, $power) = each %$units) {
189 4 50       23 if ($power < 0) {
190 0         0 $botstr .= get_class($name)->render_unit($name, -$power, $options);
191 0         0 $botstr .= " ";
192             }
193             }
194 4         12 chop($botstr);
195              
196             # Combine the numerator and denominator appropriately.
197 4 50       9 if ($botstr eq '') {
    0          
198 4         123 return $str;
199             } elsif ($botstr =~ /\s/) {
200 0         0 return "$str / ($botstr)";
201             } else {
202 0         0 return "$str / $botstr";
203             }
204             }
205              
206             # render : -> string
207             sub render {
208 50     50 0 81 my ($v, $options) = @_;
209 50         83 my ($mag, $units) = @$v;
210              
211             # No units
212 50 50       141 if (keys %$units == 0) {
213             # Special-case percentages
214 0         0 my $str = sprintf("%.4g", $mag);
215 0 0 0     0 if (($mag < 1) && ($mag >= 0.01)) {
216 0 0       0 if ($options->{abbreviate}) {
217 0         0 $str .= sprintf(" = %.4g percent", 100 * $mag);
218             } else {
219 0         0 $str .= sprintf(" = %.4g%%", 100 * $mag);
220             }
221             }
222 0         0 return $str;
223             }
224              
225 50         60 my @top;
226             my @bottom;
227 50         212 while (my ($name, $power) = each %$units) {
228 64 100       199 if ($power > 0) {
229 51         186 push @top, $name;
230             } else {
231 13         47 push @bottom, $name;
232             }
233             }
234              
235 50         61 my $str;
236 50 100       90 if (@top == 1) {
237 49         79 my ($name) = @top;
238 49         128 $str = get_class($name)->render($mag, $name, $units->{$name}, $options);
239 49         145 $str .= " ";
240             } else {
241 1         23 $str = sprintf("%.4g ", $mag);
242 1         3 foreach my $name (@top) {
243 2         6 $str .= get_class($name)->render_unit($name, $units->{$name}, $options);
244 2         5 $str .= " ";
245             }
246             }
247              
248 50 100       131 if (@bottom > 0) {
249 13         16 my $botstr;
250 13         25 foreach my $name (@bottom) {
251 13         38 $botstr .= get_class($name)->render_unit($name, -$units->{$name}, $options);
252 13         35 $botstr .= " ";
253             }
254 13         23 chop($botstr);
255              
256 13 50       26 if (@bottom > 1) {
257 0         0 $str .= "/ ($botstr) ";
258             } else {
259 13         31 $str .= "/ $botstr ";
260             }
261             }
262              
263 50         74 chop($str);
264 50         400 return $str;
265             }
266              
267             # max_range_score : amount x [ unit ] -> score
268             #
269             # Takes max score for listed units.
270             #
271             sub max_range_score {
272 282     282 0 361 my ($mag, $units) = @_;
273 282         348 my $score = 0;
274              
275 282         469 foreach my $name (@$units) {
276 282         767 my $uscore = range_score($mag, $name);
277 282 50       1192 $score = $uscore if $score < $uscore;
278             }
279              
280 282         564 return $score;
281             }
282              
283             # Arguments:
284             # $mag - The magnitude of the value (in the given unit)
285             # $unit - The unit to use to figure out what sounds best
286             # $top - ...I'll get back to you...
287             sub score {
288 282     282 0 449 my ($mag, $unit, $top) = @_;
289 282 100       699 my @rangeable = @$top ? @$top : ($unit);
290 282         744 my $pref = pref_score($unit);
291 282         641 my $range_score = max_range_score($mag, \@rangeable);
292 282         713 return $pref * $range_score;
293             }
294              
295             1;