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   172 use warnings;
  21         34  
  21         719  
6 21     21   419 use 5.0100;
  21         72  
7              
8 21         2996 use Chemistry::OpenSMILES qw(
9             is_aromatic
10             is_chiral
11             %normal_valence
12             toggle_cistrans
13 21     21   5954 );
  21         49  
14 21     21   9287 use Graph::Undirected;
  21         803674  
  21         680  
15 21     21   187 use List::Util qw(any sum);
  21         42  
  21         43504  
16              
17             my %bond_order = (
18             '-' => 1,
19             '=' => 2,
20             '#' => 3,
21             '$' => 4,
22             );
23              
24             %}
25              
26             %%
27 234     234 0 194779  
28 234 50       613 # Rules section
29              
30             # The top-level 'filter' rule
31              
32             smiles: chain ;
33              
34             chain: atom
35             {
36 470     470   18638 my $g = Graph::Undirected->new( refvertexed => 1 );
37 470         158907 $g->add_vertex( $_[1] );
38 470         31290 push @{$_[0]->{USER}{GRAPHS}}, $g;
  470         1174  
39              
40 470         877 $_[1]->{graph} = $g;
41 470         566 $_[1]->{index} = @{$_[0]->{USER}{GRAPHS}}-1;
  470         1051  
42 470         728 $_[1]->{first_of_chain} = 1;
43              
44 470         1541 return { first => $_[1],
45             last => $_[1] };
46             }
47             | chain atom
48             {
49 299     299   11297 $_[2]->{graph} = $_[1]->{last}{graph};
50 299         518 $_[2]->{index} = $_[1]->{last}{index};
51              
52 299         1029 $_[2]->{graph}->add_edge( $_[1]->{last}, $_[2] );
53              
54 299 100 100     126630 if( is_aromatic $_[1]->{last} && is_aromatic $_[2] ) {
55             $_[2]->{graph}->set_edge_attribute( $_[1]->{last},
56 45         141 $_[2],
57             'bond',
58             ':' );
59             }
60              
61 299         10474 delete $_[2]->{first_of_chain};
62              
63 299         737 _push_chirality_neighbour( $_[1]->{last}, $_[2] );
64 299         712 _push_chirality_neighbour( $_[2], $_[1]->{last} );
65              
66 299         559 $_[1]->{last} = $_[2];
67              
68 299         560 return $_[1];
69             }
70             | chain bond atom
71             {
72 60     60   2270 $_[3]->{graph} = $_[1]->{last}{graph};
73 60         109 $_[3]->{index} = $_[1]->{last}{index};
74              
75 60 100       142 if( $_[2] ne '-' ) {
76             $_[3]->{graph}->set_edge_attribute( $_[1]->{last},
77 55         206 $_[3],
78             'bond',
79             $_[2] );
80             } else {
81 5         33 $_[3]->{graph}->add_edge( $_[1]->{last}, $_[3] );
82             }
83              
84 60         50221 delete $_[3]->{first_of_chain};
85              
86 60         186 _push_chirality_neighbour( $_[1]->{last}, $_[3] );
87 60         152 _push_chirality_neighbour( $_[3], $_[1]->{last} );
88              
89 60         115 $_[1]->{last} = $_[3];
90              
91 60         126 return $_[1];
92             }
93             | chain '.' atom
94             {
95 11     11   436 my $g = Graph::Undirected->new( refvertexed => 1 );
96 11         2241 $g->add_vertex( $_[3] );
97 11         674 push @{$_[0]->{USER}{GRAPHS}}, $g;
  11         26  
98              
99 11         24 $_[3]->{graph} = $g;
100 11         14 $_[3]->{index} = @{$_[0]->{USER}{GRAPHS}}-1;
  11         37  
101 11         21 $_[3]->{first_of_chain} = 1;
102              
103 11         39 return { first => $_[3],
104             last => $_[3] };
105             }
106             | chain '(' chain ')'
107             {
108 194 100   194   8202 if( $_[1]->{last}{index} != $_[3]->{first}{index} ) {
109             $_[0]->_merge_graphs( $_[1]->{last}{index},
110 184         519 $_[3]->{first}{index} );
111             }
112              
113 194         678 $_[1]->{last}{graph}->add_edge( $_[1]->{last}, $_[3]->{first} );
114              
115 194 100 100     56155 if( is_aromatic $_[1]->{last} && is_aromatic $_[3]->{first} ) {
116             $_[1]->{last}{graph}->set_edge_attribute( $_[1]->{last},
117             $_[3]->{first},
118 3         14 'bond',
119             ':' );
120             }
121              
122 194         1069 delete $_[3]->{first}{first_of_chain};
123              
124 194         491 _push_chirality_neighbour( $_[1]->{last}, $_[3]->{first} );
125 194         529 _unshift_chirality_neighbour( $_[3]->{first}, $_[1]->{last} );
126              
127 194         439 return $_[1];
128             }
129             | chain '(' bond chain ')'
130             {
131 41 100   41   1772 if( $_[1]->{last}{index} != $_[4]->{first}{index} ) {
132             $_[0]->_merge_graphs( $_[1]->{last}{index},
133 36         102 $_[4]->{first}{index} );
134             }
135              
136 41 100       110 if( $_[3] ne '-' ) {
137             $_[1]->{last}{graph}->set_edge_attribute( $_[1]->{last},
138             $_[4]->{first},
139 39         120 'bond',
140             $_[3] );
141             } else {
142             $_[1]->{last}{graph}->add_edge( $_[1]->{last},
143 2         7 $_[4]->{first} );
144             }
145              
146 41         23454 delete $_[4]->{first}{first_of_chain};
147              
148 41         122 _push_chirality_neighbour( $_[1]->{last}, $_[4]->{first} );
149 41         136 _unshift_chirality_neighbour( $_[4]->{first}, $_[1]->{last} );
150              
151 41         96 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   4299 $_[0]->_add_ring_bond( $_[1]->{last}, $_[2] );
162 107         242 return $_[1];
163             }
164             | chain bond ringbond
165             {
166 26     26   1010 $_[0]->_add_ring_bond( $_[1]->{last}, $_[3], $_[2] );
167 25         62 return $_[1];
168             }
169 234         12736 ;
170              
171             bond: '-' | '=' | '#' | '$' | ':' | '/' | '\\' ;
172              
173             %%
174              
175             # Footer section
176              
177             sub _Error
178             {
179 10     10   165 my( $self ) = @_;
180 10 50       32 close $self->{USER}{FILEIN} if $self->{USER}{FILEIN};
181              
182 10 100 100     12 if( ${$self->{TOKEN}} eq '' &&
  10         28  
183 7 100 100     35 grep { defined $_ && !ref $_ && $_ eq '(' }
184 7         13 map { $_->[1] } @{$self->{STACK}} ) {
  2         6  
185 1         49 die "$0: syntax error: missing closing parenthesis.\n";
186             }
187              
188 9 100       14 if( ${$self->{TOKEN}} eq ')' ) {
  9         17  
189 2         78 die "$0: syntax error: unbalanced parentheses.\n";
190             }
191              
192 7         28 my $msg = "$0: syntax error at position $self->{USER}{CHARNO}";
193 234 100       15324 if( $self->YYData->{INPUT} ) {
  7         14  
194 6         38 $self->YYData->{INPUT} =~ s/\n$//;
195 6         73 die "$msg: '" . $self->YYData->{INPUT} . "'.\n";
196             } else {
197 1         43 die "$msg.\n";
198             }
199             }
200              
201             sub _Lexer
202             {
203 1824     1824   54402 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     3674 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       12899 if( $self->YYData->{INPUT} =~ s/^(\s+)// ) {
214 0         0 $self->{USER}{CHARNO} += length $1;
215             }
216              
217 1824         13161 my $hcount_re = 'H[0-9]?';
218 1824 100       3699 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       3114 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   12058 my $atom = { %+, number => $self->{USER}{ATOMNO} };
  21         9748  
  21         40980  
  107         3271  
236 107         349 $self->{USER}{ATOMNO} ++;
237 107         268 $self->{USER}{CHARNO} += length $&;
238              
239 107 100       265 if( $atom->{charge} ) {
240 19         104 $atom->{charge} =~ s/^([-+])$/${1}1/;
241 19         76 $atom->{charge} =~ s/^([-+])\1$/${1}2/;
242 19         61 $atom->{charge} = int $atom->{charge};
243             }
244              
245 107 100       245 if( $atom->{hcount} ) {
246 17         65 $atom->{hcount} =~ s/^H//;
247 17 100       69 $atom->{hcount} = $atom->{hcount} ? int $atom->{hcount} : 1;
248             } else {
249 90         153 $atom->{hcount} = 0;
250             }
251              
252 107 100       219 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       243 $atom->{class} = exists $atom->{class} ? int $atom->{class} : 0;
258              
259 107         432 return ( 'atom', $atom );
260             }
261              
262             # Bracketless atoms
263 1717 100       18264 if( $self->YYData->{INPUT} =~ s/^(Br|Cl|[BCINOPSFbcnops*])// ) {
264             my $atom = { symbol => $1,
265             class => 0,
266 733         8373 number => $self->{USER}{ATOMNO} };
267 733         1588 $self->{USER}{ATOMNO} ++;
268 733         1415 $self->{USER}{CHARNO} += length $&;
269 733         3042 return ( 'atom', $atom );
270             }
271              
272             # Ring bonds
273 984 100 100     6810 if( $self->YYData->{INPUT} =~ s/^%([0-9]{2})// ||
274             $self->YYData->{INPUT} =~ s/^([0-9])// ) {
275 133         2016 $self->{USER}{CHARNO} += length $&;
276 133         652 return ( 'ringbond', int $1 );
277             }
278              
279 851         10579 my $char = substr( $self->YYData->{INPUT}, 0, 1 );
280 851 100       5527 if( $char ne '' ) {
281 624         1010 $self->YYData->{INPUT} = substr( $self->YYData->{INPUT}, 1 );
282             }
283 851         6275 $self->{USER}{CHARNO} ++;
284 851         2767 return( $char, $char );
285             }
286              
287             sub parse
288             {
289 236     236 0 3198 my( $self, $string, $options ) = @_;
290 236 100       590 $options = {} unless $options;
291              
292 236         594 $self->YYData->{INPUT} = $string;
293 236         1826 $self->{USER}{GRAPHS} = [];
294 236         437 $self->{USER}{RINGBONDS} = {};
295 236         389 $self->{USER}{ATOMNO} = 0;
296 236         365 $self->{USER}{CHARNO} = 0;
297 236         351 $self->{USER}{OPTIONS} = $options;
298             $self->YYParse( yylex => \&_Lexer,
299             yyerror => \&_Error,
300 236         1034 yydebug => $options->{debug} );
301              
302 225 100       19372 if( scalar keys %{$self->{USER}{RINGBONDS}} ) {
  225         870  
303             die "$0: unclosed ring bond(s) detected: " .
304 2         8 join( ', ', sort { $a <=> $b } keys %{$self->{USER}{RINGBONDS}} ) .
  1         40  
  2         46  
305             ".\n";
306             }
307              
308 223         356 my @graphs = grep { defined } @{$self->{USER}{GRAPHS}};
  470         1016  
  223         448  
309 223         466 for my $graph (@graphs) {
310 228         614 for my $atom (sort { $a->{number} <=> $b->{number} } $graph->vertices) {
  1091         4720  
311 820         2902 delete $atom->{graph};
312 820         989 delete $atom->{index};
313 820 100       1357 if( !$options->{raw} ) {
314             # Promote implicit hydrogen atoms into explicit ones
315 268 100       539 if( !exists $atom->{hcount} ) {
316 207 100       526 next if !exists $normal_valence{$atom->{symbol}};
317 289 100       52529 my $degree = sum map { exists $bond_order{$_} ? $bond_order{$_} : 1 }
318 205 100       565 map { $graph->has_edge_attribute( $atom, $_, 'bond' )
  289         45720  
319             ? $graph->get_edge_attribute( $atom, $_, 'bond' )
320             : '-' }
321             $graph->neighbours( $atom );
322 205 100       9379 $degree = 0 unless $degree;
323 237         500 my( $valence ) = grep { $degree <= $_ }
324 205         251 @{$normal_valence{$atom->{symbol}}};
  205         460  
325 205 100       452 next if !defined $valence;
326 204         404 $atom->{hcount} = $valence - $degree;
327             }
328 265         653 for (1..$atom->{hcount}) {
329             my $hydrogen = { symbol => 'H',
330             class => 0,
331 315         955 number => $self->{USER}{ATOMNO} };
332 315         846 $graph->add_edge( $atom, $hydrogen );
333 315         69689 $self->{USER}{ATOMNO} ++;
334 315 100       654 if( $atom->{first_of_chain} ) {
335 131         278 _unshift_chirality_neighbour( $atom, $hydrogen );
336             } else {
337 184         373 _push_chirality_neighbour( $atom, $hydrogen );
338             }
339             }
340 265         433 delete $atom->{hcount};
341              
342             # Unify the representation of chirality
343 265 100       565 if( is_chiral $atom ) {
344 23 100 66     293 if( $atom->{chirality} =~ /^@@?$/ &&
345             $graph->degree( $atom ) == 2 ) {
346 1         446 $atom->{chirality} =~ s/@+/'@AL' . length $&/e;
  1         5  
347             }
348              
349 23         14084 $atom->{chirality} =~ s/^\@TH1$/@/;
350 23         51 $atom->{chirality} =~ s/^\@TH2$/@@/;
351             }
352              
353             # Adjust chirality for centers having lone pairs
354 265 100 100     566 if( is_chiral $atom &&
      66        
      100        
      100        
355             $atom->{first_of_chain} &&
356             $atom->{chirality} =~ /^@@?$/ &&
357             $atom->{chirality_neighbours} &&
358 6         23 scalar @{$atom->{chirality_neighbours}} == 3 ) {
359 2 100       15 $atom->{chirality} = $atom->{chirality} eq '@' ? '@@' : '@';
360             }
361             }
362 817         1400 delete $atom->{first_of_chain};
363             }
364             }
365              
366 223         835 return @graphs;
367             }
368              
369             sub _add_ring_bond
370             {
371 133     133   305 my( $self, $atom, $ring_bond, $bond ) = @_;
372 133 100       387 if( $self->{USER}{RINGBONDS}{$ring_bond} ) {
373             $self->_merge_graphs( $self->{USER}{RINGBONDS}{$ring_bond}{atom}{index},
374 65         274 $atom->{index} );
375              
376 65 50 100     371 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         47 die "$0: ring bond types for ring bond $ring_bond do not match.\n";
384             }
385 128         272 ( $bond ) = grep { defined }
386 64         178 ( $self->{USER}{RINGBONDS}{$ring_bond}{bond}, $bond );
387              
388 64 100 100     260 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         4 $bond = toggle_cistrans $bond;
393             }
394              
395 64         127 my $ring_atom = $self->{USER}{RINGBONDS}{$ring_bond}{atom};
396 64 100 100     221 if( !$bond && is_aromatic $ring_atom && is_aromatic $atom ) {
      100        
397 9         19 $bond = ':';
398             }
399 64 100 100     208 if( $bond && $bond ne '-' ) {
400 21         80 $atom->{graph}->set_edge_attribute( $ring_atom,
401             $atom,
402             'bond',
403             $bond );
404             } else {
405 43         124 $atom->{graph}->add_edge( $ring_atom, $atom );
406             }
407 64         14603 delete $self->{USER}{RINGBONDS}{$ring_bond};
408              
409 64 50 66     179 if( is_chiral $ring_atom && $ring_atom->{chirality_neighbours} ) {
410             my( $pos ) = grep { !ref $ring_atom->{chirality_neighbours}[$_] &&
411 12 100       46 $ring_atom->{chirality_neighbours}[$_] == $ring_bond }
412 3         7 0..$#{$ring_atom->{chirality_neighbours}};
  3         8  
413 3 50       13 $ring_atom->{chirality_neighbours}[$pos] = $atom if defined $pos;
414             }
415 64         183 _push_chirality_neighbour( $atom, $ring_atom );
416             } else {
417 68 100       274 $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         230 _push_chirality_neighbour( $atom, $ring_bond );
423             }
424             }
425              
426             sub _merge_graphs
427             {
428 285     285   543 my( $self, $index1, $index2 ) = @_;
429 285 100       548 return if $index1 == $index2;
430              
431 242         409 my $g1 = $self->{USER}{GRAPHS}[$index1];
432 242         342 my $g2 = $self->{USER}{GRAPHS}[$index2];
433              
434 242         670 for ($g2->vertices) {
435 611         6132 $_->{graph} = $g1;
436 611         839 $_->{index} = $index1;
437             }
438 242         550 $g1->add_vertices( $g2->vertices );
439              
440 242         28524 for ($g2->edges) {
441 375         57729 my $attributes = $g2->get_edge_attributes( @$_ );
442 375 100       75210 if( $attributes ) {
443 63         229 $g1->set_edge_attributes( @$_, $attributes );
444             } else {
445 312         688 $g1->add_edge( @$_ );
446             }
447             }
448              
449 242         21780 $self->{USER}{GRAPHS}[$index2] = undef;
450             }
451              
452             sub _push_chirality_neighbour
453             {
454 1269     1269   2020 my( $atom1, $atom2 ) = @_;
455 1269 100       2432 return unless is_chiral $atom1;
456 160         258 push @{$atom1->{chirality_neighbours}}, $atom2;
  160         417  
457             }
458              
459             sub _unshift_chirality_neighbour
460             {
461 366     366   671 my( $atom1, $atom2 ) = @_;
462 366 100       717 return unless is_chiral $atom1;
463 3         8 unshift @{$atom1->{chirality_neighbours}}, $atom2;
  3         13  
464             }
465              
466             1;