File Coverage

blib/lib/Number/Range.pm
Criterion Covered Total %
statement 127 158 80.3
branch 38 70 54.2
condition 7 18 38.8
subroutine 17 19 89.4
pod 7 9 77.7
total 196 274 71.5


line stmt bran cond sub pod time code
1             package Number::Range;
2            
3 1     1   20113 use strict;
  1         2  
  1         36  
4 1     1   4 use warnings;
  1         2  
  1         24  
5 1     1   5 use warnings::register;
  1         6  
  1         139  
6 1     1   6 use Carp;
  1         1  
  1         93  
7 1     1   770 use POSIX; # Only needed for max integer size check in large ranges
  1         6518  
  1         6  
8            
9             require Exporter;
10            
11             our @ISA = qw(Exporter);
12            
13            
14             our $VERSION = '0.12';
15            
16             sub new {
17 10     10 1 3561 my $this = shift;
18 10   33     51 my $class = ref($this) || $this;
19 10         14 my $self = {};
20 10         24 bless $self, $class;
21             # Max size of range before its stored as a pointer instead of hashed
22 10         27 $self->{max_hash_size} = 1000;
23 10         31 $self->initialize("add", @_);
24 10         44 return $self;
25             }
26            
27             sub initialize {
28 12     12 0 13 my $self = shift;
29 12         20 my $type = shift;
30 12         41 my $rangesep = qr/(?:\.\.)/;
31 12         33 my $sectsep = qr/(?:\s|,)/;
32 12         178 my $validation = qr/(?:
33             [^0-9,. -]| # These are the only allowed characters (Numbers and "separators")
34             $rangesep$sectsep| # We don't want a range separator followed by section separator
35             $sectsep$rangesep| # We don't want a section separator followed by range separator
36             \d-\d| # We don't want 10-10 since - is for negative numbers
37             ^$sectsep| # We don't want a section separator at the start
38             ^$rangesep| # We don't want a range separator at the start
39             $sectsep$| # We don't want a section separator at the end
40             $rangesep$ # We don't want a range separator at the end
41             )/x;
42 12         28 foreach my $item (@_) {
43 16 50       195 croak "$item contains invalid data" if ($item =~ m/$validation/g);
44 16         91 foreach my $section (split(/$sectsep/, $item)) {
45 27 100       103 if ($section =~ m/$rangesep/) {
46 15         51 my ($start, $end) = split(/$rangesep/, $section, 2);
47 15 50       42 if ($start > $end) {
48 0 0       0 carp "$start is > $end" if (warnings::enabled());
49 0         0 ($start, $end) = ($end, $start);
50             }
51 15 50       25 if ($start == $end) {
52 0 0       0 carp "$start:$end is pointless" if (warnings::enabled());
53 0 0       0 if ($type eq "add") {
    0          
54 0         0 $self->_addnumbers($start);
55             }
56             elsif ($type eq "del") {
57 0         0 $self->_delnumbers($start);
58             }
59             else {
60 0         0 die "Neither 'add' nor 'del' was passed initialize()";
61             }
62             }
63             else {
64 15 100       30 if ($type eq "add") {
    50          
65 14 100       34 if(($end - $start) > $self->{max_hash_size}) {
66 3         12 $self->_addrange($start, $end);
67             } else {
68 11         71 $self->_addnumbers($start .. $end);
69             }
70             }
71             elsif ($type eq "del") {
72 1 50       5 if($end - $start > $self->{max_hash_size}) {
73 0         0 $self->_delrange($start, $end);
74             } else {
75 1         6 $self->_delnumbers($start .. $end);
76             }
77             }
78             else {
79 0         0 die "Neither 'add' nor 'del' was passed initialize()";
80             }
81             }
82             }
83             else {
84 12 50       22 if ($type eq "add") {
    0          
85 12         24 $self->_addnumbers($section);
86             }
87             elsif ($type eq "del") {
88 0         0 $self->_delnumbers($section);
89             }
90             else {
91 0         0 die "Neither 'add' nor 'del' was passed initialize()";
92             }
93             }
94             }
95             }
96             }
97            
98             sub set_max_hash_size {
99 0     0 0 0 my $self = shift;
100 0         0 my $val = shift;
101 0 0       0 if($val !~ m/^\d+$/) { return 0; }
  0         0  
102 0         0 $self->{max_hash_size} = $val;
103             }
104            
105             sub _addrange {
106 3     3   4 my $self = shift;
107 3         5 my $start = shift;
108 3         4 my $end = shift;
109 3         25 $self->{_largeRangehash}{"$start .. $end"} = [$start, $end];
110             }
111            
112             sub _delrange {
113 0     0   0 my $self = shift;
114 0         0 my $start = shift;
115 0         0 my $end = shift;
116 0         0 delete $self->{_largeRangehash}{"$start .. $end"};
117             }
118            
119             sub _testlarge {
120 9     9   13 my $self = shift;
121 9         12 my $test = shift;
122 9 100       24 if(!exists($self->{_largeRangehash})) {
123 6         24 return 0;
124             }
125 3         6 foreach my $rangeID (keys(%{$self->{_largeRangehash}})) {
  3         10  
126 3         6 my $range = $self->{_largeRangehash}->{$rangeID};
127 3 50 33     18 if ($test >= @$range[0]
128             && $test <= @$range[1]) {
129 3         11 return 1;
130             }
131             }
132 0         0 return 0;
133             }
134            
135             sub _addnumbers {
136 23     23   27 my $self = shift;
137 23         28 foreach my $number (@_) {
138 571 50       45490 if (warnings::enabled()) {
139 0 0       0 carp "$number already in range" if $self->inrange($number);
140             }
141 571         1892 $self->{_rangehash}{$number} = 1;
142             }
143             }
144            
145             sub _delnumbers {
146 1     1   2 my $self = shift;
147 1         3 foreach my $number (@_) {
148 11 50       872 if (warnings::enabled()) {
149 0 0       0 carp "$number not in range or already removed" if (!$self->inrange($number));
150             }
151 11         36 delete $self->{_rangehash}{$number};
152             }
153             }
154            
155             sub inrange {
156 27     27 1 108 my $self = shift;
157 27 100       56 if (scalar(@_) == 1) {
158 25 100 100     110 if ( exists($self->{_rangehash}{-+-$_[0]})
159             || $self->_testlarge($_[0])) {
160 19         83 return 1;
161             } else {
162 6         21 return 0;
163             }
164             } else {
165 2 100       6 if (wantarray) {
166 1         2 my @returncodes;
167 1         2 foreach my $test (@_) {
168 3 100       7 push(@returncodes, ($self->inrange($test)) ? 1 : 0);
169             }
170 1         4 return @returncodes;
171             } else {
172 1         2 foreach my $test (@_) {
173 1 50       6 if (!$self->inrange($test)) {
174 0         0 return 0;
175             }
176 1         10 return 1;
177             }
178             }
179             }
180             }
181            
182             sub addrange {
183 1     1 1 2 my $self = shift;
184 1         3 $self->initialize("add", @_);
185             }
186            
187             sub delrange {
188 1     1 1 2 my $self = shift;
189 1         3 $self->initialize("del", @_);
190             }
191            
192             sub range {
193 8     8 1 47 my $self = shift;
194 8         11 my $excludeLarge = shift;
195 8 100       17 if (wantarray) {
196 5         7 my @range = keys(%{$self->{_rangehash}});
  5         71  
197 5 50 66     34 if(! $excludeLarge
198             && exists($self->{_largeRangehash})) {
199 0         0 foreach my $rangeID (keys(%{$self->{_largeRangehash}})) {
  0         0  
200 0         0 my $range = $self->{_largeRangehash}->{$rangeID};
201 0 0 0     0 if ( @$range[0] > LONG_MAX
      0        
202             || @$range[1] > LONG_MAX
203             || ( @$range[1] - @$range[0]) > LONG_MAX ) {
204 0 0       0 carp "Range to large to return" if (warnings::enabled());
205 0         0 return 0;
206             }
207            
208 0         0 @range = (@range, @$range[0]..@$range[1]);
209             }
210             }
211 5         21 my @sorted = sort {$a <=> $b} @range;
  1207         1196  
212 5         64 return @sorted;
213             }
214             else {
215 3         12 my @range = $self->range;
216 3         9 my $previous = shift @range;
217 3         7 my $format = "$previous";
218 3         4 foreach my $current (@range) {
219 159 100       225 if ($current == ($previous + 1)) {
220 157         1178 $format =~ s/\.\.$previous$//;
221 157         273 $format .= "..$current";
222             }
223             else {
224 2         3 $format .= ",$current";
225             }
226 159         182 $previous = $current;
227             }
228 3         24 return $format;
229             }
230             }
231            
232             sub size {
233 2     2 1 710 my $self = shift;
234 2         4 my @temp = keys(%{$self->{_rangehash}});;
  2         36  
235 2         11 my $size = scalar(@temp);
236 2 100       9 if(exists($self->{_largeRangehash})) {
237 1         3 foreach my $rangeID (keys(%{$self->{_largeRangehash}})) {
  1         3  
238 1         4 my $range = $self->{_largeRangehash}->{$rangeID};
239 1         5 $size += (@$range[1] - @$range[0]) + 1;
240             }
241             }
242 2         18 return $size;
243             }
244            
245             sub rangeList {
246 2     2 1 25 my $self = shift;
247 2         3 my @return;
248             # Get the range as an array (excluding large ones)
249 2         9 my @range = $self->range(1);
250            
251             # If we have any ranges
252 2 100       9 if (@range) {
253            
254             # Get the first element in the array range
255 1         2 my $previous = shift(@range);
256 1         3 my @sub = ($previous);
257            
258             # Process ranges stored as arrays
259 1         3 foreach my $current (@range) {
260 61 100       76 if ($current == ($previous + 1)) {
261 59         58 $sub[1] = $current;
262             } else {
263 2         5 push(@return,[@sub]);
264 2         3 @sub = ($current);
265             }
266 61         61 $previous = $current;
267             }
268 1         21 push(@return,[@sub]);
269             }
270            
271             # Process ranges stored as large range hash entries
272 2 50       7 if($self->{_largeRangehash}) {
273 2         4 while(my @range = each(%{$self->{_largeRangehash}}) ) {
  4         19  
274 2         10 push(@return, [int($range[1][0]), int($range[1][1])]);
275             }
276             }
277            
278 2         12 return @return;
279             }
280            
281             1;
282             __END__