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   705631 use 5.008001;
  4         26  
3 4     4   21 use strict;
  4         9  
  4         79  
4 4     4   19 use warnings;
  4         10  
  4         205  
5             our $VERSION = '0.08';
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   29 use Carp qw( croak );
  4         6  
  4         180  
31 4     4   23 use Scalar::Util qw(looks_like_number);
  4         8  
  4         432  
32              
33             use overload
34 4         44 '+'=>\&_ol_add, '-'=>\&_ol_sub,
35             '*'=>\&_ol_mult, '/'=>\&_ol_div,
36 4     4   29 '""'=>\&_ol_str;
  4         6  
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 6910 my $class = shift;
60              
61 14         29 my $unit = pop;
62 14 100 66     77 $unit = $type_aliases->{$class}->{$unit} || $unit if $unit;
63              
64             croak 'Unknown Class::Measure unit'
65 14 100 100     453 unless $unit and $type_convs->{$class}->{$unit};
66              
67 11         74 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 32 my $self = shift;
83 24         75 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 70 my $self = shift;
96 21         39 my $unit = $self->_unalias( shift );
97 21         50 $self->_conv( $unit );
98 21         28 $self->{unit} = $unit;
99 21         43 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 142 my $self = shift;
119 54 100       120 return $self->_conv(shift) if @_;
120 52         194 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 29 my $self = shift;
136 18 100       41 $self->{unit} = $self->_unalias(pop @_) if( @_>1 );
137 18         51 $self->{values} = { $self->{unit} => shift };
138 18         27 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 458 my $self = shift;
155 8   33     56 my $class = ref($self) || $self;
156 8   100     48 my $convs = $type_convs->{$class} ||= {};
157 8         20 foreach my $unit (@_){
158 30 50       76 croak('This unit has already been defined') if $convs->{$unit};
159 30         89 $convs->{$unit} = {};
160              
161 4     4   2143 no strict 'refs';
  4         13  
  4         1443  
162 30         69 *{"${class}::${unit}"} = _build_unit_sub( $unit );
  30         145  
163             }
164 8         26 return;
165             }
166              
167             sub _build_unit_sub {
168 30     30   55 my ($unit) = @_;
169              
170             return sub{
171 8     8   33 my $self = shift;
172 8 50       23 return $self->set_value( shift(), $unit ) if @_;
173 8         40 return $self->_conv( $unit );
174 30         116 };
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 125 my $self = shift;
187 1   33     11 my $class = ref($self) || $self;
188 1         3 return keys(%{$type_convs->{$class}});
  1         21  
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 20 my $self = shift;
208 6   33     28 my $class = ref($self) || $self;
209 6 50       20 croak('Wrong number of arguments (must be a multiple of two)') if( (@_+0) % 2 );
210 6   100     23 my $aliases = $type_aliases->{$class} ||= {};
211 6         17 while( @_ ){
212 26 100       61 my @aliases = ( ref($_[0]) ? @{shift()} : shift );
  10         20  
213 26         39 my $unit = shift;
214 26 50       53 croak('Unknown unit "'.$unit.'" to alias to') unless( defined $type_convs->{$class}->{$unit} );
215 26         43 foreach my $alias (@aliases){
216 54 50       112 if( defined $aliases->{$alias} ){ croak('Alias already in use'); }
  0         0  
217 54         126 $aliases->{$alias} = $unit;
218              
219 4     4   32 no strict 'refs';
  4         15  
  4         6427  
220 54         65 *{"${class}::${alias}"} = *{"${class}::${unit}"};
  54         227  
  54         118  
221             }
222             }
223 6         16 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 323 my $self = shift;
255 9 50       31 croak('Wrong number of arguments (must be a multiple of three)') if( (@_+0) % 3 );
256 9   33     36 my $class = ref($self) || $self;
257 9         25 while(@_){
258 29         61 my($from,$to,$conv);
259             # First check for coderef to avoid seeing units as number in that case:
260 29 100       125 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         15 $conv = 1 / $conv;
265             }elsif( looks_like_number($_[1]) ){
266 20         41 ($from,$conv,$to) = splice(@_,0,3);
267             }else{
268 0         0 croak('Invalid arguments');
269             }
270 29         78 $from = $self->_unalias($from);
271 29         63 $to = $self->_unalias($to);
272 29   50     73 my $units = $type_convs->{$class} ||= {};
273 29   50     51 $units->{$from} ||= {};
274 29         78 $units->{$from}->{$to} = $conv;
275 29 100       54 unless( ref $conv ){
276 28   50     54 $units->{$to} ||= {};
277 28         82 $units->{$to}->{$from} = 1/$conv;
278             }
279             }
280 9         29 $type_paths->{$class} = {};
281 9         20 return;
282             }
283              
284             sub _unalias {
285 163     163   219 my $self = shift;
286 163   66     326 my $class = ref($self) || $self;
287 163         212 my $unit = shift;
288 163 100       370 return $unit if( defined $type_convs->{$class}->{$unit} );
289 31   33     74 return $type_aliases->{$class}->{$unit} || croak('Unknown unit or alias "'.$unit.'"');
290             }
291              
292             sub _conv {
293 31     31   54 my $self = shift;
294 31   33     61 my $class = ref($self) || $self;
295 31         60 my $unit = $self->_unalias( shift );
296 31 100       208 return $self->{values}->{$unit} if( defined $self->{values}->{$unit} );
297 22         52 my $path = $self->_path( $self->unit, $unit );
298 22 50       58 croak('Unable to find an appropriate conversion path') unless( $path );
299 22         54 my $units = $type_convs->{$class};
300 22         35 my $prev_unit = shift( @$path );
301 22         66 my $value = $self->value;
302 22         51 foreach $unit (@$path){
303 60         95 my $conv = $units->{$prev_unit}->{$unit};
304 60 100       98 if( ref($conv) ){
305 1         4 $value = &{$conv}( $value, $prev_unit, $unit );
  1         3  
306             }else{
307 59         109 $value = $value * $units->{$prev_unit}->{$unit};
308             }
309 60         106 $self->{values}->{$unit} = $value;
310 60         105 $prev_unit = $unit;
311             }
312 22         83 return $value;
313             }
314              
315             sub _path {
316 25     25   316 my $self = shift;
317 25         49 my $from = $self->_unalias(shift);
318 25         51 my $to = $self->_unalias(shift);
319 25   66     63 my $class = ref($self) || $self;
320 25         59 my $key = "$from-$to";
321 25   50     66 my $paths = $type_paths->{$class} ||= {};
322 25 100       65 if( defined $paths->{$key} ){ return [@{$paths->{$key}}]; }
  3         5  
  3         10  
323              
324 22   50     47 my $units = $type_convs->{$class} ||= {};
325 22         31 my $path;
326 22         57 foreach (1..10){
327 87         149 $path = _find_path( $from, $to, $units, $_ );
328 87 100       182 last if( $path );
329             }
330 22 50       51 return 0 if(!$path);
331 22         55 $paths->{$key} = $path;
332 22         59 return [@$path];
333             }
334              
335             sub _find_path {
336 484     484   846 my($level,$to,$units) = splice(@_,0,3);
337 484 100       848 unless( ref $level ){ $level=[$level]; }
  87         150  
338 484 50       767 my $max_depth = ( @_ ? shift : 12 );
339 484 100       681 my $depth = ( @_ ? shift : 0 );
340 484 100       698 my $path = ( @_ ? shift : [] );
341 484         609 my $next_level = {};
342              
343 484         722 foreach my $unit (@$level){
344 1088 100       1910 if($unit eq $to){
345 22         39 push @$path, $unit;
346 22         63 return $path;
347             }
348             }
349              
350 462 100       1043 return 0 if( $depth+1 == $max_depth );
351 221         255 $depth ++;
352              
353 221         328 foreach my $unit (@$level){
354 397         603 push @$path, $unit;
355 397 100       461 if(_find_path( [keys %{$units->{$unit}}], $to, $units, $max_depth, $depth, $path )){
  397         1047  
356 65         88 $depth --;
357 65         164 return $path;
358             }
359 332         603 pop @$path;
360             }
361              
362 156         235 $depth --;
363 156         317 return 0;
364             }
365              
366             sub _ol_add {
367 4     4   54 my($one,$two,$opt) = @_;
368 4 100       11 if($opt){ my $tmp=$one; $one=$two; $two=$tmp; }
  1         2  
  1         3  
  1         3  
369 4 100 100     23 if( ref($two) and ref($one) ){
    100          
    50          
370 1 50       48 croak('You may only add numbers or measurements of the same class') if( ref($one) ne ref($two) );
371 1         6 $one->set_value( $one->value + $two->value($one->unit) );
372 1         36 return $one;
373             }elsif( ref $one ){
374 2         7 $one->set_value( $one->value + $two );
375 2         6 return $one;
376             }elsif( ref $two ){
377 1         6 $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   32 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     20 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         4 $one->set_value( $one->value - $two->value($one->unit) );
389 1         3 return $one;
390             }elsif( ref $one ){
391 2         8 $one->set_value( $one->value - $two );
392 2         12 return $one;
393             }elsif( ref $two ){
394 1         7 $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   13 my($one,$two,$opt) = @_;
402 4 100       14 if($opt){ my $tmp=$one; $one=$two; $two=$tmp; }
  1         2  
  1         2  
  1         2  
403 4 50 66     21 if( ref($two) and ref($one) ){
    100          
    50          
404 0         0 croak('You cannot multiply two measure classes');
405             }elsif( ref $one ){
406 3         18 $one->set_value( $one->value * $two );
407 3         9 return $one;
408             }elsif( ref $two ){
409 1         3 $two->set_value( $one * $two->value );
410 1         2 return $two;
411             }
412 0         0 return;
413             }
414              
415             sub _ol_div {
416 3     3   12 my($one,$two,$opt) = @_;
417 3 100       8 if($opt){ my $tmp=$one; $one=$two; $two=$tmp; }
  1         2  
  1         5  
  1         3  
418 3 50 66     23 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         4 $one->set_value( $one->value / $two );
422 2         7 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   10 my $self = shift;
432 1         3 return $self->value;
433             }
434              
435             1;
436             __END__