File Coverage

blib/lib/Math/Calc/Units/Convert/Base.pm
Criterion Covered Total %
statement 94 98 95.9
branch 45 52 86.5
condition 11 14 78.5
subroutine 19 20 95.0
pod 0 18 0.0
total 169 202 83.6


line stmt bran cond sub pod time code
1             package Math::Calc::Units::Convert::Base;
2 1     1   6 use strict;
  1         1  
  1         1261  
3              
4             sub major_pref {
5 5     5 0 21 return 0;
6             }
7              
8             # major_variants :void -> ( unit name )
9             #
10             # Return the set of prefix-free variants of the class that you might
11             # want to use as a final result. So for time, this would return
12             # "second" and "year" but not "millisecond" or "gigayear".
13             #
14             # Only this base class will ever use the unit passed in, and that's
15             # because this base class is used for unknown units. Subclasses should
16             # return a list of variants regardless of what is passed as $unit.
17             #
18             sub major_variants {
19 2     2 0 4 my ($self, $unit) = @_;
20 2         8 return $unit;
21             }
22              
23             # singular : unitName -> unitName
24             #
25             # Convert a possibly pluralized unit name to the uninflected singular
26             # form of the same name.
27             #
28             # Example: inches -> inch
29             # Example: inch -> inch
30             #
31             # I suppose I ought to optionally allow Lingu::EN::Inflect or whatever
32             # it's called.
33             #
34             sub singular {
35 251     251 0 323 my $self = shift;
36 251         535 local $_ = shift;
37              
38 251 100       1188 return $_ unless /s$/;
39 51 100       275 return $1 if /^(.*[^e])s$/; # doesn't end in es => just chop off the s
40 24 100       109 return $1 if /^(.*(ch|sh))es$/; # eg inches -> inch
41 19 100       73 return $1 if /^(.*[aeiou][^aeiou]e)s$/; # scales -> scale
42 17         33 chop; return $_; # Chop off the s
  17         58  
43             }
44              
45             sub unit_map {
46 38     38 0 173 return {};
47             }
48              
49             sub variants {
50 17     17 0 28 my ($self, $base) = @_;
51 17         51 my $map = $self->unit_map();
52 17         74 return ($base, keys %$map);
53             }
54              
55             # same : unit x unit -> boolean
56             #
57             # Returns whether the two units match, where a unit is a string (eg
58             # "week") and a power. So "days" and "toothpicks" are not the same,
59             # nor are feet and square feet.
60             sub same {
61 26     26 0 37 my ($self, $u, $v) = @_;
62 26 50       74 return 0 if keys %$u != keys %$v;
63 26         79 while (my ($name, $power) = each %$u) {
64 27 100       163 return 0 if ! exists $v->{$name};
65 15 50       63 return 0 if $v->{$name} != $power;
66             }
67 14         75 return 1;
68             }
69              
70             # simple_convert : unitName x unitName -> multiplier
71             #
72             # Second unit name must be canonical.
73             #
74             sub simple_convert {
75 665     665 0 1232 my ($self, $from, $to) = @_;
76 665 100       1840 return 1 if $from eq $to;
77              
78 451         1342 my $map = $self->unit_map();
79 451   66     1605 my $w = $map->{$from} || $map->{lc($from)};
80 451 100       2174 if (! $w) {
81 189         555 $from = $self->singular($from);
82 189   66     925 $w = $map->{$from} || $map->{lc($from)};
83             }
84 451 100       1242 return if ! $w; # Failed
85              
86             # We might have only gotten one step closer (hour -> minute -> sec)
87 266 100       584 if ($w->[1] ne $to) {
88 99         398 my $submult = $self->simple_convert($w->[1], $to);
89 99 50       201 return if ! defined $submult;
90 99         316 return $w->[0] * $submult;
91             } else {
92 167         497 return $w->[0];
93             }
94             }
95              
96             # to_canonical : unitName -> amount x unitName
97             #
98             # Convert the given unit to the canonical unit for this class, along
99             # with a conversion factor.
100             #
101             sub to_canonical {
102 223     223 0 314 my ($self, $unitName) = @_;
103 223         1406 my $canon = $self->canonical_unit();
104 223 100       507 if ($canon) {
105 210         625 my $mult = $self->simple_convert($unitName, $canon);
106 210 100       818 return if ! defined $mult;
107 83         299 return ($mult, $canon);
108             } else {
109 13         28 return (1, $self->singular($unitName));
110             }
111             }
112              
113             # canonical_unit : void -> unit name
114             #
115             # Return the canonical unit for this class.
116             #
117             sub canonical_unit {
118 13     13 0 18 return;
119             }
120              
121             sub abbreviated_canonical_unit {
122 0     0 0 0 my ($self) = @_;
123 0         0 return $self->canonical_unit;
124             }
125              
126             #################### RANKING, SCORING, DISPLAYING ##################
127              
128             # spread : magnitude x base unit x units to spread over
129             # -> ( )
130             #
131             # @$units MUST BE SORTED, LARGER UNITS FIRST!
132             #
133             my $THRESHOLD = 0.01;
134             sub spread {
135 13     13 0 29 my ($self, $mag, $base, $start, $units) = @_;
136 13 50       34 die if $mag < 0; # Must be given a positive value!
137 13 50       31 return [ 0, $base ] if $mag == 0;
138              
139 13         20 my $orig = $mag;
140              
141 13         17 my @desc;
142 13         16 my $started = 0;
143 13         26 foreach my $unit (@$units) {
144 86 100       168 $started = 1 if $unit eq $start;
145 86 100       159 next unless $started;
146              
147 31 100       75 last if ($mag / $orig) < $THRESHOLD;
148 19         63 my $mult = $self->simple_convert($unit, $base);
149 19         38 my $n = int($mag / $mult);
150 19 100       41 next if $n == 0;
151 17         24 $mag -= $n * $mult;
152 17         56 push @desc, [ $n, $unit ];
153             }
154              
155 13         55 return @desc;
156             }
157              
158             # range_score : amount x unitName -> score
159             #
160             # Returns 1 if the value is in range for the unit, 0.1 if the value is
161             # infinitely close to being in range, and decaying to 0.001 as the
162             # value approaches infinitely far away from the range.
163             #
164             # For the outside of range values, I convert to log space (so 1/400 is
165             # just as far away from 1 as 400 is). I then treat the allowed range
166             # as a one standard deviation wide segment of a normal distribution,
167             # and use appropriate modifiers to make the result range from 0.001 to
168             # 0.1.
169             #
170             # The above formula was carefully chosen from thousands of
171             # possibilities, by picking things at random and scribbling them down
172             # on a piece of paper, then pouring sparkling apple cider all over and
173             # using the one that was still readable.
174             #
175             # Ok, not really. Just pretend that I went to that much trouble.
176             #
177             sub range_score {
178 282     282 0 423 my ($self, $val, $unitName) = @_;
179 282         770 my $ranges = $self->get_ranges();
180 282   66     1226 my $range = $ranges->{$unitName} || $ranges->{default};
181              
182             # Return 1 if it's in range
183 282 100       698 if ($val >= $range->[0]) {
184 142 100 100     648 if (! defined $range->[1] || ($val <= $range->[1])) {
185 39         134 return 1;
186             }
187             }
188              
189 243         1776 $val = _sillylog($val);
190              
191 243         427 my $r0 = _sillylog($range->[0]);
192 243         294 my $r1;
193 243 100       438 if (defined $range->[1]) {
194 240         431 $r1 = _sillylog($range->[1]);
195             } else {
196 3         6 $r1 = 4;
197             }
198              
199 243         443 my $width = $r1 - $r0;
200 243         348 my $mean = ($r0 + $r1) / 2;
201 243         344 my $stddev = $width / 2;
202              
203 243         330 my $n = ($val - $mean) / $stddev; # Normalized value
204              
205 243         249 our $mulconst;
206 243   100     441 $mulconst ||= 0.999 * exp(1/8);
207              
208 243         1126 return 0.001 + $mulconst * exp(-$n**2/2);
209             }
210              
211             # Infinity-free logarithm
212             sub _sillylog {
213 726     726   850 my $x = shift;
214 726 50       2068 return log($x) if $x;
215 0         0 return log(1e-50);
216             }
217              
218             # pref_score : unitName -> score
219             #
220             # Maps a unit name (eg week) to a score. Higher scores are more likely
221             # to be chosen.
222             sub pref_score {
223 296     296 0 410 my ($self, $unitName) = @_;
224 296         824 my $prefs = $self->get_prefs();
225 296         531 my $specific = $prefs->{$unitName};
226 296 100       1426 return defined($specific) ? $specific : $prefs->{default};
227             }
228              
229             # get_prefs : void -> { unit name => score }
230             #
231             # Return a map of unit names to their score, higher scores meaning
232             # they're more likely to be chosen.
233             sub get_prefs {
234 4     4 0 11 return { default => 0.1 };
235             }
236              
237             sub get_ranges {
238 2     2 0 8 return { default => [ 1, undef ] };
239             }
240              
241             # render_unit : unit name x power -> descriptive string
242             #
243             # Return a rendering of the given unit name and a power to raise the
244             # unit to.
245             #
246             # Example: render_unit("weeks", 2) produces "weeks**2".
247             #
248             sub render_unit {
249 66     66 0 107 my ($self, $name, $power, $options) = @_;
250 66 50       134 if ($power == 1) {
251 66         292 return $name;
252             } else {
253 0         0 return "$name**$power";
254             }
255             }
256              
257             # render : value x name x power -> descriptive string
258             #
259             # Return a rendering of the given value with the given units.
260             #
261             # Example: render(4.8, "weeks", -1) produces "4.8 weeks**-1".
262             #
263             sub render {
264 49     49 0 96 my ($self, $val, $name, $power, $options) = @_;
265 49         559 return sprintf("%.5g ",$val).$self->render_unit($name, $power, $options);
266             }
267              
268             sub construct {
269 14     14 0 47 return;
270             }
271              
272             1;