File Coverage

blib/lib/App/Greple/Regions.pm
Criterion Covered Total %
statement 159 170 93.5
branch 39 54 72.2
condition 40 65 61.5
subroutine 33 34 97.0
pod 0 19 0.0
total 271 342 79.2


line stmt bran cond sub pod time code
1             package App::Greple::Regions;
2              
3 172     172   89435 use v5.24;
  172         497  
4 172     172   689 use warnings;
  172         239  
  172         11334  
5             BEGIN {
6 172     172   446 eval { warnings->unimport('experimental::regex_sets') };
  172         2779  
7 172         332 eval { warnings->unimport('experimental::vlb') };
  172         4454  
8             }
9 172     172   757 use Carp;
  172         244  
  172         9565  
10 172     172   716 use Data::Dumper;
  172         248  
  172         9103  
11             $Data::Dumper::Sortkeys = 1;
12              
13 172     172   686 use Exporter 'import';
  172         252  
  172         14844  
14             our @EXPORT = qw(REGION_INSIDE REGION_OUTSIDE
15             REGION_UNION REGION_INTERSECT
16             match_regions
17             classify_regions
18             select_regions
19             filter_regions
20             merge_regions
21             reverse_regions
22             match_borders
23             borders_to_regions
24             );
25             our %EXPORT_TAGS = ( );
26             our @EXPORT_OK = qw();
27              
28             use constant {
29              
30 172         89362 REGION_AREA_MASK => 1,
31             REGION_INSIDE => 0,
32             REGION_OUTSIDE => 1,
33              
34             REGION_SET_MASK => 2,
35             REGION_INTERSECT => 0,
36             REGION_UNION => 2,
37 172     172   2716 };
  172         1489  
38              
39             sub new {
40 11     11 0 18 my $class = shift;
41              
42 11         32 my $obj = bless {
43             FLAG => undef,
44             SPEC => undef,
45             }, $class;
46              
47 11 50       52 $obj->configure(@_) if @_;
48              
49 11         30 $obj;
50             }
51              
52             sub configure {
53 11     11 0 24 my $obj = shift;
54 11         26 while (@_ >= 2) {
55 22         66 $obj->{$_[0]} = $_[1];
56 22         45 splice @_, 0, 2;
57             }
58             }
59              
60 11     11 0 74 sub spec { $_[0]->{SPEC} }
61 46     46 0 211 sub flag { $_[0]->{FLAG} }
62             sub is_x {
63 46     46 0 83 my($p, $mask, $set) = @_;
64 46 100       161 ((ref $p ? $p->flag : $p) & $mask) == $set;
65             }
66 11     11 0 44 sub is_union { is_x $_[0], REGION_SET_MASK, REGION_UNION }
67 11     11 0 36 sub is_intersect { is_x $_[0], REGION_SET_MASK, REGION_INTERSECT }
68 22     22 0 48 sub is_inside { is_x $_[0], REGION_AREA_MASK, REGION_INSIDE }
69 2     2 0 5 sub is_outside { is_x $_[0], REGION_AREA_MASK, REGION_OUTSIDE }
70              
71             package App::Greple::Regions::Holder {
72              
73             sub new {
74 166     166   401 my $class = shift;
75 166         489 bless [], $class;
76             }
77              
78             sub append {
79 11     11   19 my $obj = shift;
80 11         76 push @$obj, App::Greple::Regions->new(@_);
81             }
82              
83             sub regions {
84 304     304   471 my $obj = shift;
85 304         1760 @$obj;
86             }
87              
88             sub union {
89 152     152   961 grep { $_->is_union } shift->regions;
  11         48  
90             }
91              
92             sub intersect {
93 152     152   704 grep { $_->is_intersect } shift->regions;
  11         35  
94             }
95             }
96              
97             sub match_regions {
98 212     212 0 3746 my %arg = @_;
99 212   33     962 my $pattern = $arg{pattern} // croak "Parameter error";
100 212 100       1169 my $regex = ref $pattern eq 'Regexp' ? $pattern : qr/$pattern/m;
101 212 100       956 return &match_regions_by_group($regex, $arg{index}) if $arg{group};
102              
103 200         869 my @regions;
104              
105 172     172   1428 no warnings 'utf8';
  172         310  
  172         21041  
106              
107 200         2370 while (/$regex/gp) {
108             ##
109             ## Using $-[0]/$+[0] is extremely slow with UTF-8 text.
110             ## Still not fixed in Perl 5.34.
111             ## https://qiita.com/kaz-utashiro/items/2facc87ea9ba25e81cd9
112             ##
113 1550         2482 my $pos = pos();
114 1550         5713 push @regions, [ $pos - length(${^MATCH}), $pos ];
115             }
116 200         1189 @regions;
117             }
118              
119             sub match_regions_by_group {
120 12     12 0 204 my($regex, $index) = @_;
121 12         25 my @regions;
122              
123 172     172   821 no warnings 'utf8';
  172         212  
  172         147862  
124              
125 12         227 while (/$regex/g) {
126 11 100       122 if (@- == 1) {
127 1         8 push @regions, [ $-[0], $+[0] ];
128 1 50       65 push @{$regions[-1]}, 0 if $index
  0         0  
129             } else {
130 10         36 for my $i (1 .. $#-) {
131 16   50     55 $-[$i] // next;
132 16         67 push @regions, [ $-[$i], $+[$i] ];
133 16 100       47 push @{$regions[-1]}, $i - 1 if $index
  9         67  
134             }
135             }
136             }
137 12         62 @regions;
138             }
139              
140             sub classify_regions {
141 209 50   209 0 760 my $opt = ref $_[0] eq 'HASH' ? shift : {};
142              
143 209 100       627 $opt->{strict} and goto &classify_regions_strict;
144              
145 208         373 my @list = @{+shift};
  208         498  
146 208         308 my @by = @{+shift};
  208         526  
147 208         389 my @table;
148 208         718 for my $i (keys @by) {
149 1369         1793 my($from, $to) = $by[$i]->@*;
150 1369   100     2800 while (@list and $list[0][1] < $from) {
151 348         709 shift @list;
152             }
153 1369   100     2857 while (@list and $list[0][1] == $from and $list[0][0] < $from) {
      66        
154 0         0 shift @list;
155             }
156 1369         1789 my $t = $table[$i] = [];
157 1369   100     2926 for (my $i = 0; ($i < @list) and ($list[$i][0] < $to); $i++) {
158 520         1688 push @$t, [ $list[$i]->@* ];
159             }
160             }
161 208         748 @table;
162             }
163              
164             sub classify_regions_strict {
165 1     1 0 1 my @list = @{+shift};
  1         2  
166 1         1 my @by = @{+shift};
  1         3  
167 1         1 my @table;
168 1         3 for my $i (keys @by) {
169 1         3 my($from, $to) = $by[$i]->@*;
170 1   33     3 while (@list and $list[0][0] < $from) {
171 0         0 shift @list;
172             }
173 1         2 my $t = $table[$i] = [];
174 1   33     4 while (@list and $list[0][0] < $to and $list[0][1] <= $to) {
      33        
175 0         0 push @$t, shift @list;
176             }
177             }
178 1         2 @table;
179             }
180              
181             sub select_regions {
182 13 50   13 0 43 my $opt = ref $_[0] eq 'HASH' ? shift : {};
183 13         36 my($list, $by, $flag) = @_;
184 13         47 my($inside, $outside) = ([], []);
185 13 100       27 my $target = is_inside($flag) ? $inside : $outside;
186 13 100       44 my $overlap = $opt->{strict} ? [] : $target;
187 13         60 filter_regions($list, $by, $inside, $overlap, $outside);
188 13         49 @$target;
189             }
190              
191             ##
192             ## Split @input into @inside, @overlap, @outside by @filter and return
193             ## their pointers.
194             ##
195             ## 4th and 5th result is corresponding entry of @filter for @inside
196             ## and @overlap.
197             ##
198             sub filter_regions {
199 13     13 0 18 my @input = @{+shift};
  13         40  
200 13         24 my @filter = @{+shift};
  13         24  
201 13   50     77 my($inside, $overlap, $outside) = (shift//[], shift//[], shift//[]);
      50        
      50        
202 13         42 my($inside_match, $overlap_match) = ([], []);
203              
204 13         46 while (my($i, $range) = each @filter) {
205 105         132 my($from, $to) = @$range;
206 105   100     275 while (@input and $input[0][0] < $from and $input[0][1] <= $from) {
      100        
207 51         175 push @$outside, shift @input;
208             }
209 105   100     214 while (@input and $input[0][0] < $from) {
210 2         4 push @$overlap, shift @input;
211 2         4 push @$overlap_match, $range;
212             }
213 105   100     274 while (@input and $input[0][0] < $to and $input[0][1] <= $to) {
      66        
214 68         78 push @$inside, shift @input;
215 68         169 push @$inside_match, $range;
216             }
217 105   66     304 while (@input and $input[0][0] < $to) {
218 0         0 push @$overlap, shift @input;
219 0         0 push @$overlap_match, $range;
220             }
221             }
222 13         34 push @$outside, splice @input;
223 13         51 ($inside, $overlap, $outside, $inside_match, $overlap_match);
224             }
225              
226             sub merge_regions {
227 411 50   411 0 1183 my $option = ref $_[0] eq 'HASH' ? shift : {};
228 411         668 my $nojoin = $option->{nojoin};
229 411 50       1424 my @in = $option->{destructive} ? @_ : map { [ @$_ ] } @_;
  0         0  
230 411 50       963 unless ($option->{nosort}) {
231 411 50 66     1363 @in = sort({$a->[0] <=> $b->[0] || $b->[1] <=> $a->[1]
  1605 50       2994  
232             || (@$a > 2 ? $a->[2] <=> $b->[2] : 0)
233             } @in);
234             }
235 411         603 my @out;
236 411 100       1039 push(@out, shift @in) if @in;
237 411         1018 while (@in) {
238 1472         1484 my $top = shift @in;
239              
240 1472 100 33     3002 if ($out[-1][1] > $top->[0]) {
    50 0        
      33        
241 209 100       480 $out[-1][1] = $top->[1] if $out[-1][1] < $top->[1];
242             }
243             elsif (!$nojoin
244             and $out[-1][1] == $top->[0]
245             ##
246             ## don't connect regions in different pattern group
247             ##
248             and (@$top < 3 or $out[-1][2] == $top->[2])
249             ) {
250 0 0       0 $out[-1][1] = $top->[1] if $out[-1][1] < $top->[1];
251             }
252             else {
253 1263         1761 push @out, $top;
254             }
255             }
256 411         2008 @out;
257             }
258              
259 172     172   1100 use List::Util qw(pairs);
  172         217  
  172         53436  
260              
261             sub reverse_regions {
262 12 50   12 0 147209 my $option = ref $_[0] eq 'HASH' ? shift : {};
263 12         14 my($from, $max) = @_;
264 12   33     36 my @reverse = pairs 0, map(@{$_}[0,1], @$from), $max // length;
  17         76  
265 12 100       29 return @reverse if $option->{leave_empty};
266 9         14 grep { $_->[0] != $_->[1] } @reverse
  20         51  
267             }
268              
269             sub match_borders {
270 127     127 0 295 my $regex = shift;
271 127         348 my @border = (0);
272 127         913 while (/$regex/gp) {
273 3169         3850 my $pos = pos();
274 3169         4023 for my $i ($pos - length(${^MATCH}), $pos) {
275 6338 100       13574 push @border, $i if $border[-1] != $i;
276             }
277             }
278 127 100       457 push @border, length if $border[-1] != length;
279 127         736 @border;
280             }
281              
282             sub borders_to_regions {
283 0 0   0 0   return () if @_ < 2;
284 0           map { [ $_[$_-1], $_[$_] ] } 1..$#_;
  0            
285             }
286              
287             1;