File Coverage

blib/lib/Class/Measure.pm
Criterion Covered Total %
statement 206 214 96.2
branch 76 94 80.8
condition 34 56 60.7
subroutine 28 28 100.0
pod 9 9 100.0
total 353 401 88.0


line stmt bran cond sub pod time code
1             package Class::Measure;
2 4     4   705388 use 5.008001;
  4         27  
3 4     4   22 use strict;
  4         8  
  4         89  
4 4     4   20 use warnings;
  4         8  
  4         184  
5             our $VERSION = '0.09';
6              
7             =encoding utf8
8              
9             =head1 NAME
10              
11             Class::Measure - Create, compare, and convert units of measurement.
12              
13             =head1 SYNOPSIS
14              
15             See L for some examples.
16              
17             =head1 DESCRIPTION
18              
19             This is a base class that is inherited by the Class::Measure
20             classes. This distribution comes with the class L.
21              
22             The classes L, L,
23             L, L,
24             and L are planned and will be added soon.
25              
26             The methods described here are available in all Class::Measure classes.
27              
28             =cut
29              
30 4     4   33 use Carp qw( croak );
  4         18  
  4         213  
31 4     4   27 use Scalar::Util qw(looks_like_number);
  4         7  
  4         426  
32              
33             use overload
34 4         50 '+'=>\&_ol_add, '-'=>\&_ol_sub,
35             '*'=>\&_ol_mult, '/'=>\&_ol_div,
36 4     4   28 '""'=>\&_ol_str;
  4         20  
37              
38             our $type_convs = {};
39             our $type_paths = {};
40             our $type_aliases = {};
41              
42             =head1 METHODS
43              
44             =head2 new
45              
46             my $m = new Class::Measure::Length( 1, 'inch' );
47              
48             Creates a new measurement object. You must pass an initial
49             measurement and default unit.
50              
51             In most cases the measurement class that you are using
52             will export a method to create new measurements. For
53             example L exports the
54             C method.
55              
56             =cut
57              
58             sub new {
59 14     14 1 6991 my $class = shift;
60              
61 14         29 my $unit = pop;
62 14 100 66     89 $unit = $type_aliases->{$class}->{$unit} || $unit if $unit;
63              
64             croak 'Unknown Class::Measure unit'
65 14 100 100     481 unless $unit and $type_convs->{$class}->{$unit};
66              
67 11         69 return bless {
68             unit => $unit,
69             values => { $unit => shift },
70             }, $class;
71             }
72              
73             =head2 unit
74              
75             my $unit = $m->unit();
76              
77             Returns the object's default unit.
78              
79             =cut
80              
81             sub unit {
82 24     24 1 34 my $self = shift;
83 24         97 return $self->{unit};
84             }
85              
86             =head2 set_unit
87              
88             $m->set_unit( 'feet' );
89              
90             Sets the default unit of the measurement.
91              
92             =cut
93              
94             sub set_unit {
95 21     21 1 78 my $self = shift;
96 21         51 my $unit = $self->_unalias( shift );
97 21         55 $self->_conv( $unit );
98 21         33 $self->{unit} = $unit;
99 21         47 return;
100             }
101              
102             =head2 value
103              
104             my $yards = $m->value('yards');
105             my $val = $m->value();
106             print "$m is the same as $val when in a string\n";
107              
108             Retrieves the value of the measurement in the
109             default unit. You may specify a unit in which
110             case the value is converted to the unit and returned.
111              
112             This method is also used to handle overloading of
113             stringifying the object.
114              
115             =cut
116              
117             sub value {
118 54     54 1 146 my $self = shift;
119 54 100       113 return $self->_conv(shift) if @_;
120 52         203 return $self->{values}->{$self->{unit}};
121             }
122              
123             =head2 set_value
124              
125             my $m = length( 0, 'inches' );
126             $m->set_value( 12 ); # 12 inches.
127             $m->set_value( 1, 'foot' ); # 1 foot.
128              
129             Sets the measurement in the default unit. You may
130             specify a new default unit as well.
131              
132             =cut
133              
134             sub set_value {
135 18     18 1 27 my $self = shift;
136 18 100       49 $self->{unit} = $self->_unalias(pop @_) if( @_>1 );
137 18         56 $self->{values} = { $self->{unit} => shift };
138 18         28 return;
139             }
140              
141             =head2 reg_units
142              
143             Class::Measure::Length->reg_units(
144             'inch', 'foot', 'yard',
145             );
146              
147             Registers one or more units for use in the specified
148             class. Units should be in the singular, most common,
149             form.
150              
151             =cut
152              
153             sub reg_units {
154 8     8 1 464 my $self = shift;
155 8   33     50 my $class = ref($self) || $self;
156 8   100     58 my $convs = $type_convs->{$class} ||= {};
157 8         23 foreach my $unit (@_){
158 30 50       82 croak('This unit has already been defined') if $convs->{$unit};
159 30         71 $convs->{$unit} = {};
160              
161 4     4   2121 no strict 'refs';
  4         7  
  4         1447  
162 30         66 *{"${class}::${unit}"} = _build_unit_sub( $unit );
  30         167  
163             }
164 8         26 return;
165             }
166              
167             sub _build_unit_sub {
168 30     30   52 my ($unit) = @_;
169              
170             return sub{
171 8     8   41 my $self = shift;
172 8 50       29 return $self->set_value( shift(), $unit ) if @_;
173 8         33 return $self->_conv( $unit );
174 30         135 };
175             }
176              
177             =head2 units
178              
179             my @units = Class::Measure::Length->units();
180              
181             Returns a list of all registered units.
182              
183             =cut
184              
185             sub units {
186 1     1 1 115 my $self = shift;
187 1   33     11 my $class = ref($self) || $self;
188 1         3 return keys(%{$type_convs->{$class}});
  1         26  
189             }
190              
191             =head2 reg_aliases
192              
193             Class::Measure::Length->reg_aliases(
194             ['feet','ft'] => 'foot',
195             ['in','inches'] => 'inch',
196             'yards' => 'yard'
197             );
198              
199             Register alternate names for units. Expects two
200             arguments per unit to alias. The first argument
201             being the alias (scalar) or aliases (array ref), and
202             the second argument being the unit to alias them to.
203              
204             =cut
205              
206             sub reg_aliases {
207 6     6 1 17 my $self = shift;
208 6   33     72 my $class = ref($self) || $self;
209 6 50       82 croak('Wrong number of arguments (must be a multiple of two)') if( (@_+0) % 2 );
210 6   100     52 my $aliases = $type_aliases->{$class} ||= {};
211 6         19 while( @_ ){
212 26 100       63 my @aliases = ( ref($_[0]) ? @{shift()} : shift );
  10         22  
213 26         45 my $unit = shift;
214 26 50       61 croak('Unknown unit "'.$unit.'" to alias to') unless( defined $type_convs->{$class}->{$unit} );
215 26         36 foreach my $alias (@aliases){
216 54 50       102 if( defined $aliases->{$alias} ){ croak('Alias already in use'); }
  0         0  
217 54         132 $aliases->{$alias} = $unit;
218              
219 4     4   31 no strict 'refs';
  4         8  
  4         6269  
220 54         71 *{"${class}::${alias}"} = *{"${class}::${unit}"};
  54         240  
  54         120  
221             }
222             }
223 6         20 return;
224             }
225              
226             =head2 reg_convs
227              
228             Class::Measure::Length->reg_convs(
229             12, 'inches' => 'foot',
230             'yard' => '3', 'feet'
231             );
232              
233             Registers a unit conversion. There are three distinct
234             ways to specify a new conversion. Each requires three
235             arguments.
236              
237             $count1, $unit1 => $unit2
238             $unit1 => $count2, $unit2
239              
240             These first two syntaxes create automatic reverse conversions
241             as well. So, saying there are 12 inches in a foot implies
242             that there are 1/12 feet in an inch.
243              
244             $unit1 => $unit2, $sub
245              
246             The third syntax accepts a subroutine as the last argument
247             the subroutine will be called with the value of $unit1 and
248             it's return value will be assigned to $unit2. This
249             third syntax does not create a reverse conversion automatically.
250              
251             =cut
252              
253             sub reg_convs {
254 9     9 1 314 my $self = shift;
255 9 50       32 croak('Wrong number of arguments (must be a multiple of three)') if( (@_+0) % 3 );
256 9   33     45 my $class = ref($self) || $self;
257 9         26 while(@_){
258 29         63 my($from,$to,$conv);
259             # First check for coderef to avoid seeing units as number in that case:
260 29 100       122 if( ref($_[2]) eq 'CODE' ){
    100          
    50          
261 1         9 ($from,$to,$conv) = splice(@_,0,3);
262             }elsif( looks_like_number($_[0]) ){
263 8         24 ($conv,$from,$to) = splice(@_,0,3);
264 8         19 $conv = 1 / $conv;
265             }elsif( looks_like_number($_[1]) ){
266 20         40 ($from,$conv,$to) = splice(@_,0,3);
267             }else{
268 0         0 croak('Invalid arguments');
269             }
270 29         88 $from = $self->_unalias($from);
271 29         56 $to = $self->_unalias($to);
272 29   50     59 my $units = $type_convs->{$class} ||= {};
273 29   50     69 $units->{$from} ||= {};
274 29         59 $units->{$from}->{$to} = $conv;
275 29 100       46 unless( ref $conv ){
276 28   50     71 $units->{$to} ||= {};
277 28         78 $units->{$to}->{$from} = 1/$conv;
278             }
279             }
280 9         35 $type_paths->{$class} = {};
281 9         24 return;
282             }
283              
284             sub _unalias {
285 163     163   220 my $self = shift;
286 163   66     356 my $class = ref($self) || $self;
287 163         207 my $unit = shift;
288 163 100       435 return $unit if( defined $type_convs->{$class}->{$unit} );
289 31   33     72 return $type_aliases->{$class}->{$unit} || croak('Unknown unit or alias "'.$unit.'"');
290             }
291              
292             sub _conv {
293 31     31   52 my $self = shift;
294 31   33     66 my $class = ref($self) || $self;
295 31         62 my $unit = $self->_unalias( shift );
296 31 100       214 return $self->{values}->{$unit} if( defined $self->{values}->{$unit} );
297 22         59 my $path = $self->_path( $self->unit, $unit );
298 22 50       52 croak('Unable to find an appropriate conversion path') unless( $path );
299 22         38 my $units = $type_convs->{$class};
300 22         74 my $prev_unit = shift( @$path );
301 22         83 my $value = $self->value;
302 22         103 foreach $unit (@$path){
303 60         111 my $conv = $units->{$prev_unit}->{$unit};
304 60 100       112 if( ref($conv) ){
305 1         3 $value = &{$conv}( $value, $prev_unit, $unit );
  1         3  
306             }else{
307 59         103 $value = $value * $units->{$prev_unit}->{$unit};
308             }
309 60         117 $self->{values}->{$unit} = $value;
310 60         99 $prev_unit = $unit;
311             }
312 22         83 return $value;
313             }
314              
315             sub _path {
316 25     25   318 my $self = shift;
317 25         47 my $from = $self->_unalias(shift);
318 25         62 my $to = $self->_unalias(shift);
319 25   66     60 my $class = ref($self) || $self;
320 25         66 my $key = "$from-$to";
321 25   50     75 my $paths = $type_paths->{$class} ||= {};
322 25 100       62 if( defined $paths->{$key} ){ return [@{$paths->{$key}}]; }
  3         5  
  3         8  
323              
324 22   50     63 my $units = $type_convs->{$class} ||= {};
325 22         33 my $path;
326 22         57 foreach (1..10){
327 87         165 $path = _find_path( $from, $to, $units, $_ );
328 87 100       191 last if( $path );
329             }
330 22 50       50 return 0 if(!$path);
331 22         67 $paths->{$key} = $path;
332 22         66 return [@$path];
333             }
334              
335             sub _find_path {
336 594     594   1106 my($level,$to,$units) = splice(@_,0,3);
337 594 100       1128 unless( ref $level ){ $level=[$level]; }
  87         150  
338 594 50       964 my $max_depth = ( @_ ? shift : 12 );
339 594 100       859 my $depth = ( @_ ? shift : 0 );
340 594 100       3850 my $path = ( @_ ? shift : [] );
341 594         786 my $next_level = {};
342              
343 594         948 foreach my $unit (@$level){
344 1422 100       2399 if($unit eq $to){
345 22         40 push @$path, $unit;
346 22         65 return $path;
347             }
348             }
349              
350 572 100       1263 return 0 if( $depth+1 == $max_depth );
351 253         304 $depth ++;
352              
353 253         377 foreach my $unit (@$level){
354 507         750 push @$path, $unit;
355 507 100       604 if(_find_path( [keys %{$units->{$unit}}], $to, $units, $max_depth, $depth, $path )){
  507         1426  
356 65         116 $depth --;
357 65         189 return $path;
358             }
359 442         821 pop @$path;
360             }
361              
362 188         240 $depth --;
363 188         512 return 0;
364             }
365              
366             sub _ol_add {
367 4     4   55 my($one,$two,$opt) = @_;
368 4 100       11 if($opt){ my $tmp=$one; $one=$two; $two=$tmp; }
  1         3  
  1         3  
  1         3  
369 4 100 100     34 if( ref($two) and ref($one) ){
    100          
    50          
370 1 50       4 croak('You may only add numbers or measurements of the same class') if( ref($one) ne ref($two) );
371 1         4 $one->set_value( $one->value + $two->value($one->unit) );
372 1         25 return $one;
373             }elsif( ref $one ){
374 2         11 $one->set_value( $one->value + $two );
375 2         5 return $one;
376             }elsif( ref $two ){
377 1         5 $two->set_value( $one + $two->value );
378 1         3 return $two;
379             }
380 0         0 return;
381             }
382              
383             sub _ol_sub {
384 4     4   31 my($one,$two,$opt) = @_;
385 4 100       14 if($opt){ my $tmp=$one; $one=$two; $two=$tmp; }
  1         3  
  1         3  
  1         2  
386 4 100 100     23 if( ref($two) and ref($one) ){
    100          
    50          
387 1 50       4 croak('You may only subtract numbers or measurements of the same class') if( ref($one) ne ref($two) );
388 1         3 $one->set_value( $one->value - $two->value($one->unit) );
389 1         3 return $one;
390             }elsif( ref $one ){
391 2         5 $one->set_value( $one->value - $two );
392 2         6 return $one;
393             }elsif( ref $two ){
394 1         8 $two->set_value( $one - $two->value );
395 1         3 return $two;
396             }
397 0         0 return;
398             }
399              
400             sub _ol_mult {
401 4     4   12 my($one,$two,$opt) = @_;
402 4 100       14 if($opt){ my $tmp=$one; $one=$two; $two=$tmp; }
  1         2  
  1         3  
  1         2  
403 4 50 66     22 if( ref($two) and ref($one) ){
    100          
    50          
404 0         0 croak('You cannot multiply two measure classes');
405             }elsif( ref $one ){
406 3         11 $one->set_value( $one->value * $two );
407 3         20 return $one;
408             }elsif( ref $two ){
409 1         4 $two->set_value( $one * $two->value );
410 1         4 return $two;
411             }
412 0         0 return;
413             }
414              
415             sub _ol_div {
416 3     3   8 my($one,$two,$opt) = @_;
417 3 100       9 if($opt){ my $tmp=$one; $one=$two; $two=$tmp; }
  1         2  
  1         2  
  1         3  
418 3 50 66     19 if( ref($two) and ref($one) ){
    100          
    50          
419 0         0 croak('You cannot divide one measure class by another');
420             }elsif( ref $one ){
421 2         5 $one->set_value( $one->value / $two );
422 2         5 return $one;
423             }elsif( ref $two ){
424 1         3 $two->set_value( $one / $two->value );
425 1         3 return $two;
426             }
427 0         0 return;
428             }
429              
430             sub _ol_str {
431 1     1   3 my $self = shift;
432 1         4 return $self->value;
433             }
434              
435             1;
436             __END__