File Coverage

blib/lib/Number/Range/Regex/SimpleRange.pm
Criterion Covered Total %
statement 207 211 98.1
branch 114 136 83.8
condition 23 29 79.3
subroutine 25 25 100.0
pod 0 15 0.0
total 369 416 88.7


line stmt bran cond sub pod time code
1             # Number::Range::Regex::SimpleRange
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::SimpleRange;
8              
9             # a contiguous, finite range, can be expressed as an array of TrivialRange
10              
11 14     14   83 use strict;
  14         29  
  14         639  
12 14     14   76 use vars qw ( @ISA @EXPORT @EXPORT_OK $VERSION );
  14         30  
  14         1430  
13             eval { require warnings; }; #it's ok if we can't load warnings
14              
15             require Exporter;
16 14     14   84 use base 'Exporter';
  14         24  
  14         1705  
17             @ISA = qw( Exporter Number::Range::Regex::Range );
18              
19             $VERSION = '0.32';
20              
21 14     14   83 use Number::Range::Regex::Util ':all';
  14         27  
  14         4727  
22 14     14   88 use Number::Range::Regex::Util::inf qw( neg_inf pos_inf inf_type _is_negative _pad );
  14         297  
  14         80010  
23              
24             sub new {
25 1206     1206 0 3213 my ($class, $min, $max, $passed_opts) = @_;
26              
27 1206         5668 my $opts = option_mangler( $passed_opts );
28              
29 1206 50 33     6340 die 'internal error: undefined min and max from caller: '.join(":", caller) if !( defined $min && defined $max );
30              
31 1206         10094 my $base = $opts->{base};
32 1206         4152 my $base_digits = $opts->{base_digits} = base_digits($base);
33 1206         4993 my $base_max = $opts->{base_max} = substr($base_digits, -1);
34 1206         4300 my $base_digits_regex = $opts->{base_digits_regex} = _calculate_digit_range( 0, $base_max, $base_digits );
35              
36 1206 50       10251 die "min ($min) must be a base $opts->{base} integer or /^[+-]?inf\$/" if $min !~ /^[-+]?(?:inf|[$base_digits]+)$/;
37 1206 50       8010 die "max ($max) must be a base $opts->{base} integer or /^[+-]?inf\$/" if $max !~ /^[-+]?(?:inf|[$base_digits]+)$/;
38              
39             # convert '-inf' / '+inf' strings to neg_inf / pos_inf objects
40             # and canonicalize min and max by removing leading zeroes, plus signs
41 1206         3208 foreach my $val ( \$min, \$max ) {
42 2412 100       6063 next if ref $$val; # don't do these checks on pos_inf/neg_inf objects
43 2113         3283 $$val =~ s/^\+//;
44 2113         4302 $$val =~ s/^(-?)0+/$1/; #strip leading zeroes
45 2113 100       10575 $$val = 0 if $$val =~ /^-?$/; #don't strip "0" or "-0" -> ""
46 2113 100       5748 $$val = pos_inf if $$val =~ /^[+]?inf$/;
47 2113 100       6075 $$val = neg_inf if $$val =~ /^-inf$/;
48             }
49              
50             # any infinite numbers are compared using Util::inf, but others
51             # must be compared as strings to account for bases >10
52             # TODO: this assumes we have a base in ascii-order!
53 1206         2384 my $out_of_order;
54 1206 100 100     4024 if(inf_type($min) || inf_type($max)) {
    50 66        
    100 100        
55 387         5202 $out_of_order = $min > $max;
56             } elsif( !_is_negative($min) && _is_negative($max) ) {
57 0         0 $out_of_order = 1;
58             } elsif( _is_negative($min) && !_is_negative($max) ) {
59 24         47 $out_of_order = 0;
60             } else { #min and max have same sign
61 795         1402 my $digdiff = length($max)-length($min);
62 795 100       2132 my $pmin = $digdiff > 0 ? _pad($min, $digdiff) : $min;
63 795 100       1768 my $pmax = $digdiff < 0 ? _pad($max, -$digdiff) : $max;
64 795 100       2008 $out_of_order = _is_negative($max) ? $pmin lt $pmax : $pmin gt $pmax;
65             }
66              
67 1206 100       3438 if( $out_of_order ) {
68 4 100       79 die "min($min) > max($max) (autoswap option not specified)" if !$opts->{autoswap};
69 2         9 ($min, $max) = ($max, $min);
70             }
71              
72 1204         15423 return bless { min => $min, max => $max, opts => $opts,
73             base => $base, base_max => $base_max,
74             base_digits => $base_digits, base_digits_regex => $base_digits_regex,
75             }, $class;
76             }
77              
78             sub to_string {
79 1086     1086 0 52862 my ($self, $passed_opts) = @_;
80 1086 100       3363 if( $self->{min} eq $self->{max} ) {
81 210         1073 return $self->{min};
82             # the prefer_comma option is dangerous because if you read in 3,4
83             # you don't get 3..4, but instead 3..3,4..4 which requires collapsing
84             #} elsif($self->{min}+$opts->{prefer_comma} >= $self->{max}) {
85             } else {
86 876         4360 return "$self->{min}..$self->{max}";
87             }
88             }
89              
90             sub regex {
91 1586     1586 0 11292 my ($self, $passed_opts) = @_;
92              
93 1586         5981 my $opts = option_mangler( $self->{opts}, $passed_opts );
94              
95 1586   100     6588 $self->{tranges} ||= [ $self->_calculate_tranges() ];
96              
97 1586 50       4575 my $separator = $opts->{readable} ? ' | ' : '|';
98 1586         2427 my $regex_str = join $separator, map { $_->regex( $opts ) } @{$self->{tranges}};
  5277         18686  
  1586         3988  
99 1586 50       10951 $regex_str = " $regex_str " if $opts->{readable};
100              
101 1586 50       4194 my $modifier_maybe = $opts->{readable} ? '(?x)' : '';
102 1586         2811 my ($begin_comment_maybe, $end_comment_maybe) = ('', '');
103 1586 100       4241 if($opts->{comment}) {
104 1154         2682 my ($min, $max) = ($self->{min}, $self->{max});
105 1154         2614 my $comment = "Number::Range::Regex::SimpleRange[$min..$max]";
106 1154 50       4547 $begin_comment_maybe = $opts->{readable} ? " # begin $comment" : "(?# begin $comment )";
107 1154 50       3892 $end_comment_maybe = $opts->{readable} ? " # end $comment" : "(?# end $comment )";
108             }
109 1586 100       11460 $regex_str = "(?:$regex_str)" if @{$self->{tranges}} != 1;
  1586         5892  
110              
111 1586         50674 return qr/$begin_comment_maybe$modifier_maybe$regex_str$end_comment_maybe/;
112             }
113              
114             sub _calculate_tranges {
115 439     439   779 my ($self) = @_;
116 439         964 my $min = $self->{min};
117 439         874 my $max = $self->{max};
118              
119 439 100 100     1165 if( _is_negative( $min ) && _is_negative( $max ) ) {
    100 66        
    50 33        
120 9         58 my $pos_sr = __PACKAGE__->new( -$max, -$min );
121 9         50 my @tranges = $pos_sr->_calculate_tranges();
122 9         46 @tranges = reverse map { Number::Range::Regex::TrivialRange->new(
  14         84  
123             -$_->{max}, -$_->{min} ) } @tranges;
124 9         134 return @tranges;
125             } elsif( _is_negative( $min ) && !_is_negative( $max ) ) {
126             # min..-1, 0..max
127 12         42 my $pos_lo_sr = __PACKAGE__->new( 1, -$min );
128 12         55 my @tranges = $pos_lo_sr->_calculate_tranges();
129 12         47 @tranges = reverse map { Number::Range::Regex::TrivialRange->new(
  24         261  
130             -$_->{max}, -$_->{min}, ) } @tranges;
131 12         57 push @tranges, __PACKAGE__->new( 0, $max )->_calculate_tranges();
132 12         104 return @tranges;
133             } elsif( !_is_negative( $min ) && _is_negative( $max ) ) {
134 0         0 die "_calculate_tranges() - internal error - min($min)>=0 but max($max)<0?";
135             }
136             # if we get here, $min >= 0 and $max >= 0
137              
138 418 100       1534 if ( $min eq $max ) {
139 102         603 return Number::Range::Regex::TrivialRange->new( $min, $min );
140             }
141              
142 316 100       851 if($max == pos_inf) {
143             # iterate from $self->{min} up to the next (power of 10) - 1 (e.g. 9999)
144             # then spit out a regex for any integer with a longer length
145 24         72 my $tmp = $self->{base_max} x length $self->{min};
146 24         78 my $noninf = __PACKAGE__->new($self->{min}, $tmp );
147 24         138 return ( $noninf->_calculate_tranges(),
148             Number::Range::Regex::TrivialRange->new( $tmp+1, pos_inf ) );
149             } else {
150              
151             # $min-- unless $self->{opts}->{exclusive_min} || $self->{opts}->{exclusive};
152             # $max++ unless $self->{opts}->{exclusive_max} || $self->{opts}->{exclusive};
153             # warn "WARNING: exclusive ranges untested!" if($self->{opts}->{exclusive_min} || $self->{opts}->{exclusive_max} || $self->{opts}->{exclusive});
154              
155 292         593 my $digits_diff = length($max)-length($min);
156 292         692 my $padded_min = ('0' x $digits_diff).$min;
157              
158 292         412 my $samedigits = 0;
159 292         775 for my $digit (0..length($max)-1) {
160 429 100       1321 last unless substr($padded_min, $digit, 1) eq substr($max, $digit, 1);
161 137         199 $samedigits++;
162             }
163              
164 292         862 my ($rightmost, $leftmost) = (length $max, $samedigits+1);
165              
166 292         482 my @tranges = ();
167             push @tranges,
168             $self->_do_range_setting_loop($min, $padded_min, length($max) - length($min), $rightmost,
169             [ reverse ($leftmost+1..$rightmost) ],
170             sub {
171 317     317   757 my ( $digit, $trailer_len, $header ) = @_;
172 317 100       1407 return ($trailer_len ? base_next($digit, $self->{base_digits}) : $digit, $self->{base_max});
173             }
174 292         3290 );
175              
176             push @tranges,
177             $self->_do_range_setting_loop($min, $padded_min, length($max) - length($min), $rightmost,
178             [ $leftmost ],
179             sub {
180 292     292   681 my ( $digit, $trailer_len, $header ) = @_;
181 292 100       1032 my $digit_min = $trailer_len ? base_next($digit, $self->{base_digits}) : $digit; #inclusive in ones column only!
182 292         1105 my $digit_max = substr($max, length($header), 1);
183 292 100       1071 $digit_max = base_prev($digit_max, $self->{base_digits}) if $trailer_len;
184 292         788 return ($digit_min, $digit_max);
185             }
186 292         3161 );
187              
188             push @tranges,
189             $self->_do_range_setting_loop($max, $max, 0, $rightmost,
190             [ ($leftmost+1)..$rightmost ],
191             sub {
192 317     317   562 my ( $digit, $trailer_len, $header ) = @_;
193 317 100       1236 return (0, $trailer_len ? base_prev($digit, $self->{base_digits}) : $digit);
194             }
195 292         3379 );
196              
197 292         3052 return @tranges;
198             }
199             }
200              
201             sub _do_range_setting_loop {
202 876     876   1808 my ($self, $string_base, $padded_string_base, $string_offset,
203             $rightmost, $digit_pos_range, $digit_range_sub) = @_;
204              
205 876         1239 my @ranges = ();
206 876         1827 foreach my $digit_pos (@$digit_pos_range) {
207 926         1816 my $pos = $digit_pos - $string_offset - 1;
208 926 100       2012 my $static_header = $pos < 0 ? "" : substr($string_base, 0, $pos);
209 926         1255 my $trailer_len = $rightmost - $digit_pos;
210              
211 926         1337 my $digit = substr($padded_string_base, $digit_pos-1, 1);
212              
213 926         1991 my ($digit_min, $digit_max) = $digit_range_sub->( $digit, $trailer_len, $static_header );
214              
215 926         3161 my $digit_range = _calculate_digit_range( $digit_min, $digit_max, $self->{base_digits} );
216 926 100       2914 next unless defined $digit_range;
217              
218 664         1491 my $range_min = $static_header.$digit_min.(0 x $trailer_len);
219 664         1556 my $range_max = $static_header.$digit_max.($self->{base_max} x $trailer_len);
220 664         2736 push @ranges, Number::Range::Regex::TrivialRange->new(
221             $range_min, $range_max );
222             }
223 876         2102 return @ranges;
224             }
225              
226             sub intersection {
227 27     27 0 783 my ($self, $other) = @_;
228              
229 27 100       234 if( $other->isa('Number::Range::Regex::CompoundRange') ) {
230 4         20 return Number::Range::Regex::CompoundRange->new( $self )->intersection( $other );
231             }
232 23         91 my ($lower, $upper) = _order_by_min( $self, $other );
233 23 100       122 if( $upper->{min} <= $lower->{max} ) {
234 14 100       73 return $upper if $upper->{max} <= $lower->{max};
235 6         27 return __PACKAGE__->new( $upper->{min}, $lower->{max} );
236             } else {
237 9         29 return empty_set();
238             }
239             }
240              
241             sub union {
242 336 100   336 0 50110 my $opts = option_mangler( ref $_[-1] eq 'HASH' ? pop : undef );
243 336         745 my ($self, @other) = @_;
244             #warn "sr::u, wo: $opts->{warn_overlap}, $self, @other";
245 336 50       1258 return multi_union( $self, @other ) if @other > 1;
246 336         929 my $other = shift @other;
247 336 100       2597 if( $other->isa('Number::Range::Regex::CompoundRange') ) {
248 12         53 return Number::Range::Regex::CompoundRange->new( $self )->union( $other );
249             }
250 324         8480 my ($lower, $upper) = _order_by_min( $self, $other );
251 324 100       1904 if( $upper->{min} < $lower->{max}+1 ) {
    100          
252 65 100       208 if( $opts->{warn_overlap} ) {
253 1         6 my $overlap = __PACKAGE__->new( $upper->{min}, $lower->{max} );
254 1 50       7 my $subname = $opts->{warn_overlap} eq '1' ? 'union' : $opts->{warn_overlap};
255 1         7 warn "$subname call got overlap: ".$overlap->to_string();
256             }
257             # NOTE: this is more complicated than it probably should be: we preserve
258             # the original object if we can, so if it's a TR, it stays a TR.
259             # we don't actually seem to need that, although we have tests for it.
260 65 100       244 if( $lower->{max} >= $upper->{max} ) {
261 37         314 return $lower;
262             } else {
263 28         137 return __PACKAGE__->new( $lower->{min}, $upper->{max} );
264             }
265             } elsif( $upper->{min} == $lower->{max}+1 ) {
266 89         319 return __PACKAGE__->new( $lower->{min}, $upper->{max} );
267             } else { #$upper->{min} > $lower->{max}+1
268 170         801 return Number::Range::Regex::CompoundRange->new( $lower, $upper );
269             }
270             }
271              
272             sub subtract {
273 20     20 0 38 my ($self, $other) = @_;
274 20 100       171 if( $other->isa('Number::Range::Regex::CompoundRange') ) {
275 4         22 return Number::Range::Regex::CompoundRange->new( $self )->subtract( $other);
276             }
277 16 50       60 return $self unless $self->touches($other);
278              
279 16 100       54 if( $self->{min} < $other->{min} ) {
280 10 100       41 if( $self->{max} <= $other->{max} ) {
281             # e.g. (1..7)-(3..11) = (1..2)
282             # e.g. (1..11)-(3..11) = (1..2)
283 2         10 return __PACKAGE__->new( $self->{min}, $other->{min}-1 );
284             } else {
285             # e.g. (1..7)-(2..6) = (1, 7)
286 8         47 my $r1 = __PACKAGE__->new( $self->{min}, $other->{min}-1 );
287 8         41 my $r2 = __PACKAGE__->new( $other->{max}+1, $self->{max} );
288 8         33 return $r1->union( $r2 );
289             }
290             } else {
291 6 100       24 if( $self->{max} <= $other->{max} ) {
292             # e.g. (1..7)-(1..11) = ()
293             # e.g. (1..7)-(1..7) = ()
294 4         15 return empty_set();
295             } else {
296             # e.g. (1..7)-(1..4) = (5..7)
297 2         12 return __PACKAGE__->new( $other->{max}+1, $self->{max} );
298             }
299             }
300             }
301              
302             sub xor {
303 17     17 0 2394 my ($self, $other) = @_;
304 17 100       152 if( $other->isa('Number::Range::Regex::CompoundRange') ) {
305 2         14 return Number::Range::Regex::CompoundRange->new( $self )->xor( $other );
306             }
307 15 50       53 return $self->union($other) unless $self->touches($other);
308              
309 15 100       56 if( $self->{min} == $other->{min} ) {
310 3 100       19 if( $self->{max} < $other->{max} ) {
    100          
311             # e.g. (1..7)xor(1..11) = (8..11)
312 1         9 return __PACKAGE__->new( $self->{max}+1, $other->{max} );
313             } elsif($self->{max} == $other->{max}) {
314             # e.g. (1..11)xor(1..11) = ()
315 1         5 return empty_set( $self->{opts} );
316             } else {
317             # e.g. (1..7)xor(1..6) = (7)
318 1         6 return __PACKAGE__->new( $other->{max}+1, $self->{max} );
319             }
320             } else {
321 12         192 my ($lower, $upper) = _order_by_min( $self, $other );
322 12 100       68 if($lower->{max} < $upper->{max}) {
    100          
323             # e.g. (1..7)xor(3..11) = (1..2, 8..11)
324 4         21 my $r1 = __PACKAGE__->new( $lower->{min}, $upper->{min}-1 );
325 4         21 my $r2 = __PACKAGE__->new( $lower->{max}+1, $upper->{max} );
326 4         17 return $r1->union( $r2 );
327             } elsif($lower->{max} == $upper->{max}) {
328             # e.g. (1..11)xor(3..11) = (1..2)
329 1         4 return __PACKAGE__->new( $lower->{min}, $upper->{min}-1 );
330             } else {
331             # e.g. (1..7)xor(3..6) = (1..2, 7)
332 7         42 my $r1 = __PACKAGE__->new( $lower->{min}, $upper->{min}-1 );
333 7         38 my $r2 = __PACKAGE__->new( $upper->{max}+1, $lower->{max} );
334 7         36 return $r1->union( $r2 );
335             }
336             }
337             }
338              
339             sub invert {
340 39     39 0 398 my ($self) = @_;
341 39         66 my @r;
342 39 100       167 if($self->{min} != neg_inf) {
343 36         95 push @r, __PACKAGE__->new( neg_inf, $self->{min}-1 );
344             }
345 39 100       363 if($self->{max} != pos_inf) {
346 36         142 push @r, __PACKAGE__->new( $self->{max}+1, pos_inf );
347             }
348 39         168 return multi_union( @r );
349             }
350              
351             sub overlaps {
352 70     70 0 14866 my ($self, @other) = @_;
353 70         108 foreach my $other (@other) {
354 70 50       265 if(!$other->isa( 'Number::Range::Regex::SimpleRange') ) {
355 0 0       0 return 1 if $other->overlaps($self);
356             } else {
357 70 50       227 die "other argument is not a simple range (try swapping your args)" unless $other->isa('Number::Range::Regex::SimpleRange');
358 70         175 my ($lower, $upper) = _order_by_min( $self, $other );
359 70 100       257 return 1 if $upper->{min} <= $lower->{max};
360             }
361             }
362 9         29 return;
363             }
364              
365             sub touches {
366 472     472 0 15553 my ($self, @other) = @_;
367 472         1392 foreach my $other (@other) {
368 472 50       1807 if(!$other->isa( 'Number::Range::Regex::SimpleRange') ) {
369 0 0       0 return 1 if $other->touches($self);
370             } else {
371 472 50       1476 die "other argument is not a simple range (try swapping your args)" unless $other->isa('Number::Range::Regex::SimpleRange');
372 472         1380 my ($lower, $upper) = _order_by_min( $self, $other );
373 472 100       2246 return 1 if $upper->{min} <= $lower->{max}+1;
374             }
375             }
376 305         1607 return;
377             }
378              
379             sub contains {
380 1975     1975 0 951504 my ($self, $n) = @_;
381 1975   100     17843 return ($n >= $self->{min}) && ($n <= $self->{max});
382             }
383              
384 347     347 0 512 sub has_lower_bound { my ($self) = @_; return $self->{min} != neg_inf; }
  347         1340  
385 11432     11432 0 18124 sub has_upper_bound { my ($self) = @_; return $self->{max} != pos_inf; }
  11432         28627  
386              
387             sub is_infinite {
388 46     46 0 6482 my ($self) = @_;
389 46   100     184 return !( $self->has_lower_bound && $self->has_upper_bound );
390             }
391              
392 42     42 0 2138 sub is_empty { return; }
393              
394             1;
395