File Coverage

blib/lib/Chemistry/OpenSMILES/Parser.pm
Criterion Covered Total %
statement 9 9 100.0
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 12 12 100.0


line stmt bran cond sub pod time code
1             ####################################################################
2             #
3             # This file was generated using Parse::Yapp version 1.21.
4             #
5             # Don't edit this file, use source file instead.
6             #
7             # ANY CHANGE MADE HERE WILL BE LOST !
8             #
9             ####################################################################
10             package Chemistry::OpenSMILES::Parser;
11 44     44   175429 use vars qw ( @ISA );
  44         105  
  44         4875  
12 44     44   396 use strict;
  44         102  
  44         6039  
13              
14             @ISA= qw ( Parse::Yapp::Driver );
15 44     44   27537 use Parse::Yapp::Driver;
  44         151994  
  44         18947  
16              
17             #line 3 "lib/Chemistry/OpenSMILES/Parser.yp"
18              
19              
20             =head1 NAME
21              
22             Chemistry::OpenSMILES::Parser - OpenSMILES format reader
23              
24             =head1 SYNOPSIS
25              
26             use Chemistry::OpenSMILES::Parser;
27              
28             my $parser = Chemistry::OpenSMILES::Parser->new;
29             my @moieties = $parser->parse( 'C#C.c1ccccc1' );
30              
31             $\ = "\n";
32             for my $moiety (@moieties) {
33             # $moiety is a Graph::Undirected object
34             print scalar $moiety->vertices;
35             print scalar $moiety->edges;
36             }
37              
38             =head1 DESCRIPTION
39              
40             C is OpenSMILES format reader.
41              
42             =cut
43              
44             use warnings;
45             use 5.0100;
46              
47             use Chemistry::Elements;
48             use Chemistry::OpenSMILES qw(
49             %bond_symbol_to_order
50             %normal_valence
51             is_aromatic
52             is_chiral
53             toggle_cistrans
54             );
55             use Graph::Undirected;
56             use List::Util qw( any first sum0 );
57              
58             =head1 METHODS
59              
60             =head2 C
61              
62             Parses a SMILES string and returns an array of disconnected molecular entities as separate instances of L.
63             Their interpretation is described in detail in L.
64              
65             =head3 Options
66              
67             C accepts the following options for key-value pairs in an anonymous hash for its second parameter:
68              
69             =over
70              
71             =item C
72              
73             In OpenSMILES specification the number of attached hydrogen atoms for atoms in square brackets is limited to 9.
74             IUPAC SMILES+ has increased this number to 99.
75             With the value of C the parser can be instructed to allow other than 1 digit for attached hydrogen count.
76              
77             =item C
78              
79             With C set to anything evaluating to true, the parser will not convert neither implicit nor explicit hydrogen atoms in square brackets to atom hashes of their own.
80             Moreover, it will not attempt to unify the representations of chirality.
81             It should be noted, though, that many of subroutines of Chemistry::OpenSMILES expect non-raw data structures, thus processing raw output may produce distorted results.
82             In particular, C calls from L have to be instructed to expect raw data structure:
83              
84             write_SMILES( \@moieties, { raw => 1 } );
85              
86             This option is now deprecated and may be removed in upcoming versions.
87              
88             =item C
89              
90             With C set to anything evaluating to true, the parser will emit warnings about bot bonds used in parentheses.
91             This could be used to detect the unnecessary use of dot bonds.
92              
93             =back
94              
95             =head1 CAVEATS
96              
97             Deprecated charge notations (C<--> and C<++>) are supported.
98              
99             OpenSMILES specification mandates a strict order of ring bonds and branches:
100              
101             branched_atom ::= atom ringbond* branch*
102              
103             Chemistry::OpenSMILES::Parser supports both the mandated, and inverted
104             structure, where ring bonds follow branch descriptions.
105              
106             Whitespace is not supported yet. SMILES descriptors must be cleaned of
107             it before attempting reading with Chemistry::OpenSMILES::Parser.
108              
109             The derivation of implicit hydrogen counts for aromatic atoms is not
110             unambiguously defined in the OpenSMILES specification. Thus only
111             aromatic carbon is accounted for as if having valence of 3.
112              
113             Chiral atoms with three neighbours are interpreted as having a lone pair of electrons one of its chiral neighbours.
114             The lone pair is always understood as being the second in the order of neighbour enumeration, except when the atom with the lone pair starts a chain.
115             In that case lone pair is the first.
116              
117             =cut
118              
119              
120              
121             sub new {
122             my($class)=shift;
123             ref($class)
124             and $class=ref($class);
125              
126             my($self)=$class->SUPER::new( yyversion => '1.21',
127             yystates =>
128             [
129             {#State 0
130             ACTIONS => {
131             'atom' => 2
132             },
133             GOTOS => {
134             'smiles' => 3,
135             'chain' => 1
136             }
137             },
138             {#State 1
139             ACTIONS => {
140             ":" => 6,
141             "=" => 5,
142             "/" => 7,
143             'ringbond' => 8,
144             "\\" => 10,
145             "(" => 9,
146             "\$" => 11,
147             'atom' => 12,
148             "#" => 14,
149             "-" => 15,
150             "." => 13
151             },
152             DEFAULT => -1,
153             GOTOS => {
154             'bond' => 4
155             }
156             },
157             {#State 2
158             DEFAULT => -2
159             },
160             {#State 3
161             ACTIONS => {
162             '' => 16
163             }
164             },
165             {#State 4
166             ACTIONS => {
167             'ringbond' => 17,
168             'atom' => 18
169             }
170             },
171             {#State 5
172             DEFAULT => -12
173             },
174             {#State 6
175             DEFAULT => -15
176             },
177             {#State 7
178             DEFAULT => -16
179             },
180             {#State 8
181             DEFAULT => -9
182             },
183             {#State 9
184             ACTIONS => {
185             "." => 19,
186             "-" => 15,
187             "/" => 7,
188             "#" => 14,
189             ":" => 6,
190             "=" => 5,
191             'atom' => 2,
192             "\$" => 11,
193             "\\" => 10
194             },
195             GOTOS => {
196             'bond' => 21,
197             'chain' => 20
198             }
199             },
200             {#State 10
201             DEFAULT => -17
202             },
203             {#State 11
204             DEFAULT => -14
205             },
206             {#State 12
207             DEFAULT => -3
208             },
209             {#State 13
210             ACTIONS => {
211             'atom' => 22
212             }
213             },
214             {#State 14
215             DEFAULT => -13
216             },
217             {#State 15
218             DEFAULT => -11
219             },
220             {#State 16
221             DEFAULT => 0
222             },
223             {#State 17
224             DEFAULT => -10
225             },
226             {#State 18
227             DEFAULT => -4
228             },
229             {#State 19
230             ACTIONS => {
231             'atom' => 2
232             },
233             GOTOS => {
234             'chain' => 23
235             }
236             },
237             {#State 20
238             ACTIONS => {
239             'atom' => 12,
240             "." => 13,
241             "-" => 15,
242             "#" => 14,
243             "(" => 9,
244             "\\" => 10,
245             "\$" => 11,
246             "=" => 5,
247             ":" => 6,
248             'ringbond' => 8,
249             "/" => 7,
250             ")" => 24
251             },
252             GOTOS => {
253             'bond' => 4
254             }
255             },
256             {#State 21
257             ACTIONS => {
258             'atom' => 2
259             },
260             GOTOS => {
261             'chain' => 25
262             }
263             },
264             {#State 22
265             DEFAULT => -5
266             },
267             {#State 23
268             ACTIONS => {
269             ")" => 26,
270             "\$" => 11,
271             "\\" => 10,
272             "(" => 9,
273             'ringbond' => 8,
274             "-" => 15,
275             "#" => 14,
276             "/" => 7,
277             "." => 13,
278             "=" => 5,
279             ":" => 6,
280             'atom' => 12
281             },
282             GOTOS => {
283             'bond' => 4
284             }
285             },
286             {#State 24
287             DEFAULT => -6
288             },
289             {#State 25
290             ACTIONS => {
291             'atom' => 12,
292             "-" => 15,
293             "#" => 14,
294             "." => 13,
295             "\\" => 10,
296             "(" => 9,
297             "\$" => 11,
298             ":" => 6,
299             "=" => 5,
300             "/" => 7,
301             'ringbond' => 8,
302             ")" => 27
303             },
304             GOTOS => {
305             'bond' => 4
306             }
307             },
308             {#State 26
309             DEFAULT => -8
310             },
311             {#State 27
312             DEFAULT => -7
313             }
314             ],
315             yyrules =>
316             [
317             [#Rule 0
318             '$start', 2, undef
319             ],
320             [#Rule 1
321             'smiles', 1, undef
322             ],
323             [#Rule 2
324             'chain', 1,
325             sub
326             #line 115 "lib/Chemistry/OpenSMILES/Parser.yp"
327             {
328             my $g = Graph::Undirected->new( refvertexed => 1 );
329             $g->add_vertex( $_[1] );
330             push @{$_[0]->{USER}{GRAPHS}}, $g;
331              
332             $_[1]->{graph} = $g;
333             $_[1]->{index} = @{$_[0]->{USER}{GRAPHS}}-1;
334              
335             return { first => $_[1],
336             last => $_[1] };
337             }
338             ],
339             [#Rule 3
340             'chain', 2,
341             sub
342             #line 127 "lib/Chemistry/OpenSMILES/Parser.yp"
343             {
344             $_[2]->{graph} = $_[1]->{last}{graph};
345             $_[2]->{index} = $_[1]->{last}{index};
346              
347             $_[2]->{graph}->add_edge( $_[1]->{last}, $_[2] );
348              
349             if( is_aromatic $_[1]->{last} && is_aromatic $_[2] ) {
350             $_[2]->{graph}->set_edge_attribute( $_[1]->{last},
351             $_[2],
352             'bond',
353             ':' );
354             }
355              
356             _push_chirality_neighbour( $_[1]->{last}, $_[2] );
357             _push_chirality_neighbour( $_[2], $_[1]->{last} );
358              
359             $_[1]->{last} = $_[2];
360              
361             return $_[1];
362             }
363             ],
364             [#Rule 4
365             'chain', 3,
366             sub
367             #line 148 "lib/Chemistry/OpenSMILES/Parser.yp"
368             {
369             $_[3]->{graph} = $_[1]->{last}{graph};
370             $_[3]->{index} = $_[1]->{last}{index};
371              
372             if( $_[2] ne '-' ) {
373             $_[3]->{graph}->set_edge_attribute( $_[1]->{last},
374             $_[3],
375             'bond',
376             $_[2] );
377             } else {
378             $_[3]->{graph}->add_edge( $_[1]->{last}, $_[3] );
379             }
380              
381             _push_chirality_neighbour( $_[1]->{last}, $_[3] );
382             _push_chirality_neighbour( $_[3], $_[1]->{last} );
383              
384             $_[1]->{last} = $_[3];
385              
386             return $_[1];
387             }
388             ],
389             [#Rule 5
390             'chain', 3,
391             sub
392             #line 169 "lib/Chemistry/OpenSMILES/Parser.yp"
393             {
394             my $g = Graph::Undirected->new( refvertexed => 1 );
395             $g->add_vertex( $_[3] );
396             push @{$_[0]->{USER}{GRAPHS}}, $g;
397              
398             $_[3]->{graph} = $g;
399             $_[3]->{index} = @{$_[0]->{USER}{GRAPHS}}-1;
400              
401             $_[1]->{last} = $_[3];
402              
403             return $_[1];
404             }
405             ],
406             [#Rule 6
407             'chain', 4,
408             sub
409             #line 182 "lib/Chemistry/OpenSMILES/Parser.yp"
410             {
411             if( $_[0]->{USER}{OPTIONS}{report_unnecessary_dot_usage} &&
412             $_[3]->{first}{index} != $_[3]->{last}{index} ) {
413             warn 'unnecessary use of dot in parenthesis' . "\n";
414             }
415              
416             if( $_[1]->{last}{index} != $_[3]->{first}{index} ) {
417             $_[0]->_merge_graphs( $_[1]->{last}{index},
418             $_[3]->{first}{index} );
419             }
420              
421             $_[1]->{last}{graph}->add_edge( $_[1]->{last}, $_[3]->{first} );
422              
423             if( is_aromatic $_[1]->{last} && is_aromatic $_[3]->{first} ) {
424             $_[1]->{last}{graph}->set_edge_attribute( $_[1]->{last},
425             $_[3]->{first},
426             'bond',
427             ':' );
428             }
429              
430             _push_chirality_neighbour( $_[1]->{last}, $_[3]->{first} );
431             _unshift_chirality_neighbour( $_[3]->{first}, $_[1]->{last} );
432              
433             return $_[1];
434             }
435             ],
436             [#Rule 7
437             'chain', 5,
438             sub
439             #line 208 "lib/Chemistry/OpenSMILES/Parser.yp"
440             {
441             if( $_[0]->{USER}{OPTIONS}{report_unnecessary_dot_usage} &&
442             $_[4]->{first}{index} != $_[4]->{last}{index} ) {
443             warn 'unnecessary use of dot in parenthesis' . "\n";
444             }
445              
446             if( $_[1]->{last}{index} != $_[4]->{first}{index} ) {
447             $_[0]->_merge_graphs( $_[1]->{last}{index},
448             $_[4]->{first}{index} );
449             }
450              
451             if( $_[3] ne '-' ) {
452             $_[1]->{last}{graph}->set_edge_attribute( $_[1]->{last},
453             $_[4]->{first},
454             'bond',
455             $_[3] );
456             } else {
457             $_[1]->{last}{graph}->add_edge( $_[1]->{last},
458             $_[4]->{first} );
459             }
460              
461             _push_chirality_neighbour( $_[1]->{last}, $_[4]->{first} );
462             _unshift_chirality_neighbour( $_[4]->{first}, $_[1]->{last} );
463              
464             return $_[1];
465             }
466             ],
467             [#Rule 8
468             'chain', 5,
469             sub
470             #line 235 "lib/Chemistry/OpenSMILES/Parser.yp"
471             {
472             if( $_[0]->{USER}{OPTIONS}{report_unnecessary_dot_usage} ) {
473             warn 'unnecessary use of dot in parenthesis' . "\n";
474             }
475              
476             return $_[1];
477             }
478             ],
479             [#Rule 9
480             'chain', 2,
481             sub
482             #line 248 "lib/Chemistry/OpenSMILES/Parser.yp"
483             {
484             $_[0]->_add_ring_bond( $_[1]->{last}, $_[2] );
485             return $_[1];
486             }
487             ],
488             [#Rule 10
489             'chain', 3,
490             sub
491             #line 253 "lib/Chemistry/OpenSMILES/Parser.yp"
492             {
493             $_[0]->_add_ring_bond( $_[1]->{last}, $_[3], $_[2] );
494             return $_[1];
495             }
496             ],
497             [#Rule 11
498             'bond', 1, undef
499             ],
500             [#Rule 12
501             'bond', 1, undef
502             ],
503             [#Rule 13
504             'bond', 1, undef
505             ],
506             [#Rule 14
507             'bond', 1, undef
508             ],
509             [#Rule 15
510             'bond', 1, undef
511             ],
512             [#Rule 16
513             'bond', 1, undef
514             ],
515             [#Rule 17
516             'bond', 1, undef
517             ]
518             ],
519             @_);
520             bless($self,$class);
521             }
522              
523             #line 261 "lib/Chemistry/OpenSMILES/Parser.yp"
524              
525              
526             # Footer section
527              
528             sub _Error
529             {
530             my( $self ) = @_;
531             close $self->{USER}{FILEIN} if $self->{USER}{FILEIN};
532              
533             if( ${$self->{TOKEN}} eq '' &&
534             grep { defined $_ && !ref $_ && $_ eq '(' }
535             map { $_->[1] } @{$self->{STACK}} ) {
536             die "$0: syntax error: missing closing parenthesis.\n";
537             }
538              
539             if( ${$self->{TOKEN}} eq ')' ) {
540             die "$0: syntax error: unbalanced parentheses.\n";
541             }
542              
543             my $msg = "$0: syntax error at position $self->{USER}{CHARNO}";
544             if( $self->YYData->{INPUT} ) {
545             $self->YYData->{INPUT} =~ s/\n$//;
546             die "$msg: '" . $self->YYData->{INPUT} . "'.\n";
547             } else {
548             die "$msg.\n";
549             }
550             }
551              
552             sub _Lexer
553             {
554             my( $self ) = @_;
555              
556             # If the line is empty and the input is originating from the file,
557             # another line is read.
558             if( !$self->YYData->{INPUT} && $self->{USER}{FILEIN} ) {
559             my $filein = $self->{USER}{FILEIN};
560             $self->YYData->{INPUT} = <$filein>;
561             $self->{USER}{CHARNO} = 0;
562             }
563              
564             if( $self->YYData->{INPUT} =~ s/^(\s+)// ) {
565             $self->{USER}{CHARNO} += length $1;
566             }
567              
568             my $hcount_re = 'H[0-9]?';
569             if( defined $self->{USER}{OPTIONS}{max_hydrogen_count_digits} ) {
570             $hcount_re = sprintf 'H[0-9]{0,%d}',
571             $self->{USER}{OPTIONS}{max_hydrogen_count_digits};
572             }
573              
574             # Bracket atoms
575             if( $self->YYData->{INPUT} =~ s/^\[ (?[0-9]+)?
576             (?[A-Za-z][a-z]?|\*)
577             (?@(
578             (TH|AL)[12] |
579             SP [123] |
580             (TB|OH)[0-9]{1,2} |
581             @?
582             ))?
583             (? $hcount_re)?
584             (?--|\+\+|[-+][0-9]{0,2})?
585             (:(?[0-9]+))? \]//x ) {
586             my $atom = { %+, number => $self->{USER}{ATOMNO} };
587             $self->{USER}{ATOMNO} ++;
588             $self->{USER}{CHARNO} += length $&;
589              
590             # Check for existence of the seen element
591             # Due to https://github.com/briandfoy/chemistry-elements/issues/16, Chemistry::Elements < 1.079 has 'Ha' instead of 'Db'
592             if( $atom->{symbol} eq '*' || $atom->{symbol} eq 'Db' ) {
593             # OK
594             } elsif( $atom->{symbol} eq lc $atom->{symbol} &&
595             $atom->{symbol} !~ /^(as|se|[bcnops])$/ ) {
596             die "aromatic chemical element '$atom->{symbol}' is not allowed\n";
597             } elsif( $atom->{symbol} eq 'Ha' ||
598             !Chemistry::Elements->new( $atom->{symbol} ) ) {
599             die "chemical element with symbol '$atom->{symbol}' is unknown\n";
600             }
601              
602             if( $atom->{charge} ) {
603             $atom->{charge} =~ s/^([-+])$/${1}1/;
604             $atom->{charge} =~ s/^([-+])\1$/${1}2/;
605             $atom->{charge} = int $atom->{charge};
606             }
607              
608             if( $atom->{hcount} ) {
609             $atom->{hcount} =~ s/^H//;
610             $atom->{hcount} = $atom->{hcount} ? int $atom->{hcount} : 1;
611             } else {
612             $atom->{hcount} = 0;
613             }
614              
615             if( $atom->{isotope} ) {
616             $atom->{isotope} = int $atom->{isotope};
617             }
618              
619             # Atom class is an arbitrary number, 0 by default
620             $atom->{class} = exists $atom->{class} ? int $atom->{class} : 0;
621              
622             return ( 'atom', $atom );
623             }
624              
625             # Bracketless atoms
626             if( $self->YYData->{INPUT} =~ s/^(Br|Cl|[BCINOPSFbcnops*])// ) {
627             my $atom = { symbol => $1,
628             class => 0,
629             number => $self->{USER}{ATOMNO} };
630             $self->{USER}{ATOMNO} ++;
631             $self->{USER}{CHARNO} += length $&;
632             return ( 'atom', $atom );
633             }
634              
635             # Ring bonds
636             if( $self->YYData->{INPUT} =~ s/^%([0-9]{2})// ||
637             $self->YYData->{INPUT} =~ s/^([0-9])// ) {
638             $self->{USER}{CHARNO} += length $&;
639             return ( 'ringbond', int $1 );
640             }
641              
642             my $char = substr( $self->YYData->{INPUT}, 0, 1 );
643             if( $char ne '' ) {
644             $self->YYData->{INPUT} = substr( $self->YYData->{INPUT}, 1 );
645             }
646             $self->{USER}{CHARNO} ++;
647             return( $char, $char );
648             }
649              
650             sub parse
651             {
652             my( $self, $string, $options ) = @_;
653             $options = {} unless $options;
654              
655             $self->YYData->{INPUT} = $string;
656             $self->{USER}{GRAPHS} = [];
657             $self->{USER}{RINGBONDS} = {};
658             $self->{USER}{ATOMNO} = 0;
659             $self->{USER}{CHARNO} = 0;
660             $self->{USER}{OPTIONS} = $options;
661             $self->YYParse( yylex => \&_Lexer,
662             yyerror => \&_Error,
663             yydebug => $options->{debug} );
664              
665             if( scalar keys %{$self->{USER}{RINGBONDS}} ) {
666             die "$0: unclosed ring bond(s) detected: " .
667             join( ', ', sort { $a <=> $b } keys %{$self->{USER}{RINGBONDS}} ) .
668             ".\n";
669             }
670              
671             my @graphs = grep { defined } @{$self->{USER}{GRAPHS}};
672             for my $graph (@graphs) {
673             for my $atom (sort { $a->{number} <=> $b->{number} } $graph->vertices) {
674             delete $atom->{graph};
675             delete $atom->{index};
676             next if $options->{raw};
677              
678             # Promote implicit hydrogen atoms into explicit ones
679             if( !exists $atom->{hcount} ) {
680             next if !exists $normal_valence{$atom->{symbol}};
681             my $degree = sum0 map { $_ ne ':' && exists $bond_symbol_to_order{$_} ? $bond_symbol_to_order{$_} : 1 }
682             map { $graph->has_edge_attribute( $atom, $_, 'bond' )
683             ? $graph->get_edge_attribute( $atom, $_, 'bond' )
684             : '-' }
685             $graph->neighbours( $atom );
686             my $valence = first { $degree <= $_ }
687             @{$normal_valence{$atom->{symbol}}};
688             next unless defined $valence;
689             $atom->{hcount} = $valence - $degree;
690             }
691             for (1..$atom->{hcount}) {
692             my $hydrogen = { symbol => 'H',
693             class => 0,
694             number => $self->{USER}{ATOMNO} };
695             $graph->add_edge( $atom, $hydrogen );
696             $self->{USER}{ATOMNO} ++;
697             if( is_chiral $atom ) {
698             if( $atom->{chirality_neighbours} ) {
699             if( any { $_->{number} < $atom->{number} } @{$atom->{chirality_neighbours}} ) {
700             splice @{$atom->{chirality_neighbours}}, 1, 0, $hydrogen;
701             } else {
702             _unshift_chirality_neighbour( $atom, $hydrogen );
703             }
704             } else {
705             # This only happens if chiral atom does not have neighbours other than implicit hydrogens.
706             # This is degenerate case anyway.
707             _push_chirality_neighbour( $atom, $hydrogen );
708             }
709             }
710             }
711             delete $atom->{hcount};
712              
713             # Unify the representation of chirality
714             if( is_chiral $atom ) {
715             if( $atom->{chirality} =~ /^@@?$/ ) {
716             if( $graph->degree( $atom ) == 2 ) {
717             $atom->{chirality} =~ s/@+/'@AL' . length $&/e;
718             } elsif( $graph->degree( $atom ) == 5 ) {
719             $atom->{chirality} =~ s/@+/'@TB' . length $&/e;
720             } elsif( $graph->degree( $atom ) == 6 ) {
721             $atom->{chirality} =~ s/@+/'@OH' . length $&/e;
722             }
723             }
724              
725             $atom->{chirality} =~ s/^\@TH1$/@/;
726             $atom->{chirality} =~ s/^\@TH2$/@@/;
727             }
728             }
729             }
730              
731             return @graphs;
732             }
733              
734             sub _add_ring_bond
735             {
736             my( $self, $atom, $ring_bond, $bond ) = @_;
737             if( $self->{USER}{RINGBONDS}{$ring_bond} ) {
738             $self->_merge_graphs( $self->{USER}{RINGBONDS}{$ring_bond}{atom}{index},
739             $atom->{index} );
740              
741             if( $bond && $self->{USER}{RINGBONDS}{$ring_bond}{bond} &&
742             (($bond !~ /^[\\\/]$/ &&
743             $bond ne $self->{USER}{RINGBONDS}{$ring_bond}{bond}) ||
744             ($bond eq '\\' &&
745             $self->{USER}{RINGBONDS}{$ring_bond}{bond} ne '/') ||
746             ($bond eq '/' &&
747             $self->{USER}{RINGBONDS}{$ring_bond}{bond} ne '\\')) ) {
748             die "$0: ring bond types for ring bond $ring_bond do not match.\n";
749             }
750             $bond = first { defined }
751             ( $self->{USER}{RINGBONDS}{$ring_bond}{bond}, $bond );
752              
753             if( $bond && !defined $self->{USER}{RINGBONDS}{$ring_bond}{bond} ) {
754             # If cis/trans marker is not specified when cis/trans bond is
755             # seen first, it has to be inverted:
756             $bond = toggle_cistrans $bond;
757             }
758              
759             my $ring_atom = $self->{USER}{RINGBONDS}{$ring_bond}{atom};
760             die "atom cannot be bonded to itself\n" if $atom == $ring_atom;
761             if( !$bond && is_aromatic $ring_atom && is_aromatic $atom ) {
762             $bond = ':';
763             }
764             if( $bond && $bond ne '-' ) {
765             $atom->{graph}->set_edge_attribute( $ring_atom,
766             $atom,
767             'bond',
768             $bond );
769             } else {
770             $atom->{graph}->add_edge( $ring_atom, $atom );
771             }
772             delete $self->{USER}{RINGBONDS}{$ring_bond};
773              
774             if( is_chiral $ring_atom && $ring_atom->{chirality_neighbours} ) {
775             my $pos = first { !ref $ring_atom->{chirality_neighbours}[$_] &&
776             $ring_atom->{chirality_neighbours}[$_] == $ring_bond }
777             0..$#{$ring_atom->{chirality_neighbours}};
778             $ring_atom->{chirality_neighbours}[$pos] = $atom if defined $pos;
779             }
780             _push_chirality_neighbour( $atom, $ring_atom );
781             } else {
782             $self->{USER}{RINGBONDS}{$ring_bond} =
783             { atom => $atom, $bond ? ( bond => $bond ) : () };
784              
785             # Record a placeholder for later addition of real chirality
786             # neighbour, which will be identified by the ring bond number
787             _push_chirality_neighbour( $atom, $ring_bond );
788             }
789             }
790              
791             sub _merge_graphs
792             {
793             my( $self, $index1, $index2 ) = @_;
794             return if $index1 == $index2;
795              
796             my $g1 = $self->{USER}{GRAPHS}[$index1];
797             my $g2 = $self->{USER}{GRAPHS}[$index2];
798              
799             for ($g2->vertices) {
800             $_->{graph} = $g1;
801             $_->{index} = $index1;
802             }
803             $g1->add_vertices( $g2->vertices );
804              
805             for ($g2->edges) {
806             my $attributes = $g2->get_edge_attributes( @$_ );
807             if( $attributes ) {
808             $g1->set_edge_attributes( @$_, $attributes );
809             } else {
810             $g1->add_edge( @$_ );
811             }
812             }
813              
814             $self->{USER}{GRAPHS}[$index2] = undef;
815             }
816              
817             sub _push_chirality_neighbour
818             {
819             my( $atom1, $atom2 ) = @_;
820             return unless is_chiral $atom1;
821             push @{$atom1->{chirality_neighbours}}, $atom2;
822             }
823              
824             sub _unshift_chirality_neighbour
825             {
826             my( $atom1, $atom2 ) = @_;
827             return unless is_chiral $atom1;
828             unshift @{$atom1->{chirality_neighbours}}, $atom2;
829             }
830              
831             1;
832              
833             =head1 AUTHORS
834              
835             Andrius Merkys, Emerkys@cpan.orgE
836              
837             =cut
838              
839             1;