File Coverage

lib/Chemistry/OpenSMILES/Parser.yp
Criterion Covered Total %
statement 231 235 98.3
branch 94 102 92.1
condition 56 63 88.8
subroutine 22 22 100.0
pod 0 2 0.0
total 403 424 95.0


line stmt bran cond sub pod time code
1             # Header section
2              
3             %{
4              
5 23     23   202 use warnings;
  23         54  
  23         807  
6 23     23   482 use 5.0100;
  23         82  
7              
8 23         3456 use Chemistry::OpenSMILES qw(
9             %bond_symbol_to_order
10             is_aromatic
11             is_chiral
12             %normal_valence
13             toggle_cistrans
14 23     23   6148 );
  23         55  
15 23     23   10505 use Graph::Undirected;
  23         903817  
  23         792  
16 23     23   208 use List::Util qw( any sum );
  23         50  
  23         46983  
17              
18             %}
19              
20             %%
21 235     235 0 199933  
22 235 50       641 # Rules section
23              
24             # The top-level 'filter' rule
25              
26             smiles: chain ;
27              
28             chain: atom
29             {
30 474     474   19013 my $g = Graph::Undirected->new( refvertexed => 1 );
31 474         163827 $g->add_vertex( $_[1] );
32 474         31605 push @{$_[0]->{USER}{GRAPHS}}, $g;
  474         1201  
33              
34 474         900 $_[1]->{graph} = $g;
35 474         564 $_[1]->{index} = @{$_[0]->{USER}{GRAPHS}}-1;
  474         1024  
36 474         747 $_[1]->{first_of_chain} = 1;
37              
38 474         1515 return { first => $_[1],
39             last => $_[1] };
40             }
41             | chain atom
42             {
43 307     307   11631 $_[2]->{graph} = $_[1]->{last}{graph};
44 307         518 $_[2]->{index} = $_[1]->{last}{index};
45              
46 307         981 $_[2]->{graph}->add_edge( $_[1]->{last}, $_[2] );
47              
48 307 100 100     139976 if( is_aromatic $_[1]->{last} && is_aromatic $_[2] ) {
49             $_[2]->{graph}->set_edge_attribute( $_[1]->{last},
50 45         137 $_[2],
51             'bond',
52             ':' );
53             }
54              
55 307         10452 delete $_[2]->{first_of_chain};
56              
57 307         784 _push_chirality_neighbour( $_[1]->{last}, $_[2] );
58 307         722 _push_chirality_neighbour( $_[2], $_[1]->{last} );
59              
60 307         527 $_[1]->{last} = $_[2];
61              
62 307         568 return $_[1];
63             }
64             | chain bond atom
65             {
66 62     62   2367 $_[3]->{graph} = $_[1]->{last}{graph};
67 62         118 $_[3]->{index} = $_[1]->{last}{index};
68              
69 62 100       135 if( $_[2] ne '-' ) {
70             $_[3]->{graph}->set_edge_attribute( $_[1]->{last},
71 57         232 $_[3],
72             'bond',
73             $_[2] );
74             } else {
75 5         27 $_[3]->{graph}->add_edge( $_[1]->{last}, $_[3] );
76             }
77              
78 62         51455 delete $_[3]->{first_of_chain};
79              
80 62         200 _push_chirality_neighbour( $_[1]->{last}, $_[3] );
81 62         157 _push_chirality_neighbour( $_[3], $_[1]->{last} );
82              
83 62         131 $_[1]->{last} = $_[3];
84              
85 62         141 return $_[1];
86             }
87             | chain '.' atom
88             {
89 11     11   449 my $g = Graph::Undirected->new( refvertexed => 1 );
90 11         2185 $g->add_vertex( $_[3] );
91 11         658 push @{$_[0]->{USER}{GRAPHS}}, $g;
  11         32  
92              
93 11         26 $_[3]->{graph} = $g;
94 11         17 $_[3]->{index} = @{$_[0]->{USER}{GRAPHS}}-1;
  11         23  
95 11         20 $_[3]->{first_of_chain} = 1;
96              
97 11         39 return { first => $_[3],
98             last => $_[3] };
99             }
100             | chain '(' chain ')'
101             {
102 195 100   195   8666 if( $_[1]->{last}{index} != $_[3]->{first}{index} ) {
103             $_[0]->_merge_graphs( $_[1]->{last}{index},
104 184         515 $_[3]->{first}{index} );
105             }
106              
107 195         759 $_[1]->{last}{graph}->add_edge( $_[1]->{last}, $_[3]->{first} );
108              
109 195 100 100     57424 if( is_aromatic $_[1]->{last} && is_aromatic $_[3]->{first} ) {
110             $_[1]->{last}{graph}->set_edge_attribute( $_[1]->{last},
111             $_[3]->{first},
112 3         12 'bond',
113             ':' );
114             }
115              
116 195         1086 delete $_[3]->{first}{first_of_chain};
117              
118 195         524 _push_chirality_neighbour( $_[1]->{last}, $_[3]->{first} );
119 195         540 _unshift_chirality_neighbour( $_[3]->{first}, $_[1]->{last} );
120              
121 195         486 return $_[1];
122             }
123             | chain '(' bond chain ')'
124             {
125 43 100   43   1891 if( $_[1]->{last}{index} != $_[4]->{first}{index} ) {
126             $_[0]->_merge_graphs( $_[1]->{last}{index},
127 38         117 $_[4]->{first}{index} );
128             }
129              
130 43 100       130 if( $_[3] ne '-' ) {
131             $_[1]->{last}{graph}->set_edge_attribute( $_[1]->{last},
132             $_[4]->{first},
133 41         148 'bond',
134             $_[3] );
135             } else {
136             $_[1]->{last}{graph}->add_edge( $_[1]->{last},
137 2         7 $_[4]->{first} );
138             }
139              
140 43         24294 delete $_[4]->{first}{first_of_chain};
141              
142 43         130 _push_chirality_neighbour( $_[1]->{last}, $_[4]->{first} );
143 43         134 _unshift_chirality_neighbour( $_[4]->{first}, $_[1]->{last} );
144              
145 43         141 return $_[1];
146             }
147             | chain '(' '.' chain ')'
148              
149             # According to the specification of OpenSMILES, ring bonds are
150             # allowed only before the branch enumeration. However, I think this
151             # is too strict.
152              
153             | chain ringbond
154             {
155 111     111   4338 $_[0]->_add_ring_bond( $_[1]->{last}, $_[2] );
156 111         238 return $_[1];
157             }
158             | chain bond ringbond
159             {
160 26     26   1037 $_[0]->_add_ring_bond( $_[1]->{last}, $_[3], $_[2] );
161 25         55 return $_[1];
162             }
163 235         13206 ;
164              
165             bond: '-' | '=' | '#' | '$' | ':' | '/' | '\\' ;
166              
167             %%
168              
169             # Footer section
170              
171             sub _Error
172             {
173 10     10   173 my( $self ) = @_;
174 10 50       24 close $self->{USER}{FILEIN} if $self->{USER}{FILEIN};
175              
176 10 100 100     11 if( ${$self->{TOKEN}} eq '' &&
  10         27  
177 7 100 100     33 grep { defined $_ && !ref $_ && $_ eq '(' }
178 7         14 map { $_->[1] } @{$self->{STACK}} ) {
  2         16  
179 1         52 die "$0: syntax error: missing closing parenthesis.\n";
180             }
181              
182 9 100       11 if( ${$self->{TOKEN}} eq ')' ) {
  9         20  
183 2         85 die "$0: syntax error: unbalanced parentheses.\n";
184             }
185              
186 7         27 my $msg = "$0: syntax error at position $self->{USER}{CHARNO}";
187 235 100       15258 if( $self->YYData->{INPUT} ) {
  7         19  
188 6         40 $self->YYData->{INPUT} =~ s/\n$//;
189 6         50 die "$msg: '" . $self->YYData->{INPUT} . "'.\n";
190             } else {
191 1         46 die "$msg.\n";
192             }
193             }
194              
195             sub _Lexer
196             {
197 1853     1853   55577 my( $self ) = @_;
198              
199             # If the line is empty and the input is originating from the file,
200             # another line is read.
201 1853 50 66     3775 if( !$self->YYData->{INPUT} && $self->{USER}{FILEIN} ) {
202 0         0 my $filein = $self->{USER}{FILEIN};
203 0         0 $self->YYData->{INPUT} = <$filein>;
204 0         0 $self->{USER}{CHARNO} = 0;
205             }
206              
207 1853 50       13111 if( $self->YYData->{INPUT} =~ s/^(\s+)// ) {
208 0         0 $self->{USER}{CHARNO} += length $1;
209             }
210              
211 1853         13555 my $hcount_re = 'H[0-9]?';
212 1853 100       3717 if( defined $self->{USER}{OPTIONS}{max_hydrogen_count_digits} ) {
213             $hcount_re = sprintf 'H[0-9]{0,%d}',
214 2         12 $self->{USER}{OPTIONS}{max_hydrogen_count_digits};
215             }
216              
217             # Bracket atoms
218 1853 100       3268 if( $self->YYData->{INPUT} =~ s/^\[ (?[0-9]+)?
219             (?[A-Za-z][a-z]?|\*)
220             (?@(
221             (TH|AL)[12] |
222             SP [123] |
223             (TB|OH)[0-9]{1,2} |
224             @?
225             ))?
226             (? $hcount_re)?
227             (?--|\+\+|[-+][0-9]{0,2})?
228             (:(?[0-9]+))? \]//x ) {
229 23     23   12871 my $atom = { %+, number => $self->{USER}{ATOMNO} };
  23         10443  
  23         46057  
  107         3167  
230 107         346 $self->{USER}{ATOMNO} ++;
231 107         276 $self->{USER}{CHARNO} += length $&;
232              
233 107 100       271 if( $atom->{charge} ) {
234 19         109 $atom->{charge} =~ s/^([-+])$/${1}1/;
235 19         77 $atom->{charge} =~ s/^([-+])\1$/${1}2/;
236 19         60 $atom->{charge} = int $atom->{charge};
237             }
238              
239 107 100       593 if( $atom->{hcount} ) {
240 17         71 $atom->{hcount} =~ s/^H//;
241 17 100       62 $atom->{hcount} = $atom->{hcount} ? int $atom->{hcount} : 1;
242             } else {
243 90         161 $atom->{hcount} = 0;
244             }
245              
246 107 100       219 if( $atom->{isotope} ) {
247 6         15 $atom->{isotope} = int $atom->{isotope};
248             }
249              
250             # Atom class is an arbitrary number, 0 by default
251 107 50       250 $atom->{class} = exists $atom->{class} ? int $atom->{class} : 0;
252              
253 107         451 return ( 'atom', $atom );
254             }
255              
256             # Bracketless atoms
257 1746 100       18403 if( $self->YYData->{INPUT} =~ s/^(Br|Cl|[BCINOPSFbcnops*])// ) {
258             my $atom = { symbol => $1,
259             class => 0,
260 747         8721 number => $self->{USER}{ATOMNO} };
261 747         1249 $self->{USER}{ATOMNO} ++;
262 747         1416 $self->{USER}{CHARNO} += length $&;
263 747         2729 return ( 'atom', $atom );
264             }
265              
266             # Ring bonds
267 999 100 100     7017 if( $self->YYData->{INPUT} =~ s/^%([0-9]{2})// ||
268             $self->YYData->{INPUT} =~ s/^([0-9])// ) {
269 137         2011 $self->{USER}{CHARNO} += length $&;
270 137         689 return ( 'ringbond', int $1 );
271             }
272              
273 862         10770 my $char = substr( $self->YYData->{INPUT}, 0, 1 );
274 862 100       5492 if( $char ne '' ) {
275 634         1090 $self->YYData->{INPUT} = substr( $self->YYData->{INPUT}, 1 );
276             }
277 862         6040 $self->{USER}{CHARNO} ++;
278 862         2954 return( $char, $char );
279             }
280              
281             sub parse
282             {
283 237     237 0 3545 my( $self, $string, $options ) = @_;
284 237 100       570 $options = {} unless $options;
285              
286 237         652 $self->YYData->{INPUT} = $string;
287 237         1958 $self->{USER}{GRAPHS} = [];
288 237         435 $self->{USER}{RINGBONDS} = {};
289 237         424 $self->{USER}{ATOMNO} = 0;
290 237         379 $self->{USER}{CHARNO} = 0;
291 237         369 $self->{USER}{OPTIONS} = $options;
292             $self->YYParse( yylex => \&_Lexer,
293             yyerror => \&_Error,
294 237         1064 yydebug => $options->{debug} );
295              
296 226 100       19344 if( scalar keys %{$self->{USER}{RINGBONDS}} ) {
  226         1242  
297             die "$0: unclosed ring bond(s) detected: " .
298 2         8 join( ', ', sort { $a <=> $b } keys %{$self->{USER}{RINGBONDS}} ) .
  1         43  
  2         47  
299             ".\n";
300             }
301              
302 224         346 my @graphs = grep { defined } @{$self->{USER}{GRAPHS}};
  474         964  
  224         479  
303 224         458 for my $graph (@graphs) {
304 229         691 for my $atom (sort { $a->{number} <=> $b->{number} } $graph->vertices) {
  1120         4864  
305 834         3161 delete $atom->{graph};
306 834         1017 delete $atom->{index};
307 834 100       1405 if( !$options->{raw} ) {
308             # Promote implicit hydrogen atoms into explicit ones
309 282 100       524 if( !exists $atom->{hcount} ) {
310 221 100       525 next if !exists $normal_valence{$atom->{symbol}};
311 319 100 100     57992 my $degree = sum map { $_ ne ':' && exists $bond_symbol_to_order{$_} ? $bond_symbol_to_order{$_} : 1 }
312 219 100       622 map { $graph->has_edge_attribute( $atom, $_, 'bond' )
  319         51233  
313             ? $graph->get_edge_attribute( $atom, $_, 'bond' )
314             : '-' }
315             $graph->neighbours( $atom );
316 219 100       9383 $degree = 0 unless $degree;
317 255         563 my( $valence ) = grep { $degree <= $_ }
318 219         274 @{$normal_valence{$atom->{symbol}}};
  219         541  
319 219 100       433 next if !defined $valence;
320 218         430 $atom->{hcount} = $valence - $degree;
321             }
322 279         730 for (1..$atom->{hcount}) {
323             my $hydrogen = { symbol => 'H',
324             class => 0,
325 325         1087 number => $self->{USER}{ATOMNO} };
326 325         944 $graph->add_edge( $atom, $hydrogen );
327 325         71526 $self->{USER}{ATOMNO} ++;
328 325 100       664 if( $atom->{first_of_chain} ) {
329 134         324 _unshift_chirality_neighbour( $atom, $hydrogen );
330             } else {
331 191         380 _push_chirality_neighbour( $atom, $hydrogen );
332             }
333             }
334 279         469 delete $atom->{hcount};
335              
336             # Unify the representation of chirality
337 279 100       586 if( is_chiral $atom ) {
338 23 100 66     244 if( $atom->{chirality} =~ /^@@?$/ &&
339             $graph->degree( $atom ) == 2 ) {
340 1         464 $atom->{chirality} =~ s/@+/'@AL' . length $&/e;
  1         7  
341             }
342              
343 23         13930 $atom->{chirality} =~ s/^\@TH1$/@/;
344 23         49 $atom->{chirality} =~ s/^\@TH2$/@@/;
345             }
346              
347             # Adjust chirality for centers having lone pairs
348 279 100 100     575 if( is_chiral $atom &&
      66        
      100        
      100        
349             $atom->{first_of_chain} &&
350             $atom->{chirality} =~ /^@@?$/ &&
351             $atom->{chirality_neighbours} &&
352 6         22 scalar @{$atom->{chirality_neighbours}} == 3 ) {
353 2 100       35 $atom->{chirality} = $atom->{chirality} eq '@' ? '@@' : '@';
354             }
355             }
356 831         1386 delete $atom->{first_of_chain};
357             }
358             }
359              
360 224         863 return @graphs;
361             }
362              
363             sub _add_ring_bond
364             {
365 137     137   337 my( $self, $atom, $ring_bond, $bond ) = @_;
366 137 100       365 if( $self->{USER}{RINGBONDS}{$ring_bond} ) {
367             $self->_merge_graphs( $self->{USER}{RINGBONDS}{$ring_bond}{atom}{index},
368 67         329 $atom->{index} );
369              
370 67 50 100     443 if( $bond && $self->{USER}{RINGBONDS}{$ring_bond}{bond} &&
      33        
      66        
371             (($bond !~ /^[\\\/]$/ &&
372             $bond ne $self->{USER}{RINGBONDS}{$ring_bond}{bond}) ||
373             ($bond eq '\\' &&
374             $self->{USER}{RINGBONDS}{$ring_bond}{bond} ne '/') ||
375             ($bond eq '/' &&
376             $self->{USER}{RINGBONDS}{$ring_bond}{bond} ne '\\')) ) {
377 1         44 die "$0: ring bond types for ring bond $ring_bond do not match.\n";
378             }
379 132         284 ( $bond ) = grep { defined }
380 66         174 ( $self->{USER}{RINGBONDS}{$ring_bond}{bond}, $bond );
381              
382 66 100 100     234 if( $bond && $bond =~ /^[\\\/]$/ &&
      100        
383             !defined $self->{USER}{RINGBONDS}{$ring_bond}{bond} ) {
384             # If cis/trans marker is not specified when cis/trans bond is
385             # seen first, it has to be inverted:
386 1         5 $bond = toggle_cistrans $bond;
387             }
388              
389 66         140 my $ring_atom = $self->{USER}{RINGBONDS}{$ring_bond}{atom};
390 66 100 100     223 if( !$bond && is_aromatic $ring_atom && is_aromatic $atom ) {
      100        
391 9         27 $bond = ':';
392             }
393 66 100 100     237 if( $bond && $bond ne '-' ) {
394 21         84 $atom->{graph}->set_edge_attribute( $ring_atom,
395             $atom,
396             'bond',
397             $bond );
398             } else {
399 45         127 $atom->{graph}->add_edge( $ring_atom, $atom );
400             }
401 66         14979 delete $self->{USER}{RINGBONDS}{$ring_bond};
402              
403 66 50 66     206 if( is_chiral $ring_atom && $ring_atom->{chirality_neighbours} ) {
404             my( $pos ) = grep { !ref $ring_atom->{chirality_neighbours}[$_] &&
405 12 100       42 $ring_atom->{chirality_neighbours}[$_] == $ring_bond }
406 3         7 0..$#{$ring_atom->{chirality_neighbours}};
  3         9  
407 3 50       19 $ring_atom->{chirality_neighbours}[$pos] = $atom if defined $pos;
408             }
409 66         189 _push_chirality_neighbour( $atom, $ring_atom );
410             } else {
411 70 100       259 $self->{USER}{RINGBONDS}{$ring_bond} =
412             { atom => $atom, $bond ? ( bond => $bond ) : () };
413              
414             # Record a placeholder for later addition of real chirality
415             # neighbour, which will be identified by the ring bond number
416 70         172 _push_chirality_neighbour( $atom, $ring_bond );
417             }
418             }
419              
420             sub _merge_graphs
421             {
422 289     289   539 my( $self, $index1, $index2 ) = @_;
423 289 100       579 return if $index1 == $index2;
424              
425 245         413 my $g1 = $self->{USER}{GRAPHS}[$index1];
426 245         342 my $g2 = $self->{USER}{GRAPHS}[$index2];
427              
428 245         618 for ($g2->vertices) {
429 616         5691 $_->{graph} = $g1;
430 616         840 $_->{index} = $index1;
431             }
432 245         607 $g1->add_vertices( $g2->vertices );
433              
434 245         28627 for ($g2->edges) {
435 377         56205 my $attributes = $g2->get_edge_attributes( @$_ );
436 377 100       71849 if( $attributes ) {
437 64         218 $g1->set_edge_attributes( @$_, $attributes );
438             } else {
439 313         711 $g1->add_edge( @$_ );
440             }
441             }
442              
443 245         22160 $self->{USER}{GRAPHS}[$index2] = undef;
444             }
445              
446             sub _push_chirality_neighbour
447             {
448 1303     1303   2062 my( $atom1, $atom2 ) = @_;
449 1303 100       2587 return unless is_chiral $atom1;
450 160         248 push @{$atom1->{chirality_neighbours}}, $atom2;
  160         406  
451             }
452              
453             sub _unshift_chirality_neighbour
454             {
455 372     372   657 my( $atom1, $atom2 ) = @_;
456 372 100       762 return unless is_chiral $atom1;
457 3         11 unshift @{$atom1->{chirality_neighbours}}, $atom2;
  3         16  
458             }
459              
460             1;