File Coverage

lib/Chemistry/OpenSMILES/Parser.yp
Criterion Covered Total %
statement 231 235 98.3
branch 94 102 92.1
condition 53 60 88.3
subroutine 22 22 100.0
pod 0 2 0.0
total 400 421 95.0


line stmt bran cond sub pod time code
1             # Header section
2              
3             %{
4              
5 21     21   167 use warnings;
  21         39  
  21         684  
6 21     21   371 use 5.0100;
  21         71  
7              
8 21         2897 use Chemistry::OpenSMILES qw(
9             is_aromatic
10             is_chiral
11             %normal_valence
12             toggle_cistrans
13 21     21   5847 );
  21         46  
14 21     21   9615 use Graph::Undirected;
  21         802613  
  21         756  
15 21     21   176 use List::Util qw(any sum);
  21         42  
  21         43040  
16              
17             my %bond_order = (
18             '-' => 1,
19             '=' => 2,
20             '#' => 3,
21             '$' => 4,
22             );
23              
24             %}
25              
26             %%
27 234     234 0 200866  
28 234 50       605 # Rules section
29              
30             # The top-level 'filter' rule
31              
32             smiles: chain ;
33              
34             chain: atom
35             {
36 470     470   18679 my $g = Graph::Undirected->new( refvertexed => 1 );
37 470         160426 $g->add_vertex( $_[1] );
38 470         30968 push @{$_[0]->{USER}{GRAPHS}}, $g;
  470         1188  
39              
40 470         824 $_[1]->{graph} = $g;
41 470         562 $_[1]->{index} = @{$_[0]->{USER}{GRAPHS}}-1;
  470         1040  
42 470         724 $_[1]->{first_of_chain} = 1;
43              
44 470         1480 return { first => $_[1],
45             last => $_[1] };
46             }
47             | chain atom
48             {
49 299     299   11206 $_[2]->{graph} = $_[1]->{last}{graph};
50 299         490 $_[2]->{index} = $_[1]->{last}{index};
51              
52 299         936 $_[2]->{graph}->add_edge( $_[1]->{last}, $_[2] );
53              
54 299 100 100     127276 if( is_aromatic $_[1]->{last} && is_aromatic $_[2] ) {
55             $_[2]->{graph}->set_edge_attribute( $_[1]->{last},
56 45         124 $_[2],
57             'bond',
58             ':' );
59             }
60              
61 299         10314 delete $_[2]->{first_of_chain};
62              
63 299         736 _push_chirality_neighbour( $_[1]->{last}, $_[2] );
64 299         701 _push_chirality_neighbour( $_[2], $_[1]->{last} );
65              
66 299         576 $_[1]->{last} = $_[2];
67              
68 299         552 return $_[1];
69             }
70             | chain bond atom
71             {
72 60     60   2272 $_[3]->{graph} = $_[1]->{last}{graph};
73 60         106 $_[3]->{index} = $_[1]->{last}{index};
74              
75 60 100       143 if( $_[2] ne '-' ) {
76             $_[3]->{graph}->set_edge_attribute( $_[1]->{last},
77 55         207 $_[3],
78             'bond',
79             $_[2] );
80             } else {
81 5         31 $_[3]->{graph}->add_edge( $_[1]->{last}, $_[3] );
82             }
83              
84 60         49770 delete $_[3]->{first_of_chain};
85              
86 60         175 _push_chirality_neighbour( $_[1]->{last}, $_[3] );
87 60         161 _push_chirality_neighbour( $_[3], $_[1]->{last} );
88              
89 60         120 $_[1]->{last} = $_[3];
90              
91 60         139 return $_[1];
92             }
93             | chain '.' atom
94             {
95 11     11   436 my $g = Graph::Undirected->new( refvertexed => 1 );
96 11         2153 $g->add_vertex( $_[3] );
97 11         652 push @{$_[0]->{USER}{GRAPHS}}, $g;
  11         27  
98              
99 11         20 $_[3]->{graph} = $g;
100 11         15 $_[3]->{index} = @{$_[0]->{USER}{GRAPHS}}-1;
  11         26  
101 11         21 $_[3]->{first_of_chain} = 1;
102              
103 11         36 return { first => $_[3],
104             last => $_[3] };
105             }
106             | chain '(' chain ')'
107             {
108 194 100   194   8627 if( $_[1]->{last}{index} != $_[3]->{first}{index} ) {
109             $_[0]->_merge_graphs( $_[1]->{last}{index},
110 184         491 $_[3]->{first}{index} );
111             }
112              
113 194         672 $_[1]->{last}{graph}->add_edge( $_[1]->{last}, $_[3]->{first} );
114              
115 194 100 100     56543 if( is_aromatic $_[1]->{last} && is_aromatic $_[3]->{first} ) {
116             $_[1]->{last}{graph}->set_edge_attribute( $_[1]->{last},
117             $_[3]->{first},
118 3         18 'bond',
119             ':' );
120             }
121              
122 194         1052 delete $_[3]->{first}{first_of_chain};
123              
124 194         481 _push_chirality_neighbour( $_[1]->{last}, $_[3]->{first} );
125 194         541 _unshift_chirality_neighbour( $_[3]->{first}, $_[1]->{last} );
126              
127 194         448 return $_[1];
128             }
129             | chain '(' bond chain ')'
130             {
131 41 100   41   1898 if( $_[1]->{last}{index} != $_[4]->{first}{index} ) {
132             $_[0]->_merge_graphs( $_[1]->{last}{index},
133 36         105 $_[4]->{first}{index} );
134             }
135              
136 41 100       108 if( $_[3] ne '-' ) {
137             $_[1]->{last}{graph}->set_edge_attribute( $_[1]->{last},
138             $_[4]->{first},
139 39         132 'bond',
140             $_[3] );
141             } else {
142             $_[1]->{last}{graph}->add_edge( $_[1]->{last},
143 2         8 $_[4]->{first} );
144             }
145              
146 41         24117 delete $_[4]->{first}{first_of_chain};
147              
148 41         118 _push_chirality_neighbour( $_[1]->{last}, $_[4]->{first} );
149 41         126 _unshift_chirality_neighbour( $_[4]->{first}, $_[1]->{last} );
150              
151 41         95 return $_[1];
152             }
153             | chain '(' '.' chain ')'
154              
155             # According to the specification of OpenSMILES, ring bonds are
156             # allowed only before the branch enumeration. However, I think this
157             # is too strict.
158              
159             | chain ringbond
160             {
161 107     107   4193 $_[0]->_add_ring_bond( $_[1]->{last}, $_[2] );
162 107         227 return $_[1];
163             }
164             | chain bond ringbond
165             {
166 26     26   1052 $_[0]->_add_ring_bond( $_[1]->{last}, $_[3], $_[2] );
167 25         60 return $_[1];
168             }
169 234         13100 ;
170              
171             bond: '-' | '=' | '#' | '$' | ':' | '/' | '\\' ;
172              
173             %%
174              
175             # Footer section
176              
177             sub _Error
178             {
179 10     10   174 my( $self ) = @_;
180 10 50       27 close $self->{USER}{FILEIN} if $self->{USER}{FILEIN};
181              
182 10 100 100     17 if( ${$self->{TOKEN}} eq '' &&
  10         35  
183 7 100 100     36 grep { defined $_ && !ref $_ && $_ eq '(' }
184 7         13 map { $_->[1] } @{$self->{STACK}} ) {
  2         6  
185 1         79 die "$0: syntax error: missing closing parenthesis.\n";
186             }
187              
188 9 100       10 if( ${$self->{TOKEN}} eq ')' ) {
  9         22  
189 2         86 die "$0: syntax error: unbalanced parentheses.\n";
190             }
191              
192 7         27 my $msg = "$0: syntax error at position $self->{USER}{CHARNO}";
193 234 100       15451 if( $self->YYData->{INPUT} ) {
  7         15  
194 6         40 $self->YYData->{INPUT} =~ s/\n$//;
195 6         47 die "$msg: '" . $self->YYData->{INPUT} . "'.\n";
196             } else {
197 1         48 die "$msg.\n";
198             }
199             }
200              
201             sub _Lexer
202             {
203 1824     1824   54184 my( $self ) = @_;
204              
205             # If the line is empty and the input is originating from the file,
206             # another line is read.
207 1824 50 66     3618 if( !$self->YYData->{INPUT} && $self->{USER}{FILEIN} ) {
208 0         0 my $filein = $self->{USER}{FILEIN};
209 0         0 $self->YYData->{INPUT} = <$filein>;
210 0         0 $self->{USER}{CHARNO} = 0;
211             }
212              
213 1824 50       12960 if( $self->YYData->{INPUT} =~ s/^(\s+)// ) {
214 0         0 $self->{USER}{CHARNO} += length $1;
215             }
216              
217 1824         13296 my $hcount_re = 'H[0-9]?';
218 1824 100       3720 if( defined $self->{USER}{OPTIONS}{max_hydrogen_count_digits} ) {
219             $hcount_re = sprintf 'H[0-9]{0,%d}',
220 2         10 $self->{USER}{OPTIONS}{max_hydrogen_count_digits};
221             }
222              
223             # Bracket atoms
224 1824 100       3167 if( $self->YYData->{INPUT} =~ s/^\[ (?[0-9]+)?
225             (?[A-Za-z][a-z]?|\*)
226             (?@(
227             (TH|AL)[12] |
228             SP [123] |
229             (TB|OH)[0-9]{1,2} |
230             @?
231             ))?
232             (? $hcount_re)?
233             (?--|\+\+|[-+][0-9]{0,2})?
234             (:(?[0-9]+))? \]//x ) {
235 21     21   11716 my $atom = { %+, number => $self->{USER}{ATOMNO} };
  21         9346  
  21         41678  
  107         3157  
236 107         339 $self->{USER}{ATOMNO} ++;
237 107         272 $self->{USER}{CHARNO} += length $&;
238              
239 107 100       260 if( $atom->{charge} ) {
240 19         97 $atom->{charge} =~ s/^([-+])$/${1}1/;
241 19         74 $atom->{charge} =~ s/^([-+])\1$/${1}2/;
242 19         60 $atom->{charge} = int $atom->{charge};
243             }
244              
245 107 100       248 if( $atom->{hcount} ) {
246 17         63 $atom->{hcount} =~ s/^H//;
247 17 100       58 $atom->{hcount} = $atom->{hcount} ? int $atom->{hcount} : 1;
248             } else {
249 90         154 $atom->{hcount} = 0;
250             }
251              
252 107 100       235 if( $atom->{isotope} ) {
253 6         14 $atom->{isotope} = int $atom->{isotope};
254             }
255              
256             # Atom class is an arbitrary number, 0 by default
257 107 50       247 $atom->{class} = exists $atom->{class} ? int $atom->{class} : 0;
258              
259 107         450 return ( 'atom', $atom );
260             }
261              
262             # Bracketless atoms
263 1717 100       17723 if( $self->YYData->{INPUT} =~ s/^(Br|Cl|[BCINOPSFbcnops*])// ) {
264             my $atom = { symbol => $1,
265             class => 0,
266 733         8385 number => $self->{USER}{ATOMNO} };
267 733         1135 $self->{USER}{ATOMNO} ++;
268 733         1406 $self->{USER}{CHARNO} += length $&;
269 733         2750 return ( 'atom', $atom );
270             }
271              
272             # Ring bonds
273 984 100 100     6873 if( $self->YYData->{INPUT} =~ s/^%([0-9]{2})// ||
274             $self->YYData->{INPUT} =~ s/^([0-9])// ) {
275 133         2079 $self->{USER}{CHARNO} += length $&;
276 133         662 return ( 'ringbond', int $1 );
277             }
278              
279 851         11050 my $char = substr( $self->YYData->{INPUT}, 0, 1 );
280 851 100       5399 if( $char ne '' ) {
281 624         1031 $self->YYData->{INPUT} = substr( $self->YYData->{INPUT}, 1 );
282             }
283 851         5920 $self->{USER}{CHARNO} ++;
284 851         2793 return( $char, $char );
285             }
286              
287             sub parse
288             {
289 236     236 0 4023 my( $self, $string, $options ) = @_;
290 236 100       586 $options = {} unless $options;
291              
292 236         603 $self->YYData->{INPUT} = $string;
293 236         1873 $self->{USER}{GRAPHS} = [];
294 236         446 $self->{USER}{RINGBONDS} = {};
295 236         393 $self->{USER}{ATOMNO} = 0;
296 236         360 $self->{USER}{CHARNO} = 0;
297 236         368 $self->{USER}{OPTIONS} = $options;
298             $self->YYParse( yylex => \&_Lexer,
299             yyerror => \&_Error,
300 236         1096 yydebug => $options->{debug} );
301              
302 225 100       19543 if( scalar keys %{$self->{USER}{RINGBONDS}} ) {
  225         901  
303             die "$0: unclosed ring bond(s) detected: " .
304 2         8 join( ', ', sort { $a <=> $b } keys %{$self->{USER}{RINGBONDS}} ) .
  1         38  
  2         46  
305             ".\n";
306             }
307              
308 223         367 my @graphs = grep { defined } @{$self->{USER}{GRAPHS}};
  470         1058  
  223         451  
309 223         451 for my $graph (@graphs) {
310 228         605 for my $atom (sort { $a->{number} <=> $b->{number} } $graph->vertices) {
  1091         4778  
311 820         4297 delete $atom->{graph};
312 820         1055 delete $atom->{index};
313 820 100       1437 if( !$options->{raw} ) {
314             # Promote implicit hydrogen atoms into explicit ones
315 268 100       498 if( !exists $atom->{hcount} ) {
316 207 100       545 next if !exists $normal_valence{$atom->{symbol}};
317 289 100       53537 my $degree = sum map { exists $bond_order{$_} ? $bond_order{$_} : 1 }
318 205 100       587 map { $graph->has_edge_attribute( $atom, $_, 'bond' )
  289         46776  
319             ? $graph->get_edge_attribute( $atom, $_, 'bond' )
320             : '-' }
321             $graph->neighbours( $atom );
322 205 100       9297 $degree = 0 unless $degree;
323 237         501 my( $valence ) = grep { $degree <= $_ }
324 205         261 @{$normal_valence{$atom->{symbol}}};
  205         482  
325 205 100       417 next if !defined $valence;
326 204         412 $atom->{hcount} = $valence - $degree;
327             }
328 265         721 for (1..$atom->{hcount}) {
329             my $hydrogen = { symbol => 'H',
330             class => 0,
331 315         1017 number => $self->{USER}{ATOMNO} };
332 315         962 $graph->add_edge( $atom, $hydrogen );
333 315         70419 $self->{USER}{ATOMNO} ++;
334 315 100       631 if( $atom->{first_of_chain} ) {
335 131         305 _unshift_chirality_neighbour( $atom, $hydrogen );
336             } else {
337 184         347 _push_chirality_neighbour( $atom, $hydrogen );
338             }
339             }
340 265         532 delete $atom->{hcount};
341              
342             # Unify the representation of chirality
343 265 100       583 if( is_chiral $atom ) {
344 23 100 66     268 if( $atom->{chirality} =~ /^@@?$/ &&
345             $graph->degree( $atom ) == 2 ) {
346 1         454 $atom->{chirality} =~ s/@+/'@AL' . length $&/e;
  1         6  
347             }
348              
349 23         13943 $atom->{chirality} =~ s/^\@TH1$/@/;
350 23         46 $atom->{chirality} =~ s/^\@TH2$/@@/;
351             }
352              
353             # Adjust chirality for centers having lone pairs
354 265 100 100     582 if( is_chiral $atom &&
      66        
      100        
      100        
355             $atom->{first_of_chain} &&
356             $atom->{chirality} =~ /^@@?$/ &&
357             $atom->{chirality_neighbours} &&
358 6         26 scalar @{$atom->{chirality_neighbours}} == 3 ) {
359 2 100       9 $atom->{chirality} = $atom->{chirality} eq '@' ? '@@' : '@';
360             }
361             }
362 817         1333 delete $atom->{first_of_chain};
363             }
364             }
365              
366 223         781 return @graphs;
367             }
368              
369             sub _add_ring_bond
370             {
371 133     133   301 my( $self, $atom, $ring_bond, $bond ) = @_;
372 133 100       396 if( $self->{USER}{RINGBONDS}{$ring_bond} ) {
373             $self->_merge_graphs( $self->{USER}{RINGBONDS}{$ring_bond}{atom}{index},
374 65         250 $atom->{index} );
375              
376 65 50 100     337 if( $bond && $self->{USER}{RINGBONDS}{$ring_bond}{bond} &&
      33        
      66        
377             (($bond !~ /^[\\\/]$/ &&
378             $bond ne $self->{USER}{RINGBONDS}{$ring_bond}{bond}) ||
379             ($bond eq '\\' &&
380             $self->{USER}{RINGBONDS}{$ring_bond}{bond} ne '/') ||
381             ($bond eq '/' &&
382             $self->{USER}{RINGBONDS}{$ring_bond}{bond} ne '\\')) ) {
383 1         59 die "$0: ring bond types for ring bond $ring_bond do not match.\n";
384             }
385 128         271 ( $bond ) = grep { defined }
386 64         163 ( $self->{USER}{RINGBONDS}{$ring_bond}{bond}, $bond );
387              
388 64 100 100     236 if( $bond && $bond =~ /^[\\\/]$/ &&
      100        
389             !defined $self->{USER}{RINGBONDS}{$ring_bond}{bond} ) {
390             # If cis/trans marker is not specified when cis/trans bond is
391             # seen first, it has to be inverted:
392 1         7 $bond = toggle_cistrans $bond;
393             }
394              
395 64         125 my $ring_atom = $self->{USER}{RINGBONDS}{$ring_bond}{atom};
396 64 100 100     261 if( !$bond && is_aromatic $ring_atom && is_aromatic $atom ) {
      100        
397 9         26 $bond = ':';
398             }
399 64 100 100     219 if( $bond && $bond ne '-' ) {
400 21         100 $atom->{graph}->set_edge_attribute( $ring_atom,
401             $atom,
402             'bond',
403             $bond );
404             } else {
405 43         131 $atom->{graph}->add_edge( $ring_atom, $atom );
406             }
407 64         14933 delete $self->{USER}{RINGBONDS}{$ring_bond};
408              
409 64 50 66     199 if( is_chiral $ring_atom && $ring_atom->{chirality_neighbours} ) {
410             my( $pos ) = grep { !ref $ring_atom->{chirality_neighbours}[$_] &&
411 12 100       68 $ring_atom->{chirality_neighbours}[$_] == $ring_bond }
412 3         8 0..$#{$ring_atom->{chirality_neighbours}};
  3         11  
413 3 50       13 $ring_atom->{chirality_neighbours}[$pos] = $atom if defined $pos;
414             }
415 64         175 _push_chirality_neighbour( $atom, $ring_atom );
416             } else {
417 68 100       267 $self->{USER}{RINGBONDS}{$ring_bond} =
418             { atom => $atom, $bond ? ( bond => $bond ) : () };
419              
420             # Record a placeholder for later addition of real chirality
421             # neighbour, which will be identified by the ring bond number
422 68         172 _push_chirality_neighbour( $atom, $ring_bond );
423             }
424             }
425              
426             sub _merge_graphs
427             {
428 285     285   546 my( $self, $index1, $index2 ) = @_;
429 285 100       577 return if $index1 == $index2;
430              
431 242         400 my $g1 = $self->{USER}{GRAPHS}[$index1];
432 242         341 my $g2 = $self->{USER}{GRAPHS}[$index2];
433              
434 242         620 for ($g2->vertices) {
435 611         5518 $_->{graph} = $g1;
436 611         857 $_->{index} = $index1;
437             }
438 242         503 $g1->add_vertices( $g2->vertices );
439              
440 242         28224 for ($g2->edges) {
441 375         55658 my $attributes = $g2->get_edge_attributes( @$_ );
442 375 100       71643 if( $attributes ) {
443 63         204 $g1->set_edge_attributes( @$_, $attributes );
444             } else {
445 312         691 $g1->add_edge( @$_ );
446             }
447             }
448              
449 242         23995 $self->{USER}{GRAPHS}[$index2] = undef;
450             }
451              
452             sub _push_chirality_neighbour
453             {
454 1269     1269   1999 my( $atom1, $atom2 ) = @_;
455 1269 100       2434 return unless is_chiral $atom1;
456 160         249 push @{$atom1->{chirality_neighbours}}, $atom2;
  160         418  
457             }
458              
459             sub _unshift_chirality_neighbour
460             {
461 366     366   617 my( $atom1, $atom2 ) = @_;
462 366 100       746 return unless is_chiral $atom1;
463 3         6 unshift @{$atom1->{chirality_neighbours}}, $atom2;
  3         11  
464             }
465              
466             1;