File Coverage

blib/lib/App/Greple/Regions.pm
Criterion Covered Total %
statement 35 170 20.5
branch 3 50 6.0
condition 0 60 0.0
subroutine 11 34 32.3
pod 0 19 0.0
total 49 333 14.7


line stmt bran cond sub pod time code
1             package App::Greple::Regions;
2              
3 2     2   114571 use v5.14;
  2         15  
4 2     2   11 use warnings;
  2         4  
  2         48  
5 2     2   9 use Carp;
  2         4  
  2         96  
6 2     2   11 use Data::Dumper;
  2         4  
  2         117  
7             $Data::Dumper::Sortkeys = 1;
8              
9 2     2   14 use Exporter 'import';
  2         6  
  2         210  
10             our @EXPORT = qw(REGION_INSIDE REGION_OUTSIDE
11             REGION_UNION REGION_INTERSECT
12             match_regions
13             classify_regions
14             select_regions
15             filter_regions
16             merge_regions
17             reverse_regions
18             match_borders
19             borders_to_regions
20             );
21             our %EXPORT_TAGS = ( );
22             our @EXPORT_OK = qw();
23              
24             use constant {
25              
26 2         1381 REGION_AREA_MASK => 1,
27             REGION_INSIDE => 0,
28             REGION_OUTSIDE => 1,
29              
30             REGION_SET_MASK => 2,
31             REGION_INTERSECT => 0,
32             REGION_UNION => 2,
33 2     2   14 };
  2         4  
34              
35             sub new {
36 0     0 0 0 my $class = shift;
37              
38 0         0 my $obj = bless {
39             FLAG => undef,
40             SPEC => undef,
41             }, $class;
42              
43 0 0       0 $obj->configure(@_) if @_;
44              
45 0         0 $obj;
46             }
47              
48             sub configure {
49 0     0 0 0 my $obj = shift;
50 0         0 while (@_ >= 2) {
51 0         0 $obj->{$_[0]} = $_[1];
52 0         0 splice @_, 0, 2;
53             }
54             }
55              
56 0     0 0 0 sub spec { $_[0]->{SPEC} }
57 0     0 0 0 sub flag { $_[0]->{FLAG} }
58             sub is_x {
59 0     0 0 0 my($p, $mask, $set) = @_;
60 0 0       0 ((ref $p ? $p->flag : $p) & $mask) == $set;
61             }
62 0     0 0 0 sub is_union { is_x $_[0], REGION_SET_MASK, REGION_UNION }
63 0     0 0 0 sub is_intersect { is_x $_[0], REGION_SET_MASK, REGION_INTERSECT }
64 0     0 0 0 sub is_inside { is_x $_[0], REGION_AREA_MASK, REGION_INSIDE }
65 0     0 0 0 sub is_outside { is_x $_[0], REGION_AREA_MASK, REGION_OUTSIDE }
66              
67             package App::Greple::Regions::Holder {
68              
69             sub new {
70 0     0   0 my $class = shift;
71 0         0 bless [], $class;
72             }
73              
74             sub append {
75 0     0   0 my $obj = shift;
76 0         0 push @$obj, App::Greple::Regions->new(@_);
77             }
78              
79             sub regions {
80 0     0   0 my $obj = shift;
81 0         0 @$obj;
82             }
83              
84             sub union {
85 0     0   0 grep { $_->is_union } shift->regions;
  0         0  
86             }
87              
88             sub intersect {
89 0     0   0 grep { $_->is_intersect } shift->regions;
  0         0  
90             }
91             }
92              
93             sub match_regions {
94 0     0 0 0 my %arg = @_;
95 0   0     0 my $pattern = $arg{pattern} // croak "Parameter error";
96 0 0       0 my $regex = ref $pattern eq 'Regexp' ? $pattern : qr/$pattern/m;
97 0 0       0 return &match_regions_by_group($regex) if $arg{group};
98              
99 0         0 my @regions;
100              
101 2     2   16 no warnings 'utf8';
  2         4  
  2         285  
102              
103 0         0 while (/$regex/gp) {
104             ##
105             ## this is much faster than:
106             ## my($s, $e) = ($-[0], $+[0]);
107             ##
108             ## calling pos() cost is not neglective, either.
109             ##
110 0         0 my $pos = pos();
111 0         0 push @regions, [ $pos - length(${^MATCH}), $pos ];
112             }
113 0         0 @regions;
114             }
115              
116             sub match_regions_by_group {
117 0     0 0 0 my $regex = shift;
118 0         0 my @regions;
119              
120 2     2   16 no warnings 'utf8';
  2         4  
  2         2406  
121              
122 0         0 while (/$regex/g) {
123 0 0       0 if (@- == 1) {
124 0         0 push @regions, [ $-[0], $+[0], 0 ];
125             } else {
126 0         0 for my $i (1 .. $#-) {
127 0         0 push @regions, [ $-[$i], $+[$i], $i];
128             }
129             }
130             }
131 0         0 @regions;
132             }
133              
134             sub classify_regions {
135 0 0   0 0 0 my $opt = ref $_[0] eq 'HASH' ? shift : {};
136              
137 0 0       0 $opt->{strict} and goto &classify_regions_strict;
138              
139 0         0 my @list = @{+shift};
  0         0  
140 0         0 my @by = @{+shift};
  0         0  
141 0         0 my @table;
142 0         0 for (my $i = 0; $i < @by; $i++) {
143 0         0 my($from, $to) = @{$by[$i]};
  0         0  
144 0   0     0 while (@list and $list[0][1] < $from) {
145 0         0 shift @list;
146             }
147 0   0     0 while (@list and $list[0][1] == $from and $list[0][0] < $from) {
      0        
148 0         0 shift @list;
149             }
150 0         0 my $t = $table[$i] = [];
151 0   0     0 for (my $i = 0; ($i < @list) and ($list[$i][0] < $to); $i++) {
152 0         0 push @$t, [ @{$list[$i]} ];
  0         0  
153             }
154             }
155 0         0 @table;
156             }
157              
158             sub classify_regions_strict {
159 0     0 0 0 my @list = @{+shift};
  0         0  
160 0         0 my @by = @{+shift};
  0         0  
161 0         0 my @table;
162 0         0 for (my $i = 0; $i < @by; $i++) {
163 0         0 my($from, $to) = @{$by[$i]};
  0         0  
164 0   0     0 while (@list and $list[0][0] < $from) {
165 0         0 shift @list;
166             }
167 0         0 my $t = $table[$i] = [];
168 0   0     0 while (@list and $list[0][0] < $to and $list[0][1] <= $to) {
      0        
169 0         0 push @$t, shift @list;
170             }
171             }
172 0         0 @table;
173             }
174              
175             sub select_regions {
176 0 0   0 0 0 my $opt = ref $_[0] eq 'HASH' ? shift : {};
177 0         0 my($list, $by, $flag) = @_;
178 0         0 my($inside, $outside) = ([], []);
179 0 0       0 my $target = is_inside($flag) ? $inside : $outside;
180 0 0       0 my $overlap = $opt->{strict} ? [] : $target;
181 0         0 filter_regions($list, $by, $inside, $overlap, $outside);
182 0         0 @$target;
183             }
184              
185             ##
186             ## Split @input into @inside, @overlap, @outside by @filter and return
187             ## their pointers.
188             ##
189             ## 4th and 5th result is corresponding entry of @filter for @inside
190             ## and @overlap.
191             ##
192             sub filter_regions {
193 0     0 0 0 my @input = @{+shift};
  0         0  
194 0         0 my @filter = @{+shift};
  0         0  
195 0   0     0 my($inside, $overlap, $outside) = (shift//[], shift//[], shift//[]);
      0        
      0        
196 0         0 my($inside_match, $overlap_match) = ([], []);
197              
198 0         0 for (my $i = 0; $i < @filter; $i++) {
199 0         0 my($from, $to) = @{$filter[$i]};
  0         0  
200 0   0     0 while (@input and $input[0][0] < $from and $input[0][1] <= $from) {
      0        
201 0         0 push @$outside, shift @input;
202             }
203 0   0     0 while (@input and $input[0][0] < $from) {
204 0         0 push @$overlap, shift @input;
205 0         0 $overlap_match->[$#{$overlap}] = $filter[$i];
  0         0  
206             }
207 0   0     0 while (@input and $input[0][0] < $to and $input[0][1] <= $to) {
      0        
208 0         0 push @$inside, shift @input;
209 0         0 $inside_match->[$#{$inside}] = $filter[$i];
  0         0  
210             }
211 0   0     0 while (@input and $input[0][0] < $to) {
212 0         0 push @$overlap, shift @input;
213 0         0 $overlap_match->[$#{$overlap}] = $filter[$i];
  0         0  
214             }
215             }
216 0         0 push @$outside, splice @input;
217 0         0 ($inside, $overlap, $outside, $inside_match, $overlap_match);
218             }
219              
220             sub merge_regions {
221 0 0   0 0 0 my $option = ref $_[0] eq 'HASH' ? shift : {};
222 0         0 my $nojoin = $option->{nojoin};
223 0 0       0 my @in = $option->{destructive} ? @_ : map { [ @$_ ] } @_;
  0         0  
224 0 0       0 unless ($option->{nosort}) {
225 0 0 0     0 @in = sort({$a->[0] <=> $b->[0] || $b->[1] <=> $a->[1]
  0 0       0  
226             || (@$a > 2 ? $a->[2] <=> $b->[2] : 0)
227             } @in);
228             }
229 0         0 my @out;
230 0 0       0 push(@out, shift @in) if @in;
231 0         0 while (@in) {
232 0         0 my $top = shift @in;
233              
234 0 0 0     0 if ($out[-1][1] > $top->[0]) {
    0 0        
      0        
235 0 0       0 $out[-1][1] = $top->[1] if $out[-1][1] < $top->[1];
236             }
237             elsif (!$nojoin
238             and $out[-1][1] == $top->[0]
239             ##
240             ## don't connect regions in different pattern group
241             ##
242             and (@$top < 3 or $out[-1][2] == $top->[2])
243             ) {
244 0 0       0 $out[-1][1] = $top->[1] if $out[-1][1] < $top->[1];
245             }
246             else {
247 0         0 push @out, $top;
248             }
249             }
250 0         0 @out;
251             }
252              
253 2     2   16 use List::Util qw(pairmap);
  2         4  
  2         794  
254              
255             sub reverse_regions {
256 12 50   12 0 8960 my $option = ref $_[0] eq 'HASH' ? shift : {};
257 12         25 my($from, $max) = @_;
258 12         18 my @reverse = do {
259 29     29   79 pairmap { [ $a, $b ] }
260 12         70 0, map( { $_->[0] => $_->[1] } @$from ), $max
  17         63  
261             };
262 12 100       57 return @reverse if $option->{leave_empty};
263 9         14 grep { $_->[0] != $_->[1] } @reverse
  20         64  
264             }
265              
266             sub match_borders {
267 0     0 0   my $regex = shift;
268 0           my @border = (0);
269 0           while (/$regex/gp) {
270 0           my $pos = pos();
271 0           for my $i ($pos - length(${^MATCH}), $pos) {
272 0 0         push @border, $i if $border[-1] != $i;
273             }
274             }
275 0 0         push @border, length if $border[-1] != length;
276 0           @border;
277             }
278              
279             sub borders_to_regions {
280 0 0   0 0   return () if @_ < 2;
281 0           map { [ $_[$_-1], $_[$_] ] } 1..$#_;
  0            
282             }
283              
284             1;