File Coverage

blib/lib/BioX/Seq/Fetch.pm
Criterion Covered Total %
statement 146 149 97.9
branch 51 64 81.2
condition 16 20 80.0
subroutine 12 12 100.0
pod 5 5 100.0
total 230 250 92.4


line stmt bran cond sub pod time code
1             package BioX::Seq::Fetch;
2              
3 2     2   6912 use 5.012;
  2         9  
4 2     2   15 use strict;
  2         6  
  2         69  
5 2     2   13 use warnings;
  2         6  
  2         145  
6              
7 2     2   12 use BioX::Seq;
  2         4  
  2         68  
8 2     2   1505 use File::stat;
  2         19308  
  2         263  
9              
10 2     2   23 use constant MAGIC_GZIP => pack('C3', (0x1f, 0x8b, 0x08));
  2         22  
  2         4635  
11              
12             sub new {
13              
14 10     10 1 824 my ($class,$fn,%args) = @_;
15              
16 10 100       60 die "Must define input filename for random access"
17             if (! defined $fn);
18              
19 9         48 my $self = bless {fn => $fn}, $class;
20              
21 9 100       576 open my $fh, '<', $fn
22             or die "Error opening $fn for reading: $!\n";
23              
24             # read magic bytes and reset filehandle
25 8         77 my $old_layers = join '', map {":$_"} PerlIO::get_layers($fh);
  16         76  
26 8         31 binmode($fh);
27 8         238 read( $fh, my $magic_start, 3 );
28 8         32 read( $fh, my $magic_end, 1 );
29 8         103 binmode($fh,$old_layers);
30 8         39 seek($fh,0,0);
31              
32             # detect compression (must be BGZIP)
33 8 100       36 if ($magic_start eq MAGIC_GZIP) {
34 3 100       60 die "Gzip files must be compressed with bgzip for non-sequential access"
35             if (ord($magic_end) != 0x04);
36             # TODO: find way to test missing library during unit testing
37             require Compress::BGZF::Reader
38 2 50       1085 or die "Compress::BGZF::Reader not installed, no gzip support";
39 2         35996 close $fh;
40 2         15 $fh = Compress::BGZF::Reader->new_filehandle($fn);
41             }
42              
43 7 100       1920 $self->{with_description} = 1 if ($args{with_description});
44              
45 7         36 $self->{fh} = $fh;
46              
47 7         34 $self->_load_faidx;
48              
49 3         121 return $self;
50              
51             }
52              
53             sub write_index {
54            
55 8     8 1 1330 my ($self, $fn_idx) = @_;
56              
57 8   66     62 $fn_idx //= $self->{fn} . '.fai';
58              
59 8 50       122 if (-e $fn_idx) {
60 0         0 warn "Index file exists, won't overwrite\n";
61 0         0 return 0;
62             }
63              
64 8 50       1728 open my $idx, '>:raw', $fn_idx
65             or die "Error opening $fn_idx for writing: $!\n";
66              
67 8         43 my $fh = $self->{fh};
68              
69 8         29 my $curr_id;
70             my $curr_len;
71 8         0 my $curr_bases;
72 8         0 my $curr_bytes;
73 8         0 my $curr_offset;
74 8         10 my $bl_mismatch = 0;
75 8         12 my $ll_mismatch = 0;
76 8         169 while (my $line = <$fh>) {
77 65 100       2059 if ($line =~ /^>(\S+)/) {
    100          
    100          
78 25 100       45 if (defined $curr_id) {
79 17         19 say {$idx} join "\t",
  17         88  
80             $curr_id,
81             $curr_len,
82             $curr_offset,
83             $curr_bases,
84             $curr_bytes,
85             ;
86             }
87 25         56 $curr_id = $1;
88 25         55 $curr_offset = tell $fh;
89 25         57 $curr_len = 0;
90 25         35 $curr_bases = undef;
91 25         29 $curr_bytes = undef;
92 25         27 $bl_mismatch = 0;
93 25         65 $ll_mismatch = 0;
94             }
95             elsif ($line =~ /^[A-Za-z\-\*\.]+(\r?\n?)$/) {
96 37 100       61 if ($bl_mismatch) {
97 1         12 close $idx;
98 1         111 unlink $fn_idx;
99 1         32 die "Base length mismatch\n";
100             }
101 36 100       72 if ($ll_mismatch) {
102 1         18 close $idx;
103 1         102 unlink $fn_idx;
104 1         62 die "Line length mismatch\n";
105             }
106 35         66 my $ll = length $line;
107 35         58 my $bl = $ll - length $1;
108 35         47 $curr_len += $bl;
109 35   66     116 $curr_bases //= $bl;
110 35   66     89 $curr_bytes //= $ll;
111 35 100       121 $bl_mismatch = 1 if ($bl != $curr_bases);
112 35 100       125 $ll_mismatch = 1 if ($ll != $curr_bytes);
113             }
114             elsif ($line =~ /\S/) {
115 1         89 close $idx;
116 1         103 unlink $fn_idx;
117 1         32 die "Unexpected content: $line";
118             }
119            
120             }
121              
122             # write remaining index
123             # uncoverable branch false
124 5 50       122 if (defined $curr_id) {
125 5         8 say {$idx} join "\t",
  5         22  
126             $curr_id,
127             $curr_len,
128             $curr_offset,
129             $curr_bases,
130             $curr_bytes,
131             ;
132             }
133              
134 5         300 close $idx;
135              
136             #reset filehandle
137 5         43 seek $self->{fh}, 0, 0;
138              
139 5         1116 return 1;
140              
141             }
142            
143              
144             sub _load_faidx {
145            
146 7     7   22 my ($self) = @_;
147              
148 7         16 my $fn_idx = $self->{fn} . '.fai';
149 7 50       342 $self->write_index if (! -e $fn_idx);
150              
151             # make sure FASTA file is not newer than its index; otherwise index file
152             # may be out of date and cause silent corruption downstream
153 4         41 my $mtime_fas = stat($self->{fn})->mtime;
154 4         1108 my $mtime_idx = stat($fn_idx)->mtime;
155 4 50       572 if ($mtime_fas > $mtime_idx) {
156 0         0 die "Index file exists but is older than FASTA file and probably out"
157             . " of date. Refresh or remove the existing index before"
158             . " proceeding.\n"
159             }
160              
161 4         136 my @ids;
162              
163 4 50       179 open my $in, '<', $fn_idx
164             or die "Error opening index file: $!\n";
165 4         263 while (my $line = <$in>) {
166 15         27 chomp $line;
167 15         57 my ($name, $len, $offset, $bases_per_line, $bytes_per_line)
168             = split "\t", $line;
169             die "ERROR: $fn_idx contains duplicate entries"
170 15 100       85 if ( defined $self->{idx}->{$name} );
171 14         25 my $eol = $bytes_per_line - $bases_per_line;
172 14         84 $self->{idx}->{$name} = [$len, $offset, $bases_per_line, $eol];
173 14         70 push @ids, $name;
174             }
175 3         36 close $in;
176 3         16 $self->{ids} = \@ids;
177              
178 3         18 return;
179              
180             }
181              
182             sub ids {
183              
184 1     1 1 4 my ($self) = @_;
185 1         2 return @{ $self->{ids} };
  1         6  
186              
187             }
188              
189             sub length {
190              
191 2     2 1 846 my ($self, $id) = @_;
192 2         11 return $self->{idx}->{$id}->[0];
193              
194             }
195              
196             sub fetch_seq {
197            
198 8     8 1 713 my ($self, $id, $start_bp, $end_bp) = @_;
199              
200 8 50       36 return undef if (! defined $self->{idx}->{$id});
201 8         12 my ($len, $off, $bpl, $eol) = @{ $self->{idx}->{$id} };
  8         24  
202              
203 8         15 my $fh = $self->{fh};
204              
205 8   100     29 $start_bp = $start_bp // 1;
206 8   66     16 $end_bp = $end_bp // $len;
207 8         66 --$start_bp; #make 0-based
208 8 100 100     72 die "Coordinates out of bounds" if ($start_bp < 0 || $end_bp > $len);
209 6         11 my $l = $end_bp - $start_bp;
210              
211 6         38 seek $fh, $off + $start_bp + int(($start_bp)/$bpl)*$eol, 0;
212 6         217 read($fh, my $seq, $l + int(($l+$start_bp%$bpl)/$bpl)*$eol);
213 6         218 $seq =~ s/\s//g;
214              
215 6         12 ++$start_bp;
216              
217 6         8 my $desc;
218 6 100 100     71 if ($start_bp > 1 || $end_bp < $len) {
219 2         8 $desc = "($start_bp-$end_bp)";
220             }
221              
222             # backtrack to find defline, if asked
223 6 100       17 if ($self->{with_description}) {
224              
225 1         3 my $string = '';
226 1         2 my $p = $off;
227 1         1 my $chr;
228              
229             BACK:
230 1         4 while ($p > 0) {
231 65         62 --$p;
232 65         105 seek $fh, $p, 0;
233 65 50       1570 read($fh, $chr, 1)
234             or die "Error during backtrack read: $@\n";
235             # stop at record separator
236 65 100       1302 if ($chr eq '>') {
237 2 50       6 last BACK if ($p == 0);
238 2         2 my $prev;
239 2         5 seek $fh, $p-1, 0;
240 2         51 read($fh, $prev, 1);
241 2 100       50 last BACK if ($prev =~ /[\r\n]/);
242             }
243              
244 64         123 $string = $chr . $string;
245             }
246              
247 1 50       6 die "Backtracking inexplicably failed\n"
248             if (! CORE::length $string);
249 1 50       18 if ($string =~ /^(\S+)(?:\s+(.*))?$/) {
250 1         6 my $nid = $1;
251 1         4 $desc = join ' ', grep {defined $_} $2, $desc;
  2         7  
252 1 50       5 die "ID mismatch ($id vs $nid)!\n"
253             if ($nid ne $id);
254             }
255            
256             }
257              
258 6         37 return BioX::Seq->new($seq, $id, $desc);
259              
260             }
261              
262              
263             1;
264              
265              
266             __END__