File Coverage

blib/lib/Class/Measure.pm
Criterion Covered Total %
statement 186 188 98.9
branch 57 70 81.4
condition 30 56 53.5
subroutine 28 28 100.0
pod 9 9 100.0
total 310 351 88.3


line stmt bran cond sub pod time code
1             package Class::Measure;
2 4     4   624821 use 5.008001;
  4         27  
3 4     4   16 use strict;
  4         6  
  4         72  
4 4     4   16 use warnings;
  4         9  
  4         144  
5             our $VERSION = '0.10';
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   18 use Carp qw( croak );
  4         5  
  4         180  
31 4     4   18 use Scalar::Util qw( blessed looks_like_number );
  4         7  
  4         321  
32              
33             use overload
34 4         72 '+'=>\&_ol_add, '-'=>\&_ol_sub,
35             '*'=>\&_ol_mult, '/'=>\&_ol_div,
36 4     4   23 '""'=>\&_ol_str;
  4         17  
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 29     29 1 5624 my $class = shift;
60              
61 29         45 my $unit = pop;
62 29 100 66     107 $unit = $type_aliases->{$class}->{$unit} || $unit if $unit;
63              
64             croak 'Unknown Class::Measure unit'
65 29 100 100     386 unless $unit and $type_convs->{$class}->{$unit};
66              
67 26         126 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 37     37 1 47 my $self = shift;
83 37         70 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 59 my $self = shift;
96 21         32 my $unit = $self->_unalias( shift );
97 21         43 $self->_conv( $unit );
98 21         26 $self->{unit} = $unit;
99 21         29 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 122 my $self = shift;
119 54 100       105 return $self->_conv(shift) if @_;
120 52         122 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 3     3 1 22 my $self = shift;
136 3 50       13 $self->{unit} = $self->_unalias(pop @_) if( @_>1 );
137 3         9 $self->{values} = { $self->{unit} => shift };
138 3         5 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 397 my $self = shift;
155 8   33     46 my $class = ref($self) || $self;
156 8   100     31 my $convs = $type_convs->{$class} ||= {};
157 8         21 foreach my $unit (@_){
158 30 50       59 croak('This unit has already been defined') if $convs->{$unit};
159 30         72 $convs->{$unit} = {};
160              
161 4     4   1697 no strict 'refs';
  4         7  
  4         1174  
162 30         46 *{"${class}::${unit}"} = _build_unit_sub( $unit );
  30         119  
163             }
164 8         18 return;
165             }
166              
167             sub _build_unit_sub {
168 30     30   43 my ($unit) = @_;
169              
170             return sub{
171 8     8   25 my $self = shift;
172 8 50       21 return $self->set_value( shift(), $unit ) if @_;
173 8         26 return $self->_conv( $unit );
174 30         87 };
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 77 my $self = shift;
187 1   33     9 my $class = ref($self) || $self;
188 1         2 return keys(%{$type_convs->{$class}});
  1         16  
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 15 my $self = shift;
208 6   33     20 my $class = ref($self) || $self;
209 6 50       26 croak('Wrong number of arguments (must be a multiple of two)') if( (@_+0) % 2 );
210 6   100     36 my $aliases = $type_aliases->{$class} ||= {};
211 6         18 while( @_ ){
212 26 100       50 my @aliases = ( ref($_[0]) ? @{shift()} : shift );
  10         16  
213 26         34 my $unit = shift;
214 26 50       48 croak('Unknown unit "'.$unit.'" to alias to') unless( defined $type_convs->{$class}->{$unit} );
215 26         41 foreach my $alias (@aliases){
216 54 50       85 if( defined $aliases->{$alias} ){ croak('Alias already in use'); }
  0         0  
217 54         98 $aliases->{$alias} = $unit;
218              
219 4     4   26 no strict 'refs';
  4         5  
  4         4549  
220 54         52 *{"${class}::${alias}"} = *{"${class}::${unit}"};
  54         184  
  54         93  
221             }
222             }
223 6         14 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 282 my $self = shift;
255 9 50       24 croak('Wrong number of arguments (must be a multiple of three)') if( (@_+0) % 3 );
256 9   33     29 my $class = ref($self) || $self;
257 9         28 while(@_){
258 29         37 my($from,$to,$conv);
259             # First check for coderef to avoid seeing units as number in that case:
260 29 100       104 if( ref($_[2]) eq 'CODE' ){
    100          
    50          
261 1         145 ($from,$to,$conv) = splice(@_,0,3);
262             }elsif( looks_like_number($_[0]) ){
263 8         20 ($conv,$from,$to) = splice(@_,0,3);
264 8         14 $conv = 1 / $conv;
265             }elsif( looks_like_number($_[1]) ){
266 20         43 ($from,$conv,$to) = splice(@_,0,3);
267             }else{
268 0         0 croak('Invalid arguments');
269             }
270 29         65 $from = $self->_unalias($from);
271 29         51 $to = $self->_unalias($to);
272 29   50     56 my $units = $type_convs->{$class} ||= {};
273 29   50     40 $units->{$from} ||= {};
274 29         53 $units->{$from}->{$to} = $conv;
275 29 100       48 unless( ref $conv ){
276 28   50     45 $units->{$to} ||= {};
277 28         63 $units->{$to}->{$from} = 1/$conv;
278             }
279             }
280 9         20 $type_paths->{$class} = {};
281 9         17 return;
282             }
283              
284             sub _unalias {
285 163     163   179 my $self = shift;
286 163   66     269 my $class = ref($self) || $self;
287 163         164 my $unit = shift;
288 163 100       320 return $unit if( defined $type_convs->{$class}->{$unit} );
289 31   33     60 return $type_aliases->{$class}->{$unit} || croak('Unknown unit or alias "'.$unit.'"');
290             }
291              
292             sub _conv {
293 31     31   34 my $self = shift;
294 31   33     52 my $class = ref($self) || $self;
295 31         43 my $unit = $self->_unalias( shift );
296 31 100       159 return $self->{values}->{$unit} if( defined $self->{values}->{$unit} );
297 22         36 my $path = $self->_path( $self->unit, $unit );
298 22 50       49 croak('Unable to find an appropriate conversion path') unless( $path );
299 22         29 my $units = $type_convs->{$class};
300 22         28 my $prev_unit = shift( @$path );
301 22         46 my $value = $self->value;
302 22         33 foreach $unit (@$path){
303 60         71 my $conv = $units->{$prev_unit}->{$unit};
304 60 100       81 if( ref($conv) ){
305 1         2 $value = &{$conv}( $value, $prev_unit, $unit );
  1         3  
306             }else{
307 59         74 $value = $value * $units->{$prev_unit}->{$unit};
308             }
309 60         94 $self->{values}->{$unit} = $value;
310 60         79 $prev_unit = $unit;
311             }
312 22         58 return $value;
313             }
314              
315             sub _path {
316 25     25   279 my $self = shift;
317 25         49 my $from = $self->_unalias(shift);
318 25         37 my $to = $self->_unalias(shift);
319 25   66     46 my $class = ref($self) || $self;
320 25         50 my $key = "$from-$to";
321 25   50     43 my $paths = $type_paths->{$class} ||= {};
322 25 100       53 if( defined $paths->{$key} ){ return [@{$paths->{$key}}]; }
  3         4  
  3         9  
323              
324 22   50     33 my $units = $type_convs->{$class} ||= {};
325 22         25 my $path;
326 22         41 foreach (1..10){
327 87         121 $path = _find_path( $from, $to, $units, $_ );
328 87 100       149 last if( $path );
329             }
330 22 50       39 return 0 if(!$path);
331 22         40 $paths->{$key} = $path;
332 22         50 return [@$path];
333             }
334              
335             sub _find_path {
336 569     569   815 my($level,$to,$units) = splice(@_,0,3);
337 569 100       882 unless( ref $level ){ $level=[$level]; }
  87         123  
338 569 50       721 my $max_depth = ( @_ ? shift : 12 );
339 569 100       676 my $depth = ( @_ ? shift : 0 );
340 569 100       656 my $path = ( @_ ? shift : [] );
341 569         610 my $next_level = {};
342              
343 569         681 foreach my $unit (@$level){
344 1343 100       1907 if($unit eq $to){
345 22         30 push @$path, $unit;
346 22         69 return $path;
347             }
348             }
349              
350 547 100       973 return 0 if( $depth+1 == $max_depth );
351 246         234 $depth ++;
352              
353 246         272 foreach my $unit (@$level){
354 482         557 push @$path, $unit;
355 482 100       458 if(_find_path( [keys %{$units->{$unit}}], $to, $units, $max_depth, $depth, $path )){
  482         1028  
356 65         71 $depth --;
357 65         136 return $path;
358             }
359 417         630 pop @$path;
360             }
361              
362 181         201 $depth --;
363 181         264 return 0;
364             }
365              
366             sub _ol_add {
367 4     4   39 my ($left, $right) = @_;
368 4         6 my $class = ref $left;
369              
370 4         11 my $unit = $left->unit;
371 4         10 $left = $left->value;
372 4 100 66     20 $right = $right->value( $unit ) if blessed($right) and $right->isa($class);
373              
374 4         10 return $class->new( $left + $right, $unit );
375             }
376              
377             sub _ol_sub {
378 4     4   10 my ($left, $right, $reverse) = @_;
379 4         6 my $class = ref $left;
380              
381 4         6 my $unit = $left->unit;
382 4         9 $left = $left->value;
383 4 100 66     17 $right = $right->value( $unit ) if blessed($right) and $right->isa($class);
384              
385 4 100       8 ($left, $right) = ($right, $left) if $reverse;
386              
387 4         8 return $class->new( $left - $right, $unit );
388             }
389              
390             sub _ol_mult {
391 4     4   9 my ($left, $right) = @_;
392 4         8 my $class = ref $left;
393              
394 4         7 my $unit = $left->unit;
395 4         8 $left = $left->value;
396 4 50 33     14 $right = $right->value( $unit ) if blessed($right) and $right->isa($class);
397              
398 4         11 return $class->new( $left * $right, $unit );
399             }
400              
401             sub _ol_div {
402 3     3   6 my ($left, $right, $reverse) = @_;
403 3         5 my $class = ref $left;
404              
405 3         5 my $unit = $left->unit;
406 3         7 $left = $left->value;
407 3 50 33     17 $right = $right->value( $unit ) if blessed($right) and $right->isa($class);
408              
409 3 100       9 ($left, $right) = ($right, $left) if $reverse;
410              
411 3         8 return $class->new( $left / $right, $unit );
412             }
413              
414             sub _ol_str {
415 1     1   3 my ($self) = @_;
416 1         2 return $self->value;
417             }
418              
419             1;
420             __END__