File Coverage

blib/lib/App/linerange.pm
Criterion Covered Total %
statement 83 83 100.0
branch 55 60 91.6
condition 33 37 89.1
subroutine 6 6 100.0
pod 1 1 100.0
total 178 187 95.1


line stmt bran cond sub pod time code
1             package App::linerange;
2              
3             our $DATE = '2020-10-11'; # DATE
4             our $VERSION = '0.005'; # VERSION
5              
6 1     1   67663 use 5.010001;
  1         13  
7 1     1   5 use strict;
  1         1  
  1         20  
8 1     1   5 use warnings;
  1         2  
  1         45  
9              
10 1     1   8 use List::Util qw(max);
  1         2  
  1         125  
11              
12 1     1   7 use Exporter qw(import);
  1         1  
  1         1116  
13             our @EXPORT_OK = qw(linerange);
14              
15             our %SPEC;
16              
17             $SPEC{linerange} = {
18             v => 1.1,
19             summary => 'Retrieve line ranges from a filehandle',
20             description => <<'_',
21              
22             The routine performs a single pass on the filehandle, retrieving specified line
23             ranges.
24              
25              
26             _
27             args => {
28             fh => {
29             schema => 'filehandle*',
30             req => 1,
31             },
32             spec => {
33             summary => 'Line range specification',
34             description => <<'_',
35              
36             A comma-separated list of empty strings ("", which means all lines), specific
37             line numbers ("N") or line ranges ("N1..N2" or "N1-N2", or "N1+M" which means N2
38             is set to N1+M), where N, N1, and N2 are line number specification. Line number
39             begins at 1; it can also be a negative integer (-1 means the last line, -2 means
40             second last, and so on). N1..N2 is the same as N2..N1. Each line or range can
41             optionally be followed by "/M" to mean every M'th line (where M is an integer
42             starting from 1).
43              
44             Examples:
45              
46             * 3 (third line)
47             * 1..5 (first to fifth line)
48             * 3+0 (third line)
49             * 3+1 (third to fourth line)
50             * -3+1 (third last to second last)
51             * 5..1 (first to fifth line)
52             * -5..-1 (fifth last to last line)
53             * -1..-5 (fifth last to last line)
54             * 5..-3 (fifth line to third last)
55             * -3..5 (fifth line to third last)
56             * /3 (every 3rd line, i.e. 3, 6, 9, ...)
57             * /2 (every other line, i.e. 2, 4, 6, ...)
58             * 2..-1/3 (every 3rd line starting from line 2, i.e. 4, 7, 10, ...)
59              
60             _
61             schema => 'str',
62             default => '',
63             pos => 0,
64             },
65             },
66             examples => [
67             {
68             summary => 'By default, if spec is empty, get all lines',
69             args => {},
70             test => 0,
71             'x.doc.show_result' => 0,
72             },
73             {
74             summary => 'Get every other lines',
75             args => {spec=>'/2'},
76             test => 0,
77             'x.doc.show_result' => 0,
78             },
79             {
80             summary => 'Get lines 1-10',
81             args => {spec=>'1-10'},
82             test => 0,
83             'x.doc.show_result' => 0,
84             },
85             {
86             summary => 'Get lines 1 to 10, .. is synonym for -',
87             args => {spec=>'1 .. 10'},
88             test => 0,
89             'x.doc.show_result' => 0,
90             },
91             {
92             summary => 'Get lines 1-10 as well as 21-30',
93             args => {spec=>'1-10, 21 - 30'},
94             test => 0,
95             'x.doc.show_result' => 0,
96             },
97             {
98             summary => 'You can specify negative number, get the 5th line until 2nd last',
99             args => {spec=>'5 .. -2'},
100             test => 0,
101             'x.doc.show_result' => 0,
102             },
103             {
104             summary => 'You can specify negative number, get the 10th last until last',
105             args => {spec=>'-10 .. -1'},
106             test => 0,
107             'x.doc.show_result' => 0,
108             },
109             {
110             summary => 'Instead of N1-N2, you can use N1+M to mean N1-(N1+M), get 3rd line',
111             args => {spec=>'3+0'},
112             test => 0,
113             'x.doc.show_result' => 0,
114             },
115             {
116             summary => 'Instead of N1-N2, you can use N1+M to mean N1-(N1+M), get 3rd to 5th line',
117             args => {spec=>'3+2'},
118             test => 0,
119             'x.doc.show_result' => 0,
120             },
121             {
122             summary => 'Instead of N1-N2, you can use N1+M to mean N1-(N1+M), get 3rd last to last line',
123             args => {spec=>'-3+2'},
124             test => 0,
125             'x.doc.show_result' => 0,
126             },
127             ],
128             };
129              
130             sub linerange {
131 39     39 1 1088 my %args = @_;
132              
133 39   50     116 my $fh = $args{fh} // \*ARGV;
134              
135 39         58 my @ranges;
136             my @buffer;
137 39         55 my $bufsize = 0;
138 39         50 my $exit_after_linum = 0; # set this to a positive line number if we can optimize
139              
140 39         165 my @simple_specs = split /\s*,\s*/, $args{spec};
141 39 100       95 @simple_specs = ('') unless @simple_specs;
142              
143 39         84 for my $spec2 (@simple_specs) {
144 45 100       310 $spec2 =~ m!\A\s*
145             (?:
146             ([+-]?[0-9]+) # 1) start
147             \s*
148             (?:
149             (\.\.|-|\+)\s* # 2) range 'operator'
150             ([+-]?[0-9]+)\s* # 3) end
151             )?
152             )?
153             (?:
154             /\s*
155             ([0-9]+) # 4) every
156             )?
157             \z!x
158             or return [400, "Invalid line number/range specification '$spec2'"];
159              
160 44         84 my ($ln1, $ln2, $every);
161 44 100 66     166 if (!defined $1 && !defined $2) {
162 11         18 $ln1 = 1;
163 11         16 $ln2 = -1;
164             } else {
165 33         60 $ln1 = $1;
166 33   66     94 $ln2 = $3 // $1;
167 33 100 100     135 if (defined $2 && $2 eq '+') {
168 7         15 $ln2 = $ln1 + $ln2;
169 7 100       15 if ($ln1 > 0) {
170 3 50       18 $ln2 = 1 if $ln2 < 1;
171             } else {
172 4 50       12 $ln2 = -1 if $ln2 > -1;
173             }
174             }
175             }
176 44   100     148 $every = $4 // 1;
177 44 100       90 if ($every == 0) {
178 1         11 return [400, "Invalid 0 in every in range specification '$spec2', ".
179             "start from 1"];
180             }
181              
182 43 100 100     246 if ($ln1 == 0 || $ln2 == 0) {
    100 100        
    100 100        
183 2         19 return [400, "Invalid line number 0 in ".
184             "range specification '$spec2', start from 1"];
185             } elsif ($ln1 > 0 && $ln2 > 0) {
186 13 100       43 push @ranges, $ln1 > $ln2 ?
187             [$ln2, $ln1, $every] : [$ln1, $ln2, $every];
188 13 50       28 unless ($exit_after_linum < 0) {
189 13 50       26 $exit_after_linum = $ln1 if $exit_after_linum < $ln1;
190 13 100       31 $exit_after_linum = $ln2 if $exit_after_linum < $ln2;
191             }
192             } elsif ($ln1 < 0 && $ln2 < 0) {
193 10 100       24 $bufsize = -$ln1 if $bufsize < -$ln1;
194 10 100       20 $bufsize = -$ln2 if $bufsize < -$ln2;
195 10 100       30 push @ranges, $ln1 > $ln2 ?
196             [$ln1, $ln2, $every] : [$ln2, $ln1, $every];
197 10         21 $exit_after_linum = -1;
198             } else {
199 18         22 $exit_after_linum = -1;
200 18 100       34 if ($ln1 > 0) {
201 17 100       35 $bufsize = -$ln2 if $bufsize < -$ln2;
202 17         57 push @ranges, [$ln1, $ln2, $every];
203             } else {
204 1 50       4 $bufsize = -$ln1 if $bufsize < -$ln1;
205 1         4 push @ranges, [$ln2, $ln1, $every];
206             }
207             }
208             }
209              
210 35         45 my %reslines; # result lines, key = linenum
211 35         51 my $linenum = 0;
212 35         373 while (defined(my $line = <$fh>)) {
213 437         595 $linenum++;
214 437 100 100     778 last if $exit_after_linum >= 0 && $linenum > $exit_after_linum;
215 427 100       633 if ($bufsize) {
216 400         648 push @buffer, $line;
217 400 100       669 if (@buffer > $bufsize) { shift @buffer }
  334         432  
218             }
219 427         689 for my $range (@ranges) {
220             # check if line is included by range (N1-N2)
221             next unless
222 510 100 100     1773 $range->[0] > 0 && $linenum >= $range->[0] &&
      100        
      100        
223             ($range->[1] < 0 ||
224             $range->[1] > 0 && $linenum <= $range->[1]);
225             # check if line is included by every (N3)
226             #say "D:linenum=$linenum, range=".join(",",@$range).", ".($linenum-1 - $range->[0]+1)." % $range->[2] == ".(($linenum-1 + $range->[0]-1) % $range->[2]);
227 279 100 66     968 next unless $range->[0] > 0 && (($linenum-1 - $range->[0]+1) % $range->[2] == $range->[2]-1);
228 161         578 $reslines{$linenum} = $line;
229             }
230             }
231              
232 35         104 my $bufstartline = $linenum - @buffer + 1;
233              
234             # remove positive-only ranges
235 35         75 @ranges = grep { $_->[1] < 0 } @ranges;
  41         150  
236              
237             # add/remove result lines that are in the buffer
238 35         77 for my $stage (0..1) {
239 70         104 for my $range (@ranges) {
240 56 100       173 my $bufpos1 = $range->[0] > 0 ?
241             max($range->[0] - $bufstartline, 0) :
242             $linenum + $range->[0] + 1 - $bufstartline;
243 56         97 my $bufpos2 = $linenum + $range->[1] + 1 - $bufstartline;
244 56 100       97 ($bufpos1, $bufpos2) = ($bufpos2, $bufpos1) if $bufpos1 > $bufpos2;
245 56 100       94 if ($stage == 0) {
246 28         79 for my $offset ((0 .. $bufpos1-1), ($bufpos2+1 .. $#buffer)) {
247 30         64 delete $reslines{ $bufstartline + $offset };
248             }
249             } else {
250 28         52 for my $offset ($bufpos1 .. $bufpos2) {
251             # check with every again
252 47 100       104 next unless ($offset % $range->[2] == $range->[2]-1);
253             #say "D:adding result line in buffer: offset=$offset, linenum=".($bufstartline + $offset);
254 34         99 $reslines{ $bufstartline + $offset } = $buffer[$offset];
255             }
256             }
257             }
258             }
259              
260 35         193 [200, "OK", [map {$reslines{$_}} sort {$a <=> $b} keys %reslines]];
  167         552  
  325         455  
261             }
262              
263             1;
264             # ABSTRACT: Retrieve line ranges from a filehandle
265              
266             __END__