File Coverage

blib/lib/Range/Object.pm
Criterion Covered Total %
statement 135 143 94.4
branch 41 48 85.4
condition 13 18 72.2
subroutine 27 32 84.3
pod 12 12 100.0
total 228 253 90.1


line stmt bran cond sub pod time code
1             package Range::Object;
2              
3             # This is basically what common::sense does, but without the pragma itself
4             # to remain compatible with Perls older than 5.8
5              
6 7     7   45636 use strict;
  7         12  
  7         180  
7              
8 7     7   33 no warnings;
  7         12  
  7         293  
9 7         385 use warnings qw(FATAL closed internal debugging pack malloc portable
10             prototype inplace io pipe unpack deprecated glob digit
11 7     7   45 printf reserved taint closure semicolon);
  7         17  
12 7     7   46 no warnings qw(exec newline unopened);
  7         21  
  7         191  
13              
14 7     7   37 use Carp;
  7         15  
  7         359  
15 7     7   44 use List::Util qw( first );
  7         17  
  7         418  
16              
17             ### PACKAGE VARIABLE ###
18             #
19             # Version of this module.
20             #
21              
22             # This is for compatibility with older Perls
23 7     7   37 use vars qw( $VERSION );
  7         9  
  7         9884  
24              
25             $VERSION = '0.94';
26              
27             ### PUBLIC CLASS METHOD (CONSTRUCTOR) ###
28             #
29             # Initializes new instance of $class from @input_range.
30             #
31              
32             sub new {
33 43     43 1 28758 my ($class, @input_range) = @_;
34              
35 43         141 my $self = bless { range => [] }, $class;
36              
37 43         129 return $self->add(@input_range);
38             }
39              
40             ### PUBLIC INSTANCE METHOD ###
41             #
42             # Validates @input_range of items and adds them to internal storage.
43             #
44              
45             sub add {
46 76     76 1 18537 my ($self, @input_range) = @_;
47              
48             # Nothing to do
49 76 50       197 return $self unless @input_range;
50              
51 76         221 my @validated_input = $self->_validate_and_expand(@input_range);
52              
53             # Expand existing range and overlay the new one
54 40         156 my %existing_values = map {; "$_" => 1 } $self->_full_range();
  110         354  
55 40         209 @existing_values{ @validated_input } = (1) x @validated_input;
56              
57             # Collapse resulting hash and replace current range with new values
58 40         216 $self->{range} = [ $self->_collapse_range( keys %existing_values ) ];
59              
60 40         381 return $self;
61             }
62              
63             ### PUBLIC INSTANCE METHOD ###
64             #
65             # Removes items in @input_range from internal storage.
66             #
67              
68             sub remove {
69 8     8 1 17976 my ($self, @input_range) = @_;
70              
71             # Nothing to do
72 8 50       32 return $self unless @input_range;
73              
74 8         30 my @validated_input = $self->_validate_and_expand(@input_range);
75              
76             # Expand existing range and remove what needs to be removed
77 8         109 my %existing_values = map {; "$_" => 1 } $self->_full_range();
  161         491  
78 8         54 delete @existing_values{ @validated_input };
79              
80             # Collapse resulting hash and replace current range with new values
81 8         509 $self->{range} = [ $self->_collapse_range( keys %existing_values ) ];
82              
83 8         1720 return $self;
84             }
85              
86             ### PUBLIC INSTANCE METHOD ###
87             #
88             # Returns sorted array or string representation of internal storage.
89             # In scalar context it can use optional list separator instead of
90             # default one.
91             #
92              
93             sub range {
94 48     48 1 28926 my ($self, $separator) = @_;
95              
96 48 100       213 return wantarray ? $self->_sort_range()
97             : $self->stringify($separator)
98             ;
99             }
100              
101             ### PUBLIC INSTANCE METHOD ###
102             #
103             # Returns sorted and collapsed representation of internal storage.
104             # In list context, resulting list consists of separate values and/or
105             # range hashrefs with three elements: start, end and count.
106             # In scalar context, result is a string of separate values and/or
107             # ranges separated by value returned by delimiter() method.
108             # Optional list separator can be used instead of default one in
109             # scalar context.
110             #
111              
112             sub collapsed {
113 48     48 1 15976 my ($self, $separator) = @_;
114              
115 48 100       155 return wantarray ? @{ $self->{range} }
  24         148  
116             : $self->stringify_collapsed($separator)
117             ;
118             }
119              
120             ### PUBLIC INSTANCE METHOD ###
121             #
122             # Returns the number of separate items in internal storage.
123             #
124              
125             sub size {
126 42     42 1 20329 my ($self) = @_;
127              
128 42         85 my $size = 0;
129 42         68 for my $item ( @{ $self->{range} } ) {
  42         103  
130 236 100       405 $size += ref $item ? $item->{count} : 1;
131             };
132              
133 42         101 return $size;
134             }
135              
136             ### PUBLIC INSTANCE METHOD ###
137             #
138             # Tests if items of @input_range are matching items in our internal storage.
139             # Returns true/false in scalar context, list of mismatching items in list
140             # context.
141             #
142              
143             sub in {
144 852     852 1 146386 my ($self, @input_range) = @_;
145              
146 852         1887 my @validated_range = $self->_validate_and_expand(@input_range);
147              
148 852 100       1602 if ( wantarray ) {
149             # This should be reasonably fast
150 42         102 return grep { !defined $self->_search_range("$_") } @validated_range;
  810         1598  
151             }
152             else {
153             # This should be even faster
154             return defined first {
155 810     810   2298 my $result = $self->_search_range("$_");
156 810 100       7256 !defined $result ? ''
    100          
157             : $result == 0 ? 1
158             : $result
159             }
160 810         3948 @validated_range;
161             };
162              
163 0         0 return; # Just for fun
164             }
165              
166             ### PUBLIC INSTANCE METHOD ###
167             #
168             # Returns string representation of all items in internal storage (sorted).
169             #
170              
171             sub stringify {
172 36     36 1 99 my ($self, $separator) = @_;
173              
174 36   33     157 $separator ||= $self->_list_separator();
175              
176 36         92 return join $separator, $self->_sort_range();
177             }
178              
179             ### PUBLIC INSTANCE METHOD ###
180             #
181             # Returns string representation of collapsed current range.
182             #
183              
184             sub stringify_collapsed {
185 42     42 1 142 my ($self, $separator) = @_;
186              
187 42   33     156 $separator ||= $self->_list_separator();
188              
189             my @collapsed_range
190             = map {
191             ref($_) ? $self->_stringify_range( $_->{start}, $_->{end} )
192 298 100       586 : "$_"
193             }
194 42         58 @{ $self->{range} };
  42         86  
195              
196 42         176 return join $separator, @collapsed_range;
197             }
198              
199             ### PUBLIC INSTANCE METHOD ###
200             #
201             # Returns regex that is used to validate range items. Regex should
202             # include patterns both for separate disjointed items and contiguous
203             # ranges.
204             # Default pattern matches everything.
205             #
206              
207 0     0 1 0 sub pattern { qr/.*?/xms }
208              
209             ### PUBLIC INSTANCE METHOD ###
210             #
211             # Returns regex that is used to separate items in a range list.
212             # Default is no separator.
213             #
214              
215 0     0 1 0 sub separator { qr//xms }
216              
217             ### PUBLIC INSTANCE METHOD ###
218             #
219             # Returns default range delimiter for current class.
220             #
221              
222 0     0 1 0 sub delimiter { '-' }
223              
224             ############## PRIVATE METHODS BELOW ##############
225              
226             ### PRIVATE INSTANCE METHOD ###
227             #
228             # Returns default list separator for use with stringify() and
229             # stringify_collapsed()
230             #
231              
232 18     18   57 sub _list_separator { q{,} }
233              
234             ### PRIVATE INSTANCE METHOD ###
235             #
236             # Validates and unpacks input @input_range of items, returns full list.
237             #
238              
239             sub _validate_and_expand {
240 576     576   996 my ($self, @input_range) = @_;
241              
242             # Nothing to do
243 576 50       1226 return unless @input_range;
244              
245             # We need the patterns
246 576         1389 my $pattern = $self->pattern();
247 576         1300 my $separator = $self->separator();
248              
249             # We use hash to avoid duplicates
250 576         946 my %temp = ();
251              
252             # Expand and validate items in @input_range; add them if all is OK
253             ITEM:
254 576         1099 while ( @input_range ) {
255 1303         2025 my $item = shift @input_range;
256              
257 1303 100 66     6593 if ( $separator && $item =~ $separator ) {
258 15         189 unshift @input_range, split $separator, $item;
259 15         73 next ITEM;
260             };
261              
262 1288 100 100     10586 croak "Invalid input: $item"
      100        
263             if !defined $item || $item eq '' || $item !~ /$pattern/;
264              
265             # Default expansion mechanism is Perl range operator (..)
266 1269         2012 my @items = eval { $self->_explode_range($item) };
  1269         2561  
267 1269 50       2534 croak "Invalid input item '$item': $@" if $@;
268              
269             # Store result to temp hash, avoiding duplicates
270 1269         4345 @temp{ @items } = (1) x @items;
271             };
272              
273             # We need to sort items because order matters
274 557         1967 my @result = $self->_sort_range( keys %temp );
275              
276 557         2031 return @result;
277             }
278              
279             ### PRIVATE INSTANCE METHOD ###
280             #
281             # Explodes stringified range of items using Perl range operator.
282             #
283              
284             sub _explode_range {
285 1281     1281   2198 my ($self, $string) = @_;
286              
287 1281         2490 my $delimiter = $self->delimiter();
288              
289             # Shortcut
290 1281         2138 for ($string) {
291             # Remove whitespace and normalize separators
292 1281         2204 s/\s+//g;
293 1281         1827 s/;/,/g;
294              
295             # Replace delimiters with (..) honoring qw() constructs
296 1281 100       5588 s{ \) \s* $delimiter \s* qw\( } {)..qw(}gx
297             unless s{ (\d) \s* $delimiter \s* (\d) } {$1..$2}gx;
298             };
299              
300 1281         48085 my $items_ref = eval "[$string]";
301              
302 1281         5076 return @$items_ref;
303             }
304              
305             ### PRIVATE INSTANCE METHOD ###
306             #
307             # Tests if a sigle value is in current range.
308             #
309              
310             sub _search_range {
311 1038     1038   1701 my ($self, $value) = @_;
312              
313             return first {
314 4336 100   4336   10357 ref($_) ? $self->_is_in_range_hashref($_, $value)
315             : $self->_equal_value("$_", $value)
316             }
317 1038         2467 @{ $self->{range} };
  1038         2447  
318             }
319              
320             ### PRIVATE INSTANCE METHOD ###
321             #
322             # Tests if a single value is within boundaries of collapsed range item.
323             # Default method uses string comparison.
324             #
325              
326             sub _is_in_range_hashref {
327 1502     1502   2185 my ($self, $range_ref, $value) = @_;
328              
329             return ( ($value ge $range_ref->{start})
330             && ($value le $range_ref->{end})
331 1502   100     4529 );
332             }
333              
334             ### PRIVATE INSTANCE METHOD ###
335             #
336             # Returns sorted list of all single items within current range.
337             # Default sort is string-based.
338             #
339             # Works in list context only, croaks if called otherwise.
340             #
341              
342             sub _sort_range {
343 332     332   640 my ($self, @range) = @_;
344              
345 332 50       592 croak "Internal error: _sort_range can only be used in list context"
346             unless wantarray;
347              
348 332 100       988 return sort { $a cmp $b } @range ? @range : $self->_full_range();
  3278         4051  
349             }
350              
351             ### PRIVATE INSTANCE METHOD ###
352             #
353             # Returns full list of items in current range.
354             #
355              
356             sub _full_range {
357 132     132   234 my ($self) = @_;
358              
359 132 50       272 croak "Internal error: _full_range can only be used in list context"
360             unless wantarray;
361              
362 132         309 my $delimiter = $self->delimiter();
363              
364             return map {
365             ref($_) ? $self->_explode_range( $_->{start}
366             . $delimiter
367             . $_->{end}
368             )
369 587 100       1833 : "$_"
370             }
371 132         193 @{ $self->{range} };
  132         495  
372             }
373              
374             ### PRIVATE INSTANCE METHOD ###
375             #
376             # Returns collapsed list of current range items. Separate items are
377             # returned as is, contiguous ranges are collapsed and returned as
378             # hashrefs { start => $start, end => $end, count => $count }.
379             #
380             # Works in list context only, croaks if called otherwise.
381             #
382              
383             sub _collapse_range {
384 42     42   149 my ($self, @range) = @_;
385              
386 42 50       95 croak "Internal error: _collapse_range can only be used in list context"
387             unless wantarray;
388              
389 42         83 my ($first, $last, $count, @result);
390              
391             ITEM:
392 42         117 for my $item ( $self->_sort_range(@range) ) {
393             # If $first is defined, it means range has started
394 630 100       966 if ( !defined $first ) {
395 42         74 $first = $last = $item;
396 42         66 $count = 1;
397 42         91 next ITEM;
398             };
399              
400             # If $last immediately preceeds $item in range,
401             # $item becomes next $last
402 588 100       991 if ( $self->_next_in_range($last, $item) ) {
403 394         495 $last = $item;
404 394         411 $count++;
405 394         644 next ITEM;
406             };
407              
408             # If $item doesn't follow $last and $last is defined,
409             # it means current contiguous range is complete
410 194 100       388 if ( !$self->_equal_value($first, $last) ) {
411 88         272 push @result, {
412             start => $first,
413             end => $last,
414             count => $count,
415             };
416 88         140 $first = $last = $item;
417 88         106 $count = 1;
418 88         144 next ITEM;
419             };
420              
421             # If $last wasn't defined, range was never contiguous
422 106         199 push @result, "$first";
423 106         141 $first = $last = $item;
424 106         167 $count = 1;
425             }
426              
427             # We're here when last item has been processed
428 42 100       120 push @result,
429             $self->_equal_value($first, $last) ? "$first"
430             : {
431             start => $first,
432             end => $last,
433             count => $count,
434             }
435             ;
436              
437 42         212 return @result;
438             }
439              
440             ### PRIVATE INSTANCE METHOD ###
441             #
442             # Tests if two values are equal. This method has to be overridden.
443             #
444              
445             sub _equal_value {
446 0     0   0 my ($self, $first, $second) = @_;
447              
448 0         0 croak "Internal error: Can't use _equal_value with Range::Object";
449             }
450              
451             ### PRIVATE INSTANCE METHOD ###
452             #
453             # Tests if two values are consequent. This method has to be overridden.
454             #
455              
456             sub _next_in_range {
457 0     0   0 my ($self, $first, $second) = @_;
458              
459 0         0 croak "Internal error: Can't use _next_in_range with Range::Object";
460             }
461              
462             ### PRIVATE INSTANCE METHOD ###
463             #
464             # Returns stringified representation of a range within $first and $last
465             # boundaries.
466             #
467              
468             sub _stringify_range {
469 225     225   381 my ($self, $first, $last) = @_;
470              
471 225         364 my $delimiter = $self->delimiter();
472              
473 225         542 return $first . $delimiter . $last;
474             }
475              
476             1;
477              
478             __END__