File Coverage

blib/lib/Array/IntSpan.pm
Criterion Covered Total %
statement 202 217 93.0
branch 111 132 84.0
condition 42 48 87.5
subroutine 19 23 82.6
pod 10 18 55.5
total 384 438 87.6


line stmt bran cond sub pod time code
1             #
2             # This file is part of Array-IntSpan
3             #
4             # This software is Copyright (c) 2014 by Dominique Dumont.
5             #
6             # This is free software, licensed under:
7             #
8             # The Artistic License 2.0 (GPL Compatible)
9             #
10             ##########################################################################
11             #
12             # Array::IntSpan - a Module for handling arrays using IntSpan techniques
13             #
14             # Author: Toby Everett, Dominique Dumont
15             #
16             ##########################################################################
17             # Copyright 2003-2004,2010,2014 Dominique Dumont. All rights reserved.
18             # Copyright 2000 Toby Everett. All rights reserved.
19             #
20             # This file is distributed under the Artistic 2.0 License. See
21             # https://www.perlfoundation.org/artistic-license-20.html
22             #
23             # For comments, questions, bugs or general interest, feel free to
24             # contact Dominique Dumont at ddumont@cpan.org
25             ##########################################################################
26              
27 8     8   515060 use strict;
  8         65  
  8         239  
28 8     8   38 use warnings ;
  8         16  
  8         20582  
29              
30             package Array::IntSpan;
31             $Array::IntSpan::VERSION = '2.004';
32              
33 29     29 0 134 sub min { my @a = sort {$a <=> $b} @_ ; return $a[0] ; }
  29         60  
  29         49  
34 25     25 0 84 sub max { my @a = sort {$b <=> $a} @_ ; return $a[0] ; }
  25         70  
  25         61  
35              
36             sub new {
37 58     58 1 27597 my $class = shift;
38              
39 58         112 my $self = [@_];
40 58         99 bless $self, $class;
41 58         167 $self->_check_structure;
42 58         118 return $self;
43             }
44              
45             #internal function
46             sub search {
47 143     143 0 4770 my ($self,$start,$end,$index) = @_ ;
48              
49             # Binary search for the first element that is *entirely* before the
50             # element to be inserted
51 143         298 while ($start < $end) {
52 258         421 my $mid = int(($start+$end)/2);
53 258 100       422 if ($self->[$mid][1] < $index) {
54 77         159 $start = $mid+1;
55             } else {
56 181         330 $end = $mid;
57             }
58             }
59 143         241 return $start ;
60             }
61              
62             # clear the range. Note the the $self ref is preserved
63             sub clear {
64 0     0 1 0 my $self = shift;
65 0         0 @$self = () ;
66             }
67              
68             sub set_range_as_string {
69 1     1 1 7 my $self = shift;
70 1         3 my $str = shift;
71              
72 1         7 $str =~ s/\s//g;
73              
74 1         6 foreach my $substr (split /,/, $str) {
75 3 100       16 my @range = $substr =~ /-/ ? split /-/,$substr : ($substr) x 2;
76 3         7 $self->set_range(@range, @_);
77             }
78             }
79              
80             sub set {
81 1     1 1 6 my $self = shift;
82 1         2 my $idx = shift;
83              
84 1         4 $self->set_range($idx, $idx, @_);
85             }
86              
87             sub set_range {
88 11     11 1 1383 my $self = shift;
89              
90             #Test that we were passed appropriate values
91 11 50 66     41 @_ == 3 or @_ == 4 or
92             croak("Array::IntSpan::set_range should be called with 3 values and an ".
93             "optional code ref.");
94 11 50       28 $_[0] <= $_[1] or
95             croak("Array::IntSpan::set_range called with bad indices: ".
96             "$_[0] and $_[1].");
97              
98 11 50 66     37 not defined $_[3] or ref($_[3]) eq 'CODE' or
99             croak("Array::IntSpan::set_range called without 4th parameter ".
100             "set as a sub ref");
101              
102 11         26 my ($offset,$length,@list) = $self -> get_splice_parms(@_) ;
103              
104             #print "splice $offset,$length,@list\n";
105 11         31 splice @$self, $offset,$length,@list ;
106              
107 11 100       76 return $length ? 1 : 0 ;
108             }
109              
110             # not well tested or documented. May be useless...
111             sub check_clobber {
112 0     0 0 0 my $self = shift;
113              
114 0         0 my @clobbered = $self->clobbered_items(@_) ;
115              
116 0         0 map {warn "will clobber @$_ with @_\n" ;} @clobbered ;
  0         0  
117              
118 0         0 return @clobbered ;
119             }
120              
121             sub get_element
122             {
123 0     0 1 0 my ($self,$idx) = @_;
124 0         0 my $ref = $self->[$idx] ;
125 0 0       0 return () unless defined $ref ;
126 0         0 return @$ref ;
127             }
128              
129             # call-back:
130             # filler (start, end)
131             # copy (start, end, payload )
132             # set (start, end, payload)
133              
134             sub get_range {
135 32     32 1 9216 my $self = shift;
136             #my($new_elem) = [@_];
137 32         67 my ($start_elem,$end_elem, $filler, $copy, $set) = @_ ;
138              
139 32 50   21   134 $copy = sub{$_[2];} unless defined $copy ;
  21         42  
140              
141 32         63 my $end_range = $#{$self};
  32         52  
142 32         57 my $range_size = @$self ; # nb of elements
143              
144             # Before we binary search, first check if we fall before the range
145 32 100 100     126 if ($end_range < 0 or $self->[$end_range][1] < $start_elem)
146             {
147 5 100       32 my @arg = ref($filler) ?
    100          
148             ([$start_elem,$end_elem,&$filler($start_elem,$end_elem)]) :
149             defined $filler ? ([@_]) : () ;
150 5 100       20 push @$self, @arg if @arg;
151 5         18 return ref($self)->new(@arg) ;
152             }
153              
154             # Before we binary search, first check if we fall after the range
155 27 100       60 if ($end_elem < $self->[0][0])
156             {
157 1 50       6 my @arg = ref($filler) ?
    50          
158             ([$start_elem,$end_elem,&$filler($start_elem,$end_elem)]) :
159             defined $filler ? ([@_]) : () ;
160 1 50       4 unshift @$self, @arg if @arg;
161 1         4 return ref($self)->new(@arg) ;
162             }
163              
164 26         60 my $start = $self->search(0, $range_size, $start_elem) ;
165 26         46 my $end = $self->search($start,$range_size, $end_elem) ;
166              
167 26         44 my $start_offset = $start_elem - $self->[$start][0] ;
168 26 100       58 my $end_offset = defined $self->[$end] ?
169             $end_elem - $self->[$end][0] : undef ;
170              
171             #print "get_range: start $start, end $end, start_offset $start_offset";
172             #print ", end_offset $end_offset" if defined $end_offset ;
173             #print "\n";
174              
175 26         39 my @extracted ;
176             my @replaced ;
177 26         32 my $length = 0;
178              
179             # handle the start
180 26 100 100     63 if (defined $filler and $start_offset < 0)
181             {
182 4         9 my $e = min ($end_elem, $self->[$start][0]-1) ;
183 4 100       12 my $new = ref($filler) ? &$filler($start_elem, $e) : $filler ;
184 4         7 my @a = ($start_elem, $e, $new) ;
185             # don't use \@a, as we don't want @extracted and @replaced to
186             # point to the same memory area. But $new must point to the same
187             # object
188 4         10 push @extracted, [ @a ] ;
189 4         8 push @replaced, [ @a ] ;
190             }
191              
192 26 100       50 if ($self->[$start][0] <= $end_elem)
193             {
194 24         48 my $s = max ($start_elem,$self->[$start][0]) ;
195 24         57 my $e = min ($end_elem, $self->[$start][1]) ;
196 24         33 my $payload = $self->[$start][2] ;
197 24 100       49 if ($self->[$start][0] < $s)
198             {
199 10         12 my $s1 = $self->[$start][0];
200 10         14 my $e1 = $s - 1 ;
201 10         19 push @replaced, [$s1, $e1 , &$copy($s1,$e1,$payload) ];
202             }
203             # must duplicate the start, end variable
204 24         45 push @extracted, [$s, $e, $payload];
205 24         52 push @replaced, [$s, $e, $payload];
206 24 100       59 if ($e < $self->[$start][1])
207             {
208 7         9 my $s3 = $e+1 ;
209 7         10 my $e3 = $self->[$start][1] ;
210 7         15 push @replaced, [$s3, $e3, &$copy($s3, $e3,$payload) ] ;
211             }
212 24 50       42 &$set($s,$e, $payload) if defined $set ;
213 24         31 $length ++ ;
214             }
215              
216             # handle the middle if any
217 26 100       56 if ($start + 1 <= $end -1 )
218             {
219             #print "adding " ;
220 6         15 foreach my $idx ( $start+1 .. $end - 1)
221             {
222             #print "idx $idx," ;
223 8 100       18 if (defined $filler)
224             {
225 4         7 my $start_fill = $self->[$idx-1][1]+1 ;
226 4         6 my $end_fill = $self->[$idx][0]-1 ;
227 4 100       7 if ($start_fill <= $end_fill)
228             {
229 2 100       5 my $new = ref($filler) ? &$filler($start_fill, $end_fill)
230             : $filler ;
231 2         5 push @extracted, [$start_fill, $end_fill, $new] ;
232 2         5 push @replaced, [$start_fill, $end_fill, $new];
233             }
234             }
235 8         11 push @extracted, [@{$self->[$idx]}];
  8         16  
236 8         13 push @replaced , [@{$self->[$idx]}];
  8         13  
237 8         12 $length++ ;
238             }
239             #print "\n";
240             }
241              
242             # handle the end
243 26 100       45 if ($end > $start)
244             {
245 14 100       28 if (defined $filler)
246             {
247             # must add end element filler
248 7         10 my $start_fill = $self->[$end-1][1]+1 ;
249 7 100 100     22 my $end_fill = (not defined $end_offset or $end_offset < 0) ?
250             $end_elem : $self->[$end][0]-1 ;
251 7 100       15 if ($start_fill <= $end_fill)
252             {
253 5 100       10 my $new = ref($filler) ? &$filler($start_fill, $end_fill) :
254             $filler ;
255 5         9 push @extracted, [$start_fill, $end_fill, $new] ;
256 5         9 push @replaced, [$start_fill, $end_fill, $new];
257             }
258             }
259              
260 14 100 100     54 if (defined $end_offset and $end_offset >= 0)
261             {
262 6         9 my $payload = $self->[$end][2] ;
263 6         9 my $s = $self->[$end][0] ;
264 6         13 my @a = ($s,$end_elem, $payload) ;
265 6         10 push @extracted, [@a];
266 6         12 push @replaced , [@a];
267 6 100       14 if ($end_elem < $self->[$end][1])
268             {
269 4         9 my $s2 = $end_elem + 1 ;
270 4         7 my $e2 = $self->[$end][1] ;
271 4         9 push @replaced , [$s2, $e2, &$copy($s2,$e2,$payload)];
272             }
273 6 50       13 &$set($s,$end_elem, $payload) if defined $set ;
274 6         9 $length++ ;
275             }
276             }
277              
278 26 100       44 if (defined $filler)
279             {
280 11         28 splice (@$self, $start,$length , @replaced) ;
281             }
282              
283 26         70 my $ret = ref($self)->new(@extracted) ;
284 26         120 return $ret ;
285             }
286              
287             sub clobbered_items {
288 8     8 0 9960 my $self = shift;
289 8         17 my($range_start,$range_stop,$range_value) = @_;
290              
291 8         18 my $item = $self->get_range($range_start,$range_stop) ;
292              
293 8         24 return grep {$_->[2] ne $range_value} @$item ;
  8         33  
294             }
295              
296              
297             # call-back:
298             # set (start, end, payload)
299             sub consolidate {
300 14     14 1 1500 my ($self,$bottom,$top,$set) = @_;
301              
302 14 100 100     60 $bottom = 0 if (not defined $bottom or $bottom < 0 );
303 14 100 100     50 $top = $#$self if (not defined $top or $top > $#$self) ;
304              
305             #print "consolidate from $top to $bottom\n";
306              
307 14         37 for (my $i= $top; $i>0; $i--)
308             {
309 45 100 100     225 if ($self->[$i][2] eq $self->[$i-1][2] and
310             $self->[$i][0] == $self->[$i-1][1]+1 )
311             {
312             #print "consolidate splice ",$i-1,",2\n";
313 9         21 my ($s,$e,$p) = ($self->[$i-1][0], $self->[$i][1], $self->[$i][2]);
314 9         26 splice @$self, $i-1, 2, [$s, $e, $p] ;
315 9 100       29 $set->($s,$e,$p) if defined $set ;
316             }
317             }
318              
319             }
320              
321             sub set_consolidate_range {
322 13     13 0 1253 my $self = shift;
323              
324             #Test that we were passed appropriate values
325 13 50 66     48 @_ == 3 or @_ == 5 or
326             croak("Array::IntSpan::set_range should be called with 3 values ".
327             "and 2 optional code ref.");
328 13 50       29 $_[0] <= $_[1] or
329             croak("Array::IntSpan::set_range called with bad indices: $_[0] and $_[1].");
330              
331 13 50 66     37 not defined $_[3] or ref($_[3]) eq 'CODE' or
332             croak("Array::IntSpan::set_range called without 4th parameter set as a sub ref");
333              
334 13         39 my ($offset,$length,@list) = $self -> get_splice_parms(@_[0,1,2,3]) ;
335              
336             #print "splice $offset,$length\n";
337 13         33 splice @$self, $offset,$length,@list ;
338 13         23 my $nb = @list ;
339              
340 13         53 $self->consolidate($offset - 1 , $offset+ $nb , $_[4]) ;
341              
342 13 100       101 return $length ? 1 : 0 ;#($b , $t ) ;
343              
344             }
345              
346             # get_range_list
347             # scalar context -> return a string
348             # list context => returns list of list
349              
350             sub get_range_list {
351 2     2 1 7 my ($self, %options) = @_;
352 2 100       6 if (wantarray) {
353 1         3 return map { [ @$_[0,1] ] } @$self;
  3         11  
354             }
355             else {
356             return join ', ' , map {
357 1         3 my ($a,$b) = @$_;
  3         7  
358 3 50       16 $a == $b ? $a
    100          
359             : $a+1==$b ? join(', ',$a,$b)
360             : join('-',$a,$b);
361             } @$self;
362             }
363             }
364              
365             # internal function
366             # call-back:
367             # copy (start, end, payload )
368             sub get_splice_parms {
369 52     52 0 19023 my $self = shift;
370 52         106 my ($start_elem,$end_elem,$value,$copy) = @_ ;
371              
372 52         77 my $end_range = $#{$self};
  52         70  
373 52         72 my $range_size = @$self ; # nb of elements
374              
375             #Before we binary search, we'll first check to see if this is an append operation
376 52 100 100     220 if ( $end_range < 0 or
377             $self->[$end_range][1] < $start_elem
378             )
379             {
380 8 50       44 return defined $value ? ( $range_size, 0, [$start_elem,$end_elem,$value]) :
381             ($range_size, 0) ;
382             }
383              
384             # Check for prepend operation
385 44 100       102 if ($end_elem < $self->[0][0] ) {
386 2 50       11 return defined $value ? ( 0 , 0, [$start_elem,$end_elem,$value]) : (0,0);
387             }
388              
389             #Binary search for the first element after the last element that is entirely
390             #before the element to be inserted (say that ten times fast)
391 42         100 my $start = $self->search(0, $range_size, $start_elem) ;
392 42         76 my $end = $self->search($start,$range_size, $end_elem) ;
393              
394 42         73 my $start_offset = $start_elem - $self->[$start][0] ;
395 42 100       82 my $end_offset = defined $self->[$end] ?
396             $end_elem - $self->[$end][0] : undef ;
397              
398             #print "get_splice_parms: start $start, end $end, start_offset $start_offset";
399             #print ", end_offset $end_offset" if defined $end_offset ;
400             #print "\n";
401              
402 42         58 my @modified = () ;
403              
404             #If we are here, we need to test for whether we need to frag the
405             #conflicting element
406 42 100       79 if ($start_offset > 0) {
407 15         23 my $item = $self->[$start][2] ;
408 15         21 my $s = $self->[$start][0] ;
409 15         19 my $e = $start_elem-1 ;
410 15 100       61 my $new = defined($copy) ? $copy->($s,$e,$item) : $item ;
411 15         53 push @modified ,[$s, $e, $new ];
412             }
413              
414 42 100       132 push @modified, [$start_elem,$end_elem,$value] if defined $value ;
415              
416             #Do a fragmentation check
417 42 100 100     184 if (defined $end_offset
      100        
418             and $end_offset >= 0
419             and $end_elem < $self->[$end][1]
420             ) {
421 11         16 my $item = $self->[$end][2] ;
422 11         22 my $s = $end_elem+1 ;
423 11         16 my $e = $self->[$end][1] ;
424 11 100       23 my $new = defined($copy) ? $copy->($s,$e,$item) : $item ;
425 11         71 push @modified , [$s, $e, $new] ;
426             }
427              
428 42 100 100     145 my $extra = (defined $end_offset and $end_offset >= 0) ? 1 : 0 ;
429              
430 42         120 return ($start, $end - $start + $extra , @modified);
431             }
432              
433             sub lookup {
434 7     7 1 449 my $self = shift;
435 7         11 my($key) = @_;
436              
437 7         10 my($start, $end) = (0, $#{$self});
  7         17  
438 7 100       21 return undef unless $end >= 0 ; # completely empty span
439              
440 6         12 while ($start < $end) {
441 11         24 my $mid = int(($start+$end)/2);
442 11 100       17 if ($self->[$mid][1] < $key) {
443 7         24 $start = $mid+1;
444             } else {
445 4         8 $end = $mid;
446             }
447             }
448 6 50 33     23 if ($self->[$start]->[0] <= $key && $self->[$start]->[1] >= $key) {
449 6         26 return $self->[$start]->[2];
450             }
451 0         0 return undef;
452             }
453              
454             sub _check_structure {
455 58     58   77 my $self = shift;
456              
457 58 100       149 return unless $#$self >= 0;
458              
459 51         129 foreach my $i (0..$#$self) {
460 122 50       135 @{$self->[$i]} == 3 or
  122         217  
461             croak("Array::IntSpan::_check_structure failed - element $i lacks 3 entries.");
462 122 50       204 $self->[$i][0] <= $self->[$i][1] or
463             croak("Array::IntSpan::_check_structure failed - element $i has bad indices.");
464 122 100       216 if ($i > 0) {
465 71 50       165 $self->[$i-1][1] < $self->[$i][0] or
466             croak("Array::IntSpan::_check_structure failed - element $i (",
467             ,$self->[$i][0],",",$self->[$i][1],
468             ") doesn't come after previous element (",
469             $self->[$i-1][0],",",$self->[$i-1][1],")");
470             }
471             }
472             }
473              
474             #The following code is courtesy of Mark Jacob-Dominus,
475             sub croak {
476 0     0 0   require Carp;
477 8     8   64 no warnings 'redefine' ;
  8         16  
  8         590  
478 0           *croak = \&Carp::croak;
479 0           goto &croak;
480             }
481              
482             1;
483              
484             __END__