File Coverage

blib/lib/Number/Range/Regex/CompoundRange.pm
Criterion Covered Total %
statement 172 180 95.5
branch 55 72 76.3
condition 14 15 93.3
subroutine 19 21 90.4
pod 0 14 0.0
total 260 302 86.0


line stmt bran cond sub pod time code
1             # Number::Range::Regex::CompoundRange
2             #
3             # Copyright 2012 Brian Szymanski. All rights reserved. This module is
4             # free software; you can redistribute it and/or modify it under the same
5             # terms as Perl itself.
6              
7             package Number::Range::Regex::CompoundRange;
8              
9 14     14   80 use strict;
  14         24  
  14         583  
10              
11 14     14   71 use vars qw ( @ISA @EXPORT @EXPORT_OK $VERSION );
  14         31  
  14         1926  
12             eval { require warnings; }; #it's ok if we can't load warnings
13              
14             require Exporter;
15 14     14   72 use base 'Exporter';
  14         24  
  14         1691  
16             @ISA = qw( Exporter Number::Range::Regex::Range );
17              
18             $VERSION = '0.32';
19              
20 14     14   8929 use Number::Range::Regex::Util;
  14         51  
  14         4531  
21 14     14   8803 use Number::Range::Regex::Util::inf qw ( neg_inf pos_inf );
  14         38  
  14         46783  
22              
23             sub new {
24 918 50   918 0 10955 my $opts = option_mangler( ref $_[-1] eq 'HASH' ? pop : undef );
25 918         2205 my ($class, @ranges) = @_;
26             # TODO: do we need to collapse&sort the ranges? either by calling
27             # multi_union (which has a collapsing effect) or by an explicit sort
28             # by min + verify no overlaps + _collapse_ranges ?
29 918         8729 return bless { ranges => [ @ranges ], opts => $opts }, $class;
30             }
31              
32             sub to_string {
33 403     403 0 21933 my ($self, $passed_opts) = @_;
34 403         1959 return join(',', map { $_->to_string() } @{$self->{ranges}});
  868         5248  
  403         1202  
35             }
36              
37             sub regex {
38 216     216 0 13470 my ($self, $passed_opts) = @_;
39              
40 216         1010 my $opts = option_mangler( $self->{opts}, $passed_opts );
41              
42 216 50       769 my $separator = $opts->{readable} ? ' | ' : '|';
43 216         367 my $regex_str;
44 216 100       337 if(@{$self->{ranges}}) {
  216         622  
45 197         321 $regex_str = join $separator, map { $_->regex( { %$opts, comment => 0 } ) } @{$self->{ranges}};
  437         4971  
  197         398  
46             } else {
47 19         33 $regex_str = '(?!)'; # never matches
48             }
49 216 50       1026 $regex_str = " $regex_str " if $opts->{readable};
50              
51 216 50       539 my $modifier_maybe = $opts->{readable} ? '(?x)' : '';
52 216         422 my ($begin_comment_maybe, $end_comment_maybe) = ('', '');
53 216 50       546 if($opts->{comment}) {
54 216         620 my $comment = "Number::Range::Regex::CompoundRange[".$self->to_string."]";
55 216 50       1032 $begin_comment_maybe = $opts->{readable} ? " # begin $comment" : "(?# begin $comment )";
56 216 50       923 $end_comment_maybe = $opts->{readable} ? " # end $comment" : "(?# end $comment )";
57             }
58 216         5415 return qr/(?:$begin_comment_maybe$modifier_maybe(?:$regex_str)$end_comment_maybe)/;
59             }
60              
61             sub _do_unequal_min {
62             #warn "in _do_unequal_min";
63 513     513   796 my ($self, $lower, $upper, $ptr, $ranges) = @_;
64 513 100       1702 if( $lower->{max} > $upper->{max} ) {
    100          
65             # 3 ranges, last of which may yet overlap
66 47         224 my $r1 = Number::Range::Regex::SimpleRange->new( $lower->{min}, $upper->{min}-1 );
67 47         89 my $r2 = $upper;
68 47         235 my $r3 = Number::Range::Regex::SimpleRange->new( $upper->{max}+1, $lower->{max} );
69             #warn "l: $lower->{min}..$lower->{max} -> $r1->{min}..$r1->{max},$r2->{min}..$r2->{max},$r3->{min}..$r3->{max}";
70 47         145 splice( @$ranges, $$ptr, 1, ($r1, $r2, $r3) );
71 47         392 $$ptr += 2; # $r3 may overlap something else
72             } elsif( $lower->{max} >= $upper->{min} ) {
73             # 2 ranges, latter of which may yet overlap
74 32         177 my $r1 = Number::Range::Regex::SimpleRange->new( $lower->{min}, $upper->{min}-1 );
75 32         180 my $r2 = Number::Range::Regex::SimpleRange->new( $upper->{min}, $lower->{max} );
76             #warn "l: $lower->{min}..$lower->{max} -> $r1->{min}..$r1->{max},$r2->{min}..$r2->{max}";
77 32         104 splice( @$ranges, $$ptr, 1, ($r1, $r2 ) );
78 32         302 $$ptr += 1;
79             } else { # $lower->{max} < $upper->{min}
80             # 1 range, no overlap
81             #warn "l: $lower->{min}..$lower->{max} is ok";
82 434         2563 $$ptr++;
83             }
84             }
85              
86             sub sectionify {
87 725     725 0 1033 my ($self, $other) = @_;
88              
89 725         815 my @s_ranges = @{$self->{ranges}};
  725         6850  
90 725 50       7753 my @o_ranges = $other->isa('Number::Range::Regex::CompoundRange') ? @{$other->{ranges}} :
  44 100       256  
91             $other->isa('Number::Range::Regex::SimpleRange') ? ( $other ) :
92             die "other is neither a simple nor compound range!";
93              
94             #warn "s_ranges1: ".join ",", map { "$_->{min}..$_->{max}" } @s_ranges;
95             #warn "o_ranges1: ".join ",", map { "$_->{min}..$_->{max}" } @o_ranges;
96              
97             # munge ranges so that there are no partial overlaps - only
98             # non-overlaps and complete overlaps e.g:
99             # if s=(6..12) and o=(7..13):
100             # s=(6,7..12) and o=(7..12,13);
101             # if s=(6..12) and o=(7..9):
102             # s=(6,7..9,10..12) and o=(7..9);
103 725         1598 my ($s_ptr, $o_ptr) = (0, 0);
104 725   100     3087 while( ($s_ptr < @s_ranges) && ($o_ptr < @o_ranges) ) {
105             #warn "s_ranges: @s_ranges, o_ranges: @o_ranges";
106 585         856 my $this_s = $s_ranges[$s_ptr];
107 585         1469 my $this_o = $o_ranges[$o_ptr];
108             #warn "checking this_s: $this_s->{min}..$this_s->{max}, this_o: $this_o->{min}..$this_o->{max}";
109 585 100       1751 if( $this_s->{min} < $this_o->{min} ) {
    100          
110             #printf STDERR "l==s, ";
111 368         963 $self->_do_unequal_min($this_s, $this_o, \$s_ptr, \@s_ranges );
112             } elsif( $this_s->{min} > $this_o->{min} ) {
113             #printf STDERR "l==o, ";
114 145         415 $self->_do_unequal_min($this_o, $this_s, \$o_ptr, \@o_ranges );
115             } else { # $this_s->{min} == $this_o->{min}
116 72 100       307 if( $this_s->{max} < $this_o->{max} ) {
    100          
117             # 2 ranges, latter of which may yet overlap
118 13         35 my $r1 = $this_s;
119 13         72 my $r2 = Number::Range::Regex::SimpleRange->new($this_s->{max}+1, $this_o->{max} );
120 13         41 splice( @o_ranges, $o_ptr, 1, ($r1, $r2) );
121             #warn "o: $this_o->{min}..$this_o->{max} -> $r1->{min}..$r1->{max},$r2->{min}..$r2->{max}";
122 13         78 $o_ptr++; # $r2 may overlap something else
123             } elsif( $this_s->{max} > $this_o->{max} ) {
124             # 2 ranges, latter of which may yet overlap
125 19         42 my $r1 = $this_o;
126 19         117 my $r2 = Number::Range::Regex::SimpleRange->new($this_o->{max}+1, $this_s->{max} );
127 19         71 splice( @s_ranges, $s_ptr, 1, ($r1, $r2) );
128             #warn "s: $this_s->{min}..$this_s->{max} -> $r1->{min}..$r1->{max},$r2->{min}..$r2->{max}";
129 19         133 $s_ptr++; # $r2 may overlap something else
130             } else { # $this_s->{max} == $this_o->{min}
131             # 1 range, no overlap
132             #warn "s/o: $this_o->{min}..$this_o->{max} is ok";
133 40         64 $s_ptr++;
134 40         170 $o_ptr++;
135             }
136             }
137             }
138              
139             #warn "s_ranges2: ".join ",", map { "$_->{min}..$_->{max}" } @s_ranges;
140             #warn "o_ranges2: ".join ",", map { "$_->{min}..$_->{max}" } @o_ranges;
141              
142 725         951 my $sections;
143 725         1210 ($s_ptr, $o_ptr) = (0, 0);
144 725   100     2825 while( ($s_ptr < @s_ranges) && ($o_ptr < @o_ranges) ) {
145 553         4817 my $this_s = $s_ranges[$s_ptr];
146 553         786 my $this_o = $o_ranges[$o_ptr];
147 553 100       1600 if( $this_s->{min} < $this_o->{min} ) {
    100          
148 328         336 push @{$sections->{just_self}}, $this_s;
  328         891  
149 328         402 push @{$sections->{in_either}}, $this_s;
  328         10373  
150 328         1415 $s_ptr++;
151             } elsif( $this_o->{min} < $this_s->{min} ) {
152 106         129 push @{$sections->{just_other}}, $this_o;
  106         256  
153 106         141 push @{$sections->{in_either}}, $this_o;
  106         199  
154 106         471 $o_ptr++;
155             } else { # $this_s->{min} == $this_o->{min}
156 119 50       384 die "internal error in sectionify" unless $this_s->{max} == $this_o->{max};
157 119         142 push @{$sections->{in_both}}, $this_s;
  119         307  
158 119         171 push @{$sections->{in_either}}, $this_s;
  119         219  
159 119         151 $s_ptr++;
160 119         494 $o_ptr++;
161             }
162             }
163 725 100       1793 if( $o_ptr < @o_ranges ) {
    100          
164 661         41197 push @{$sections->{just_other}}, @o_ranges[$o_ptr..$#o_ranges];
  661         5509  
165 661         985 push @{$sections->{in_either}}, @o_ranges[$o_ptr..$#o_ranges];
  661         2167  
166             } elsif( $s_ptr < @s_ranges ) {
167 52         81 push @{$sections->{just_self}}, @s_ranges[$s_ptr..$#s_ranges];
  52         335  
168 52         70 push @{$sections->{in_either}}, @s_ranges[$s_ptr..$#s_ranges];
  52         173  
169             }
170              
171             #warn "just_self: ".join ",", map { "$_->{min}..$_->{max}" } @{$sections->{just_self}};
172             #warn "in_both: ".join ",", map { "$_->{min}..$_->{max}" } @{$sections->{in_both}};
173             #warn "in_either: ".join ",", map { "$_->{min}..$_->{max}" } @{$sections->{in_either}};
174             #warn "just_other: ".join ",", map { "$_->{min}..$_->{max}" } @{$sections->{just_other}};
175              
176 725         2496 return $sections;
177             }
178              
179              
180             sub intersection {
181 10     10 0 22 my ($self, $other) = @_;
182 10         29 my $sections = $self->sectionify( $other );
183 10         28 return multi_union( @{$sections->{in_both}} );
  10         50  
184             }
185              
186             sub subtract {
187 22     22 0 50 my ($self, $other) = @_;
188 22         74 my $sections = $self->sectionify( $other );
189 22         45 return multi_union( @{$sections->{just_self}} );
  22         103  
190             }
191              
192             sub xor {
193 12     12 0 31 my ($self, $other) = @_;
194 12         34 my $sections = $self->sectionify( $other );
195 12         23 return multi_union( @{$sections->{just_self}}, @{$sections->{just_other}} );
  12         180  
  12         57  
196             }
197              
198             sub invert {
199 35     35 0 558 my ($self) = @_;
200 35         72 my @included = @{$self->{ranges}};
  35         117  
201 35 100       169 return Number::Range::Regex::SimpleRange->new( neg_inf, pos_inf ) unless @included;
202 25         63 my @excluded = ();
203 25 100       133 if($included[0]->{min} != neg_inf ) {
204 22         63 push @excluded, Number::Range::Regex::SimpleRange->new( neg_inf, $included[0]->{min}-1 );
205             }
206 25         126 for(my $c=1; $c<@included; ++$c) {
207 34         77 my $last = $included[$c-1];
208 34         61 my $this = $included[$c];
209 34 50       135 if($last->{max}+1 > $this->{min}-1) {
210 0         0 die "internal error - overlapping SRs?";
211             } else {
212 34         213 push @excluded, Number::Range::Regex::SimpleRange->new( $last->{max}+1, $this->{min}-1 );
213             }
214             }
215 25 100       122 if($included[-1]->{max} != pos_inf) {
216 22         83 push @excluded, Number::Range::Regex::SimpleRange->new( $included[-1]->{max}+1, pos_inf );
217             }
218 25         142 return __PACKAGE__->new( @excluded );
219             }
220              
221             sub union {
222 681 100   681 0 3143 my $opts = option_mangler( ref $_[-1] eq 'HASH' ? pop : undef );
223 681         1377 my ($self, @other) = @_;
224             #warn "cr::u, wo: $opts->{warn_overlap}, $self, @other";
225 681 50       1826 return multi_union( $self, @other ) if @other > 1;
226 681         4957 my $sections = $self->sectionify( $other[0] );
227 681 100 100     3609 if( $opts->{warn_overlap} && $sections->{in_both} && @{ $sections->{in_both} } ) {
  2   66     7  
228 2 50       5 my $subname = $opts->{warn_overlap} eq '1' ? 'union' : $opts->{warn_overlap};
229 2         5 warn "$subname call got overlap(s): ", join ",", @{ $sections->{in_both} };
  2         10  
230             }
231 681         936 my @in_either = _collapse_ranges( @{$sections->{in_either}} );
  681         1957  
232 681 50       2445 if( @in_either == 0 ) {
    100          
233 0         0 return empty_set();
234             } elsif( @in_either == 1 ) {
235 570         7781 return $in_either[0];
236             } else {
237 111         373 return __PACKAGE__->new( @in_either );
238             }
239             }
240              
241             sub _collapse_ranges {
242 684     684   1274 my @ranges = @_;
243 684         971 my $last_r;
244 684         937 my $this_r = $ranges[0];
245 684         5918 for (my $rpos = 1; $rpos < @ranges; $rpos++ ) {
246 370         534 $last_r = $this_r;
247 370         496 $this_r = $ranges[$rpos];
248 370 100       1043 if($last_r->touches($this_r)) {
249 69         273 $this_r = $last_r->union( $this_r );
250 69         189 splice(@ranges, $rpos-1, 2, $this_r);
251 69         213 $rpos--;
252             }
253             }
254 684         1880 return @ranges;
255             }
256              
257             #sub _is_contiguous {
258             # my ($self) = @_;
259             # my $last_r;
260             # my $this_r = $self->{ranges}->[0];
261             # for (my $rpos = 1; $rpos < @{$self->{ranges}}; $rpos++ ) {
262             # $last_r = $this_r;
263             # $this_r = $self->{ranges}->[$rpos];
264             # return if $last_r->{max}+1 < $this_r->{min};
265             # }
266             # return ($self->{ranges}->[0]->{min}, $self->{ranges}->[-1]->{max});
267             #}
268              
269             sub contains {
270 536     536 0 446362 my ($self, $n) = @_;
271 536         750 foreach my $r (@{$self->{ranges}}) {
  536         1376  
272 857 100       2557 return 1 if $r->contains( $n );
273             }
274 329         1123 return;
275             }
276              
277             sub is_empty {
278 95     95 0 4053 my ($self) = @_;
279 95         123 return !@{$self->{ranges}};
  95         534  
280             }
281              
282             sub has_lower_bound {
283 0     0 0 0 my ($self) = @_;
284 0 0       0 return if $self->is_empty;
285 0         0 return $self->{ranges}->[0]->has_lower_bound;
286             }
287              
288             sub has_upper_bound {
289 0     0 0 0 my ($self) = @_;
290 0 0       0 return if $self->is_empty;
291 0         0 return $self->{ranges}->[-1]->has_upper_bound;
292             }
293              
294             sub is_infinite {
295 44     44 0 4017 my ($self) = @_;
296 44 100       156 return if $self->is_empty;
297 39   100     203 return ! ( $self->{ranges}->[0]->has_lower_bound && $self->{ranges}->[-1]->has_upper_bound );
298             }
299              
300             1;
301