File Coverage

blib/lib/Set/IntSpan/Fast/PP.pm
Criterion Covered Total %
statement 192 217 88.4
branch 58 68 85.2
condition 11 20 55.0
subroutine 35 41 85.3
pod 0 28 0.0
total 296 374 79.1


line stmt bran cond sub pod time code
1             package Set::IntSpan::Fast::PP;
2              
3 7     7   21498 use warnings;
  7         16  
  7         204  
4 7     7   35 use strict;
  7         12  
  7         190  
5 7     7   36 use Carp;
  7         12  
  7         536  
6 7     7   304 use Data::Types qw(is_int);
  7         11  
  7         335  
7 7     7   68 use List::Util qw(min max);
  7         11  
  7         1165  
8              
9             =head1 NAME
10              
11             Set::IntSpan::Fast::PP - Pure Perl implementation.
12              
13             =head1 VERSION
14              
15             This document describes Set::IntSpan::Fast::PP version 1.15
16              
17             =cut
18              
19             our $VERSION = '1.15';
20              
21 7     7   56 use constant POSITIVE_INFINITY => 2**31 - 2;
  7         18  
  7         498  
22 7     7   35 use constant NEGATIVE_INFINITY => -2**31 + 100;
  7         14  
  7         21126  
23              
24             sub new {
25 5758     5758 0 3147185 my $class = shift;
26 5758         25827 my $self = bless [], $class;
27 5758 100       17565 $self->add_from_string( @_ ) if @_;
28 5758         24618 return $self;
29             }
30              
31             sub invert {
32 6466     6466 0 10307 my $self = shift;
33              
34 6466 100       14705 if ( $self->is_empty() ) {
35              
36             # Empty set
37 72         219 @$self = ( NEGATIVE_INFINITY, POSITIVE_INFINITY );
38             }
39             else {
40              
41             # Either add or remove infinity from each end. The net
42             # effect is always an even number of additions and deletions
43 6394 100       13731 if ( $self->[0] == NEGATIVE_INFINITY ) {
44 3107         3218 shift @{$self};
  3107         5052  
45             }
46             else {
47 3287         4259 unshift @{$self}, NEGATIVE_INFINITY;
  3287         9728  
48             }
49              
50 6394 100       16596 if ( $self->[-1] == POSITIVE_INFINITY ) {
51 3107         3145 pop @{$self};
  3107         12054  
52             }
53             else {
54 3287         3959 push @{$self}, POSITIVE_INFINITY;
  3287         8082  
55             }
56             }
57             }
58              
59             sub copy {
60 510     510 0 1156 my $self = shift;
61 510         775 my $class = ref $self;
62 510         1133 my $copy = $class->new();
63             # This might not work for subclasses - in which case they should
64             # override copy
65 510         1746 @$copy = @$self;
66 510         1383 return $copy;
67             }
68              
69 0     0 0 0 sub empty { @{ $_[0] } = () }
  0         0  
70              
71             sub add {
72 0     0 0 0 my $self = shift;
73 0         0 $self->add_range( $self->_list_to_ranges( @_ ) );
74             }
75              
76             sub remove {
77 0     0 0 0 my $self = shift;
78 0         0 $self->remove_range( $self->_list_to_ranges( @_ ) );
79             }
80              
81             sub add_range {
82 15880     15880 0 1288582 my $self = shift;
83              
84             $self->_iterate_ranges(
85             @_,
86             sub {
87 22981     22981   39814 my ( $from, $to ) = @_;
88              
89 22981         50026 my $fpos = $self->_find_pos( $from );
90 22981         59137 my $tpos = $self->_find_pos( $to + 1, $fpos );
91              
92 22981 100       92434 $from = $self->[ --$fpos ] if ( $fpos & 1 );
93 22981 100       48851 $to = $self->[ $tpos++ ] if ( $tpos & 1 );
94              
95 22981         173306 splice @$self, $fpos, $tpos - $fpos, ( $from, $to );
96             }
97 15880         101702 );
98             }
99              
100             sub add_from_string {
101 15     15 0 2446 my $self = shift;
102              
103 15         23 my $ctl = {};
104 15         59 my $match_number = qr/\s* (-?\d+) \s*/x;
105 15         255 my $match_single = qr/^ $match_number $/x;
106 15         40 my $match_range;
107              
108 15         51 my @to_add = ();
109              
110             # Iterate args. Default punctuation spec prepended.
111 15         92 for my $el ( { sep => qr/,/, range => qr/-/, }, @_ ) {
112              
113             # Allow parsing options to be set.
114 40 100       126 if ( 'HASH' eq ref $el ) {
115 21         91 %$ctl = ( %$ctl, %$el );
116 21         47 for ( values %$ctl ) {
117 42 100       116 $_ = quotemeta( $_ ) unless ref $_ eq 'Regexp';
118             }
119 21         478 $match_range = qr/^ $match_number $ctl->{range} $match_number $/x;
120             }
121             else {
122 19         114 for my $part ( split $ctl->{sep}, $el ) {
123 49 100       352 if ( my ( $start, $end ) = ( $part =~ $match_range ) ) {
    50          
124 22         60 push @to_add, $start, $end;
125             }
126             elsif ( my ( $el ) = ( $part =~ $match_single ) ) {
127 27         90 push @to_add, $el, $el;
128             }
129             else {
130 0 0       0 croak "Invalid range string"
131             unless $part =~ $match_single;
132             }
133             }
134             }
135             }
136              
137 15         88 $self->add_range( @to_add );
138             }
139              
140             sub remove_range {
141 2854     2854 0 2205463 my $self = shift;
142              
143 2854         8219 $self->invert();
144 2854         9502 $self->add_range( @_ );
145 2854         115342 $self->invert();
146             }
147              
148             sub remove_from_string {
149 1     1 0 2 my $self = shift;
150              
151 1         8 $self->invert();
152 1         3 $self->add_from_string( @_ );
153 1         49 $self->invert();
154             }
155              
156             sub merge {
157 0     0 0 0 my $self = shift;
158              
159 0         0 for my $other ( @_ ) {
160 0         0 my $iter = $other->iterate_runs();
161 0         0 while ( my ( $from, $to ) = $iter->() ) {
162 0         0 $self->add_range( $from, $to );
163             }
164             }
165             }
166              
167             sub compliment {
168 0     0 0 0 croak "That's very kind of you - but I expect you meant complement()";
169             }
170              
171             sub complement {
172 506     506 0 1107 my $new = shift->copy();
173 506         1005 $new->invert();
174 506         1698 return $new;
175             }
176              
177             sub union {
178 3     3 0 1087 my $new = shift->copy;
179 3         16 $new->merge( @_ );
180 3         126 return $new;
181             }
182              
183             sub intersection {
184 250     250 0 11770 my $self = shift;
185 250         358 my $class = ref $self;
186 250         1142 my $new = $class->new();
187 250         383 $new->merge( map { $_->complement() } $self, @_ );
  504         1259  
188 250         35325 $new->invert();
189 250         1340 return $new;
190             }
191              
192             sub xor {
193 1     1 0 65 my $self = shift;
194 1         4 return $self->union( @_ )
195             ->intersection( $self->intersection( @_ )->complement() );
196             }
197              
198             sub diff {
199 1     1 0 66 my $self = shift;
200 1         2 my $other = shift;
201 1         5 return $self->intersection( $other->union( @_ )->complement() );
202             }
203              
204             sub is_empty {
205 6466     6466 0 8500 my $self = shift;
206 6466         22169 return @$self == 0;
207             }
208              
209             *contains = *contains_all;
210              
211             sub contains_any {
212 3     3 0 14 my $self = shift;
213              
214 3         8 for my $i ( @_ ) {
215 7         18 my $pos = $self->_find_pos( $i + 1 );
216 7 100       22 return 1 if $pos & 1;
217             }
218              
219 2         10 return;
220             }
221              
222             sub contains_all {
223 80     80 0 542 my $self = shift;
224              
225 80         158 for my $i ( @_ ) {
226 80         185 my $pos = $self->_find_pos( $i + 1 );
227 80 100       224 return unless $pos & 1;
228             }
229              
230 40         81 return 1;
231             }
232              
233             sub contains_all_range {
234 242     242 0 996 my ( $self, $lo, $hi ) = @_;
235              
236 242 50       482 croak "Range limits must be in ascending order" if $lo > $hi;
237              
238 242         706 my $pos = $self->_find_pos( $lo + 1 );
239 242   100     1449 return ( $pos & 1 ) && $hi < $self->[$pos];
240             }
241              
242             sub cardinality {
243 139     139 0 99756 my $self = shift;
244              
245 139         198 my $card = 0;
246 139         435 my $iter = $self->iterate_runs( @_ );
247 139         298 while ( my ( $from, $to ) = $iter->() ) {
248 520         1509 $card += $to - $from + 1;
249             }
250              
251 139         1202 return $card;
252             }
253              
254             sub superset {
255 2     2 0 403 my $other = pop;
256 2         14 return $other->subset( reverse( @_ ) );
257             }
258              
259             sub subset {
260 4     4 0 8 my $self = shift;
261 4   33     13 my $other = shift || croak "I need two sets to compare";
262 4         10 return $self->equals( $self->intersection( $other ) );
263             }
264              
265             sub equals {
266 273 50   273 0 987 return unless @_;
267              
268             # Array of array refs
269 273         510 my @edges = @_;
270 273         395 my $medge = scalar( @edges ) - 1;
271              
272 273         361 POS: for ( my $pos = 0;; $pos++ ) {
273 597         871 my $v = $edges[0]->[$pos];
274 597 100       907 if ( defined( $v ) ) {
275 510         1048 for ( @edges[ 1 .. $medge ] ) {
276 595         1026 my $vv = $_->[$pos];
277 595 100 100     3590 return unless defined( $vv ) && $vv == $v;
278             }
279             }
280             else {
281 87         172 for ( @edges[ 1 .. $medge ] ) {
282 95 100       321 return if defined $_->[$pos];
283             }
284             }
285              
286 393 100       998 last POS unless defined( $v );
287             }
288              
289 69         247 return 1;
290             }
291              
292             sub as_array {
293 8426     8426 0 2485947 my $self = shift;
294 8426         18018 my @ar = ();
295 8426         18435 my $iter = $self->iterate_runs();
296 8426         17470 while ( my ( $from, $to ) = $iter->() ) {
297 34361         141611 push @ar, ( $from .. $to );
298             }
299              
300 8426         98382 return @ar;
301             }
302              
303             sub as_string {
304 10     10 0 26 my $self = shift;
305 10         65 my $ctl = { sep => ',', range => '-' };
306 10 100       43 %$ctl = ( %$ctl, %{ $_[0] } ) if @_;
  3         16  
307 10         46 my $iter = $self->iterate_runs();
308 10         18 my @runs = ();
309 10         24 while ( my ( $from, $to ) = $iter->() ) {
310 23 100       84 push @runs,
311             $from == $to ? $from : join( $ctl->{range}, $from, $to );
312             }
313 10         130 return join( $ctl->{sep}, @runs );
314             }
315              
316             sub iterate_runs {
317 12283     12283 0 3966532 my $self = shift;
318              
319 12283 100       28208 if ( @_ ) {
320              
321             # Clipped iterator
322 138         213 my ( $clip_lo, $clip_hi ) = @_;
323              
324 138         436 my $pos = $self->_find_pos( $clip_lo ) & ~1;
325 138         405 my $limit = ( $self->_find_pos( $clip_hi + 1, $pos ) + 1 ) & ~1;
326              
327             return sub {
328 658 100       1886 TRY: {
329 628     628   764 return if $pos >= $limit;
330              
331 520         1900 my @r = ( $self->[$pos], $self->[ $pos + 1 ] - 1 );
332 520         660 $pos += 2;
333              
334             # Catch some edge cases
335 520 100       929 redo TRY if $r[1] < $clip_lo;
336 490 50       1387 return if $r[0] > $clip_hi;
337              
338             # Clip to range
339 490 100       859 $r[0] = $clip_lo if $r[0] < $clip_lo;
340 490 100       789 $r[1] = $clip_hi if $r[1] > $clip_hi;
341              
342 490         1882 return @r;
343             }
344 138         817 };
345             }
346             else {
347              
348             # Unclipped iterator
349 12145         16361 my $pos = 0;
350 12145         16480 my $limit = scalar( @$self );
351              
352             return sub {
353 60873 100   60873   254478 return if $pos >= $limit;
354 48728         124847 my @r = ( $self->[$pos], $self->[ $pos + 1 ] - 1 );
355 48728         54521 $pos += 2;
356 48728         170453 return @r;
357 12145         85832 };
358             }
359              
360             }
361              
362             sub _list_to_ranges {
363 0     0   0 my $self = shift;
364 0         0 my @list = sort { $a <=> $b } @_;
  0         0  
365 0         0 my @ranges = ();
366 0         0 my $count = scalar( @list );
367 0         0 my $pos = 0;
368 0         0 while ( $pos < $count ) {
369 0         0 my $end = $pos + 1;
370 0   0     0 $end++ while $end < $count && $list[$end] <= $list[ $end - 1 ] + 1;
371 0         0 push @ranges, ( $list[$pos], $list[ $end - 1 ] );
372 0         0 $pos = $end;
373             }
374              
375 0         0 return @ranges;
376             }
377              
378             # Return the index of the first element >= the supplied value. If the
379             # supplied value is larger than any element in the list the returned
380             # value will be equal to the size of the list.
381             sub _find_pos {
382 45962     45962   55872 my $self = shift;
383 45962         48426 my $val = shift;
384 45962   100     165241 my $low = shift || 0;
385              
386 45962         74287 my $high = scalar( @$self );
387              
388 45962         104062 while ( $low < $high ) {
389 64996         112768 my $mid = int( ( $low + $high ) / 2 );
390 64996 100       138925 if ( $val < $self->[$mid] ) {
    100          
391 25511         67169 $high = $mid;
392             }
393             elsif ( $val > $self->[$mid] ) {
394 33748         92341 $low = $mid + 1;
395             }
396             else {
397 5737         11962 return $mid;
398             }
399             }
400              
401 40225         68976 return $low;
402             }
403              
404             sub _iterate_ranges {
405 15880     15880   20040 my $self = shift;
406 15880         18835 my $cb = pop;
407              
408 15880         18858 my $count = scalar( @_ );
409              
410 15880 50       38414 croak "Range list must have an even number of elements"
411             if ( $count % 2 ) != 0;
412              
413 15880         42198 for ( my $p = 0; $p < $count; $p += 2 ) {
414 22981         39929 my ( $from, $to ) = ( $_[$p], $_[ $p + 1 ] );
415 22981 50 33     58517 croak "Range limits must be integers"
416             unless is_int( $from ) && is_int( $to );
417 22981 50       452660 croak "Range limits must be in ascending order"
418             unless $from <= $to;
419 22981 50 33     111501 croak "Value out of range"
420             unless $from >= NEGATIVE_INFINITY && $to <= POSITIVE_INFINITY;
421              
422             # Internally we store inclusive/exclusive ranges to
423             # simplify comparisons, hence '$to + 1'
424 22981         87857 $cb->( $from, $to + 1 );
425             }
426             }
427              
428             1;
429             __END__