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   33168 use strict;
  7         13  
  7         242  
7              
8 7     7   35 no warnings;
  7         11  
  7         730  
9 7         486 use warnings qw(FATAL closed internal debugging pack malloc portable
10             prototype inplace io pipe unpack deprecated glob digit
11 7     7   93 printf reserved taint closure semicolon);
  7         17  
12 7     7   37 no warnings qw(exec newline unopened);
  7         12  
  7         224  
13              
14 7     7   43 use Carp;
  7         12  
  7         505  
15 7     7   41 use List::Util qw( first );
  7         84  
  7         575  
16              
17             ### PACKAGE VARIABLE ###
18             #
19             # Version of this module.
20             #
21              
22             # This is for compatibility with older Perls
23 7     7   40 use vars qw( $VERSION );
  7         9  
  7         14065  
24              
25             $VERSION = '0.93';
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 55033 my ($class, @input_range) = @_;
34              
35 43         250 my $self = bless { range => [] }, $class;
36              
37 43         160 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 31145 my ($self, @input_range) = @_;
47              
48             # Nothing to do
49 76 50       261 return $self unless @input_range;
50              
51 76         322 my @validated_input = $self->_validate_and_expand(@input_range);
52              
53             # Expand existing range and overlay the new one
54 40         204 my %existing_values = map {; "$_" => 1 } $self->_full_range();
  110         445  
55 40         306 @existing_values{ @validated_input } = (1) x @validated_input;
56              
57             # Collapse resulting hash and replace current range with new values
58 40         289 $self->{range} = [ $self->_collapse_range( keys %existing_values ) ];
59              
60 40         462 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 26631 my ($self, @input_range) = @_;
70              
71             # Nothing to do
72 8 50       44 return $self unless @input_range;
73              
74 8         43 my @validated_input = $self->_validate_and_expand(@input_range);
75              
76             # Expand existing range and remove what needs to be removed
77 8         791 my %existing_values = map {; "$_" => 1 } $self->_full_range();
  161         583  
78 8         68 delete @existing_values{ @validated_input };
79              
80             # Collapse resulting hash and replace current range with new values
81 8         1252 $self->{range} = [ $self->_collapse_range( keys %existing_values ) ];
82              
83 8         98 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 53421 my ($self, $separator) = @_;
95              
96 48 100       242 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 33804 my ($self, $separator) = @_;
114              
115 48 100       172 return wantarray ? @{ $self->{range} }
  24         228  
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 19804 my ($self) = @_;
127              
128 42         72 my $size = 0;
129 42         62 for my $item ( @{ $self->{range} } ) {
  42         118  
130 236 100       468 $size += ref $item ? $item->{count} : 1;
131             };
132              
133 42         109 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 240089 my ($self, @input_range) = @_;
145              
146 852         5240 my @validated_range = $self->_validate_and_expand(@input_range);
147              
148 852 100       2024 if ( wantarray ) {
149             # This should be reasonably fast
150 42         106 return grep { !defined $self->_search_range("$_") } @validated_range;
  810         2172  
151             }
152             else {
153             # This should be even faster
154             return defined first {
155 810     810   3061 my $result = $self->_search_range("$_");
156 810 100       8981 !defined $result ? ''
    100          
157             : $result == 0 ? 1
158             : $result
159             }
160 810         4780 @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 81 my ($self, $separator) = @_;
173              
174 36   33     200 $separator ||= $self->_list_separator();
175              
176 36         99 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 137 my ($self, $separator) = @_;
186              
187 42   33     195 $separator ||= $self->_list_separator();
188              
189             my @collapsed_range
190 298 100       861 = map {
191 42         100 ref($_) ? $self->_stringify_range( $_->{start}, $_->{end} )
192             : "$_"
193             }
194 42         141 @{ $self->{range} };
195              
196 42         301 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   61 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   1303 my ($self, @input_range) = @_;
241              
242             # Nothing to do
243 576 50       1862 return unless @input_range;
244              
245             # We need the patterns
246 576         1945 my $pattern = $self->pattern();
247 576         1620 my $separator = $self->separator();
248              
249             # We use hash to avoid duplicates
250 576         3977 my %temp = ();
251              
252             # Expand and validate items in @input_range; add them if all is OK
253             ITEM:
254 576         1314 while ( @input_range ) {
255 1303         2127 my $item = shift @input_range;
256              
257 1303 100 66     9427 if ( $separator && $item =~ $separator ) {
258 15         245 unshift @input_range, split $separator, $item;
259 15         76 next ITEM;
260             };
261              
262 1288 100 100     17885 croak "Invalid input: $item"
      100        
263             if !defined $item || $item eq '' || $item !~ /$pattern/;
264              
265             # Default expansion mechanism is Perl range operator (..)
266 1269         2062 my @items = eval { $self->_explode_range($item) };
  1269         3777  
267 1269 50       2706 croak "Invalid input item '$item': $@" if $@;
268              
269             # Store result to temp hash, avoiding duplicates
270 1269         9257 @temp{ @items } = (1) x @items;
271             };
272              
273             # We need to sort items because order matters
274 557         6072 my @result = $self->_sort_range( keys %temp );
275              
276 557         3687 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   2070 my ($self, $string) = @_;
286              
287 1281         3245 my $delimiter = $self->delimiter();
288              
289             # Shortcut
290 1281         3099 for ($string) {
291             # Remove whitespace and normalize separators
292 1281         2022 s/\s+//g;
293 1281         2081 s/;/,/g;
294              
295             # Replace delimiters with (..) honoring qw() constructs
296 1281 100       8824 s{ \) \s* $delimiter \s* qw\( } {)..qw(}gx
297             unless s{ (\d) \s* $delimiter \s* (\d) } {$1..$2}gx;
298             };
299              
300 1281         69717 my $items_ref = eval "[$string]";
301              
302 1281         8775 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   1560 my ($self, $value) = @_;
312              
313             return first {
314 4336 100   4336   15918 ref($_) ? $self->_is_in_range_hashref($_, $value)
315             : $self->_equal_value("$_", $value)
316             }
317 1038         3290 @{ $self->{range} };
  1038         3179  
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   1933 my ($self, $range_ref, $value) = @_;
328              
329 1502   100     7211 return ( ($value ge $range_ref->{start})
330             && ($value le $range_ref->{end})
331             );
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   1102 my ($self, @range) = @_;
344              
345 332 50       715 croak "Internal error: _sort_range can only be used in list context"
346             unless wantarray;
347              
348 332 100       1381 return sort { $a cmp $b } @range ? @range : $self->_full_range();
  3267         4584  
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   202 my ($self) = @_;
358              
359 132 50       336 croak "Internal error: _full_range can only be used in list context"
360             unless wantarray;
361              
362 132         1063 my $delimiter = $self->delimiter();
363              
364 587 100       2455 return map {
365 132         745 ref($_) ? $self->_explode_range( $_->{start}
366             . $delimiter
367             . $_->{end}
368             )
369             : "$_"
370             }
371 132         192 @{ $self->{range} };
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   182 my ($self, @range) = @_;
385              
386 42 50       111 croak "Internal error: _collapse_range can only be used in list context"
387             unless wantarray;
388              
389 42         63 my ($first, $last, $count, @result);
390              
391             ITEM:
392 42         159 for my $item ( $self->_sort_range(@range) ) {
393             # If $first is defined, it means range has started
394 630 100       1163 if ( !defined $first ) {
395 42         72 $first = $last = $item;
396 42         54 $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       1517 if ( $self->_next_in_range($last, $item) ) {
403 394         454 $last = $item;
404 394         402 $count++;
405 394         759 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       614 if ( !$self->_equal_value($first, $last) ) {
411 88         364 push @result, {
412             start => $first,
413             end => $last,
414             count => $count,
415             };
416 88         142 $first = $last = $item;
417 88         98 $count = 1;
418 88         184 next ITEM;
419             };
420              
421             # If $last wasn't defined, range was never contiguous
422 106         218 push @result, "$first";
423 106         165 $first = $last = $item;
424 106         150 $count = 1;
425             }
426              
427             # We're here when last item has been processed
428 42 100       174 push @result,
429             $self->_equal_value($first, $last) ? "$first"
430             : {
431             start => $first,
432             end => $last,
433             count => $count,
434             }
435             ;
436              
437 42         300 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   709 my ($self, $first, $last) = @_;
470              
471 225         546 my $delimiter = $self->delimiter();
472              
473 225         774 return $first . $delimiter . $last;
474             }
475              
476             1;
477              
478             __END__