File Coverage

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


line stmt bran cond sub pod time code
1             package App::Greple::Regions;
2              
3 135     135   89894 use v5.24;
  135         522  
4 135     135   827 use warnings;
  135         266  
  135         11877  
5             BEGIN {
6 135     135   504 eval { warnings->unimport('experimental::regex_sets') };
  135         3366  
7 135         349 eval { warnings->unimport('experimental::vlb') };
  135         5132  
8             }
9 135     135   901 use Carp;
  135         282  
  135         10480  
10 135     135   1012 use Data::Dumper;
  135         260  
  135         10225  
11             $Data::Dumper::Sortkeys = 1;
12              
13 135     135   926 use Exporter 'import';
  135         284  
  135         16157  
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 135         101363 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 135     135   846 };
  135         309  
38              
39             sub new {
40 9     9 0 18 my $class = shift;
41              
42 9         46 my $obj = bless {
43             FLAG => undef,
44             SPEC => undef,
45             }, $class;
46              
47 9 50       107 $obj->configure(@_) if @_;
48              
49 9         37 $obj;
50             }
51              
52             sub configure {
53 9     9 0 22 my $obj = shift;
54 9         34 while (@_ >= 2) {
55 18         65 $obj->{$_[0]} = $_[1];
56 18         46 splice @_, 0, 2;
57             }
58             }
59              
60 9     9 0 85 sub spec { $_[0]->{SPEC} }
61 36     36 0 204 sub flag { $_[0]->{FLAG} }
62             sub is_x {
63 36     36 0 88 my($p, $mask, $set) = @_;
64 36 100       136 ((ref $p ? $p->flag : $p) & $mask) == $set;
65             }
66 9     9 0 47 sub is_union { is_x $_[0], REGION_SET_MASK, REGION_UNION }
67 9     9 0 27 sub is_intersect { is_x $_[0], REGION_SET_MASK, REGION_INTERSECT }
68 16     16 0 43 sub is_inside { is_x $_[0], REGION_AREA_MASK, REGION_INSIDE }
69 2     2 0 6 sub is_outside { is_x $_[0], REGION_AREA_MASK, REGION_OUTSIDE }
70              
71             package App::Greple::Regions::Holder {
72              
73             sub new {
74 129     129   362 my $class = shift;
75 129         552 bless [], $class;
76             }
77              
78             sub append {
79 9     9   22 my $obj = shift;
80 9         136 push @$obj, App::Greple::Regions->new(@_);
81             }
82              
83             sub regions {
84 242     242   1027 my $obj = shift;
85 242         1981 @$obj;
86             }
87              
88             sub union {
89 121     121   1592 grep { $_->is_union } shift->regions;
  9         36  
90             }
91              
92             sub intersect {
93 121     121   864 grep { $_->is_intersect } shift->regions;
  9         54  
94             }
95             }
96              
97             sub match_regions {
98 169     169 0 3687 my %arg = @_;
99 169   33     863 my $pattern = $arg{pattern} // croak "Parameter error";
100 169 100       1088 my $regex = ref $pattern eq 'Regexp' ? $pattern : qr/$pattern/m;
101 169 100       927 return &match_regions_by_group($regex, $arg{index}) if $arg{group};
102              
103 157         352 my @regions;
104              
105 135     135   3434 no warnings 'utf8';
  135         1678  
  135         22549  
106              
107 157         2237 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 1509         3021 my $pos = pos();
114 1509         8874 push @regions, [ $pos - length(${^MATCH}), $pos ];
115             }
116 157         1597 @regions;
117             }
118              
119             sub match_regions_by_group {
120 12     12 0 32 my($regex, $index) = @_;
121 12         23 my @regions;
122              
123 135     135   1593 no warnings 'utf8';
  135         620  
  135         180336  
124              
125 12         171 while (/$regex/g) {
126 11 100       52 if (@- == 1) {
127 1         14 push @regions, [ $-[0], $+[0] ];
128 1 50       128 push @{$regions[-1]}, 0 if $index
  0         0  
129             } else {
130 10         37 for my $i (1 .. $#-) {
131 16   50     64 $-[$i] // next;
132 16         92 push @regions, [ $-[$i], $+[$i] ];
133 16 100       78 push @{$regions[-1]}, $i - 1 if $index
  9         46  
134             }
135             }
136             }
137 12         107 @regions;
138             }
139              
140             sub classify_regions {
141 150 50   150 0 944 my $opt = ref $_[0] eq 'HASH' ? shift : {};
142              
143 150 100       677 $opt->{strict} and goto &classify_regions_strict;
144              
145 149         357 my @list = @{+shift};
  149         612  
146 149         365 my @by = @{+shift};
  149         681  
147 149         914 my @table;
148 149         694 for my $i (keys @by) {
149 1264         2582 my($from, $to) = $by[$i]->@*;
150 1264   100     3963 while (@list and $list[0][1] < $from) {
151 324         902 shift @list;
152             }
153 1264   100     3821 while (@list and $list[0][1] == $from and $list[0][0] < $from) {
      66        
154 0         0 shift @list;
155             }
156 1264         2744 my $t = $table[$i] = [];
157 1264   100     4255 for (my $i = 0; ($i < @list) and ($list[$i][0] < $to); $i++) {
158 462         2253 push @$t, [ $list[$i]->@* ];
159             }
160             }
161 149         901 @table;
162             }
163              
164             sub classify_regions_strict {
165 1     1 0 2 my @list = @{+shift};
  1         2  
166 1         6 my @by = @{+shift};
  1         2  
167 1         2 my @table;
168 1         2 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     3 while (@list and $list[0][0] < $to and $list[0][1] <= $to) {
      33        
175 0         0 push @$t, shift @list;
176             }
177             }
178 1         3 @table;
179             }
180              
181             sub select_regions {
182 9 50   9 0 52 my $opt = ref $_[0] eq 'HASH' ? shift : {};
183 9         23 my($list, $by, $flag) = @_;
184 9         29 my($inside, $outside) = ([], []);
185 9 100       28 my $target = is_inside($flag) ? $inside : $outside;
186 9 100       36 my $overlap = $opt->{strict} ? [] : $target;
187 9         34 filter_regions($list, $by, $inside, $overlap, $outside);
188 9         55 @$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 9     9 0 21 my @input = @{+shift};
  9         31  
200 9         18 my @filter = @{+shift};
  9         39  
201 9   50     68 my($inside, $overlap, $outside) = (shift//[], shift//[], shift//[]);
      50        
      50        
202 9         44 my($inside_match, $overlap_match) = ([], []);
203              
204 9         52 while (my($i, $range) = each @filter) {
205 101         163 my($from, $to) = @$range;
206 101   100     337 while (@input and $input[0][0] < $from and $input[0][1] <= $from) {
      100        
207 51         200 push @$outside, shift @input;
208             }
209 101   100     307 while (@input and $input[0][0] < $from) {
210 2         5 push @$overlap, shift @input;
211 2         9 push @$overlap_match, $range;
212             }
213 101   100     319 while (@input and $input[0][0] < $to and $input[0][1] <= $to) {
      66        
214 66         107 push @$inside, shift @input;
215 66         241 push @$inside_match, $range;
216             }
217 101   66     422 while (@input and $input[0][0] < $to) {
218 0         0 push @$overlap, shift @input;
219 0         0 push @$overlap_match, $range;
220             }
221             }
222 9         28 push @$outside, splice @input;
223 9         44 ($inside, $overlap, $outside, $inside_match, $overlap_match);
224             }
225              
226             sub merge_regions {
227 359 50   359 0 1408 my $option = ref $_[0] eq 'HASH' ? shift : {};
228 359         726 my $nojoin = $option->{nojoin};
229 359 50       1377 my @in = $option->{destructive} ? @_ : map { [ @$_ ] } @_;
  0         0  
230 359 50       2010 unless ($option->{nosort}) {
231 359 50 66     1114 @in = sort({$a->[0] <=> $b->[0] || $b->[1] <=> $a->[1]
  1566 50       4338  
232             || (@$a > 2 ? $a->[2] <=> $b->[2] : 0)
233             } @in);
234             }
235 359         638 my @out;
236 359 100       1119 push(@out, shift @in) if @in;
237 359         1040 while (@in) {
238 1437         2236 my $top = shift @in;
239              
240 1437 100 33     4561 if ($out[-1][1] > $top->[0]) {
    50 0        
      33        
241 202 50       619 $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 1235         2832 push @out, $top;
254             }
255             }
256 359         2297 @out;
257             }
258              
259 135     135   1295 use List::Util qw(pairs);
  135         255  
  135         58548  
260              
261             sub reverse_regions {
262 12 50   12 0 267692 my $option = ref $_[0] eq 'HASH' ? shift : {};
263 12         24 my($from, $max) = @_;
264 12   33     35 my @reverse = pairs 0, map(@{$_}[0,1], @$from), $max // length;
  17         105  
265 12 100       36 return @reverse if $option->{leave_empty};
266 9         15 grep { $_->[0] != $_->[1] } @reverse
  20         61  
267             }
268              
269             sub match_borders {
270 109     109 0 304 my $regex = shift;
271 109         439 my @border = (0);
272 109         5009 while (/$regex/gp) {
273 2711         4651 my $pos = pos();
274 2711         5022 for my $i ($pos - length(${^MATCH}), $pos) {
275 5422 100       17479 push @border, $i if $border[-1] != $i;
276             }
277             }
278 109 100       622 push @border, length if $border[-1] != length;
279 109         872 @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;