mod write;
pub use write::{write, write_with_priority, write_with_priority_styled, WriteStyle, Written};
use std::collections::{HashMap, HashSet};
use omgkit_core::{AtomData, AtomFlags, BondData, BondDirection, BondOrder, ChiralTag, MolBuilder};
use crate::error::{ParseError, ParseErrorKind as K, Result};
pub fn parse(input: &str) -> Result<MolBuilder> {
Parser::new(input.as_bytes()).run()
}
pub fn parse_line(line: &str) -> Result<MolBuilder> {
let line = line.trim();
let (smi, name) = match line.find(char::is_whitespace) {
Some(i) => (&line[..i], line[i..].trim()),
None => (line, ""),
};
let mut mol = parse(smi)?;
if !name.is_empty() {
mol.set_name(name);
}
Ok(mol)
}
#[derive(Debug, Clone, Copy)]
struct RingOpen {
atom: u32,
order: Option<BondOrder>,
explicit_order: bool,
swap_ends: bool,
dir: BondDirection,
seq: u32,
pos: usize,
}
#[derive(Debug, Clone, Copy)]
struct RingBond {
number: u32,
seq: u32,
open_seq: u32,
begin: u32,
end: u32,
order: BondOrder,
dir: BondDirection,
}
#[derive(Debug, Clone, Copy)]
struct Pending {
order: Option<BondOrder>,
explicit_order: bool,
swap_ends: bool,
dir: BondDirection,
pos: usize,
}
#[derive(Debug, Clone)]
struct ChiralRec {
atom: u32,
tag: ChiralTag,
is_smiles_start: bool,
num_explicit_hs: u8,
}
struct Parser<'a> {
src: &'a [u8],
pos: usize,
mol: MolBuilder,
prev: Option<u32>,
branches: Vec<(u32, usize)>,
rings: HashMap<u32, RingOpen>,
ring_bonds: Vec<RingBond>,
ring_seq: u32,
pending: Option<Pending>,
chiral: Vec<ChiralRec>,
chiral_of: HashMap<u32, usize>,
ring_seqs_of: HashMap<u32, Vec<u32>>,
ring_bond_index: HashMap<u32, u32>,
pending_ring_pairs: HashSet<(u32, u32)>,
}
fn endpoint_pair(a: u32, b: u32) -> (u32, u32) {
if a <= b {
(a, b)
} else {
(b, a)
}
}
impl<'a> Parser<'a> {
fn new(src: &'a [u8]) -> Self {
Self {
src,
pos: 0,
mol: MolBuilder::new(),
prev: None,
branches: Vec::new(),
rings: HashMap::new(),
ring_bonds: Vec::new(),
ring_seq: 0,
pending: None,
chiral: Vec::new(),
ring_seqs_of: HashMap::new(),
chiral_of: HashMap::new(),
ring_bond_index: HashMap::new(),
pending_ring_pairs: HashSet::new(),
}
}
fn err<T>(&self, kind: K, pos: usize) -> Result<T> {
Err(ParseError::new(kind, pos, self.src))
}
fn peek(&self) -> Option<u8> {
self.src.get(self.pos).copied()
}
fn bump(&mut self) -> Option<u8> {
let b = self.peek();
if b.is_some() {
self.pos += 1;
}
b
}
fn eat(&mut self, b: u8) -> bool {
if self.peek() == Some(b) {
self.pos += 1;
true
} else {
false
}
}
fn run(mut self) -> Result<MolBuilder> {
if self.src.is_empty() {
return self.err(K::Empty, 0);
}
while let Some(b) = self.peek() {
match b {
b'(' => self.open_branch()?,
b')' => self.close_branch()?,
b'[' => self.bracket_atom()?,
b'.' => {
if let Some(p) = self.pending {
return self.err(K::DanglingBond, p.pos);
}
self.pos += 1;
self.prev = None;
}
b'-' | b'=' | b'#' | b'$' | b':' | b'/' | b'\\' | b'<' => self.bond_symbol()?,
b'0'..=b'9' => {
let pos = self.pos;
let n = u32::from(self.bump().expect("已 peek") - b'0');
self.ring_closure(n, pos)?;
}
b'%' => {
let pos = self.pos;
self.pos += 1;
let n = self.ring_number()?;
self.ring_closure(n, pos)?;
}
_ => self.organic_atom()?,
}
}
if let Some(p) = self.pending {
return self.err(K::DanglingBond, p.pos);
}
if let Some(&(_, _)) = self.branches.last() {
let pos = self.pos;
return self.err(K::UnbalancedParen, pos);
}
if let Some((&num, open)) = self.rings.iter().min_by_key(|(_, o)| o.pos) {
let pos = open.pos;
return self.err(K::UnclosedRingBond(num), pos);
}
if self.mol.num_atoms() == 0 {
return self.err(K::Empty, 0);
}
let mut ring_bonds = std::mem::take(&mut self.ring_bonds);
ring_bonds.sort_by_key(|r| (r.number, r.seq));
let (pos, src) = (self.pos, self.src);
for rb in ring_bonds {
let mut bd = BondData::new(rb.begin, rb.end, rb.order);
bd.direction = rb.dir;
mark_aromatic(&mut bd);
let idx = self
.mol
.add_bond_data(bd)
.map_err(|_| ParseError::new(K::RingBondToSelf(rb.number), pos, src))?;
self.ring_bond_index.insert(rb.open_seq, idx);
}
self.fix_chirality();
Ok(self.mol)
}
fn open_branch(&mut self) -> Result<()> {
let pos = self.pos;
self.pos += 1;
match self.prev {
Some(a) => {
self.branches.push((a, self.mol.num_atoms()));
Ok(())
}
None => self.err(K::UnbalancedParen, pos),
}
}
fn close_branch(&mut self) -> Result<()> {
let pos = self.pos;
self.pos += 1;
match self.branches.pop() {
Some((atom, n_at_open)) => {
if self.mol.num_atoms() == n_at_open {
return self.err(K::EmptyBranch, pos);
}
if let Some(p) = self.pending {
return self.err(K::DanglingBond, p.pos);
}
self.prev = Some(atom);
Ok(())
}
None => self.err(K::UnbalancedParen, pos),
}
}
fn bond_symbol(&mut self) -> Result<()> {
let pos = self.pos;
if self.pending.is_some() {
return self.err(K::DanglingBond, pos);
}
let b = self.bump().expect("已 peek");
let (order, explicit_order, swap_ends, dir) = match b {
b'-' => {
if self.eat(b'>') {
(Some(BondOrder::Dative), true, false, BondDirection::None)
} else {
(Some(BondOrder::Single), true, false, BondDirection::None)
}
}
b'<' => {
if !self.eat(b'-') {
return self.err(K::UnexpectedChar('<'), pos);
}
(Some(BondOrder::Dative), true, true, BondDirection::None)
}
b'=' => (Some(BondOrder::Double), true, false, BondDirection::None),
b'#' => (Some(BondOrder::Triple), true, false, BondDirection::None),
b'$' => (Some(BondOrder::Quadruple), true, false, BondDirection::None),
b':' => (Some(BondOrder::Aromatic), true, false, BondDirection::None),
b'/' => (None, false, false, BondDirection::UpRight),
b'\\' => {
self.eat(b'\\');
(None, false, false, BondDirection::DownRight)
}
_ => unreachable!("由调用方 match 保证"),
};
self.pending = Some(Pending {
order,
explicit_order,
swap_ends,
dir,
pos,
});
Ok(())
}
fn organic_atom(&mut self) -> Result<()> {
let pos = self.pos;
let b = self.peek().expect("已 peek");
let two = self.src.get(self.pos + 1).copied();
let (z, len, aromatic) = match (b, two) {
(b'C', Some(b'l')) => (17u8, 2usize, false),
(b'B', Some(b'r')) => (35, 2, false),
(b'B', _) => (5, 1, false),
(b'C', _) => (6, 1, false),
(b'N', _) => (7, 1, false),
(b'O', _) => (8, 1, false),
(b'P', _) => (15, 1, false),
(b'S', _) => (16, 1, false),
(b'F', _) => (9, 1, false),
(b'I', _) => (53, 1, false),
(b'b', _) => (5, 1, true),
(b'c', _) => (6, 1, true),
(b'n', _) => (7, 1, true),
(b'o', _) => (8, 1, true),
(b'p', _) => (15, 1, true),
(b's', _) => (16, 1, true),
(b'*', _) => (0, 1, false),
_ => {
let c = char::from(b);
return self.err(K::UnexpectedChar(c), pos);
}
};
self.pos += len;
let mut atom = AtomData::new(z);
if aromatic {
atom.flags.insert(AtomFlags::AROMATIC);
}
self.push_atom(atom, None, pos)
}
fn bracket_atom(&mut self) -> Result<()> {
let open_pos = self.pos;
self.pos += 1;
let isotope = self.opt_number(5)?;
let sym_pos = self.pos;
let (z, aromatic) = self.bracket_symbol(sym_pos)?;
let (tag, stereo_perm) = self.opt_chirality()?;
let mut hcount = 0u8;
if self.peek() == Some(b'H') {
self.pos += 1;
hcount = match self.opt_number(2)? {
Some(n) => u8::try_from(n)
.map_err(|_| ParseError::new(K::NumberOverflow, self.pos, self.src))?,
None => 1,
};
}
let charge = self.opt_charge()?;
let mut map = 0u16;
if self.eat(b':') {
let n = self.opt_number(5)?.ok_or_else(|| {
ParseError::new(K::BadBracketAtom("`:` 后缺少映射号"), self.pos, self.src)
})?;
map = u16::try_from(n)
.map_err(|_| ParseError::new(K::NumberOverflow, self.pos, self.src))?;
}
if !self.eat(b']') {
let kind = if self.peek().is_none() {
K::UnexpectedEnd
} else {
K::BadBracketAtom("方括号未正确闭合")
};
return self.err(kind, self.pos);
}
let mut atom = AtomData::new(z);
atom.isotope = isotope
.map(|n| u16::try_from(n).unwrap_or(u16::MAX))
.unwrap_or(0);
atom.num_explicit_hs = hcount;
atom.formal_charge = charge;
atom.atom_map = map;
atom.chiral_tag = tag;
atom.stereo_perm = stereo_perm;
atom.flags.insert(AtomFlags::NO_IMPLICIT);
if aromatic {
atom.flags.insert(AtomFlags::AROMATIC);
}
self.push_atom(atom, Some((tag, hcount)), open_pos)
}
fn bracket_symbol(&mut self, pos: usize) -> Result<(u8, bool)> {
let b = match self.peek() {
Some(b) => b,
None => return self.err(K::UnexpectedEnd, pos),
};
if b == b'*' {
self.pos += 1;
return Ok((0, false));
}
if b.is_ascii_uppercase() {
let mut sym = String::new();
sym.push(char::from(b));
let second = self.src.get(self.pos + 1).copied();
if let Some(c) = second {
if c.is_ascii_lowercase() {
let two: String = [char::from(b), char::from(c)].iter().collect();
if let Some(z) = omgkit_core::element::atomic_num_of(&two) {
self.pos += 2;
return Ok((z, false));
}
}
}
if let Some(z) = omgkit_core::element::atomic_num_of(&sym) {
self.pos += 1;
return Ok((z, false));
}
if let Some(c) = second {
if c.is_ascii_lowercase() {
sym.push(char::from(c));
}
}
return self.err(K::UnknownElement(sym), pos);
}
if b.is_ascii_lowercase() {
let second = self.src.get(self.pos + 1).copied();
if let Some(c) = second {
if c.is_ascii_lowercase() {
let two: String = [char::from(b), char::from(c)].iter().collect();
let up = format!("{}{}", two[..1].to_ascii_uppercase(), &two[1..]);
if matches!(two.as_str(), "se" | "as" | "te" | "si") {
if let Some(z) = omgkit_core::element::atomic_num_of(&up) {
self.pos += 2;
return Ok((z, true));
}
}
}
}
let up = char::from(b).to_ascii_uppercase().to_string();
if let Some(z) = omgkit_core::element::atomic_num_of(&up) {
if omgkit_core::element::can_be_aromatic_lowercase(z) {
self.pos += 1;
return Ok((z, true));
}
}
return self.err(K::UnknownElement(char::from(b).to_string()), pos);
}
self.err(K::BadBracketAtom("缺少元素符号"), pos)
}
fn opt_chirality(&mut self) -> Result<(ChiralTag, u8)> {
if self.peek() != Some(b'@') {
return Ok((ChiralTag::Unspecified, 0));
}
self.pos += 1;
if self.eat(b'@') {
return Ok((ChiralTag::Cw, 0));
}
let rest = &self.src[self.pos..];
let two = rest.get(..2).map(<[u8]>::to_ascii_uppercase);
let (name, max, class) = match two.as_deref() {
Some(b"TH") => ("TH", 2, ChiralTag::Ccw),
Some(b"AL") => ("AL", 2, ChiralTag::Allene),
Some(b"SP") => ("SP", 3, ChiralTag::SquarePlanar),
Some(b"TB") => ("TB", 20, ChiralTag::TrigonalBipyramidal),
Some(b"OH") => ("OH", 30, ChiralTag::Octahedral),
_ => return Ok((ChiralTag::Ccw, 0)),
};
self.pos += 2;
let pos = self.pos;
let perm = self.opt_number(2)?.unwrap_or(0);
if perm > max {
return self.err(
K::StereoPermOutOfRange {
geometry: name,
got: perm,
max,
},
pos,
);
}
if name == "TH" {
return Ok((
if perm == 2 {
ChiralTag::Cw
} else {
ChiralTag::Ccw
},
0,
));
}
Ok((class, u8::try_from(perm).unwrap_or(0)))
}
fn opt_charge(&mut self) -> Result<i8> {
let sign = match self.peek() {
Some(b'+') => 1i8,
Some(b'-') => -1i8,
_ => return Ok(0),
};
self.pos += 1;
let mut n = 1i32;
while self.peek() == Some(if sign > 0 { b'+' } else { b'-' }) {
self.pos += 1;
n += 1;
}
if n > 1 {
return i8::try_from(n * i32::from(sign))
.map_err(|_| ParseError::new(K::NumberOverflow, self.pos, self.src));
}
if let Some(v) = self.opt_number(2)? {
n = i32::try_from(v).unwrap_or(i32::MAX);
}
i8::try_from(n * i32::from(sign))
.map_err(|_| ParseError::new(K::NumberOverflow, self.pos, self.src))
}
fn opt_number(&mut self, max_digits: usize) -> Result<Option<u32>> {
let start = self.pos;
let mut val: u64 = 0;
let mut n = 0;
while n < max_digits {
match self.peek() {
Some(d @ b'0'..=b'9') => {
val = val * 10 + u64::from(d - b'0');
self.pos += 1;
n += 1;
}
_ => break,
}
}
if n == 0 {
return Ok(None);
}
u32::try_from(val)
.map(Some)
.map_err(|_| ParseError::new(K::NumberOverflow, start, self.src))
}
fn ring_number(&mut self) -> Result<u32> {
if self.eat(b'(') {
let n = self.opt_number(5)?.ok_or_else(|| {
ParseError::new(K::BadBracketAtom("`%(` 后缺少数字"), self.pos, self.src)
})?;
if !self.eat(b')') {
return self.err(K::BadBracketAtom("`%(` 未闭合"), self.pos);
}
return Ok(n);
}
let pos = self.pos;
match self.opt_number(2)? {
Some(n) if self.pos - pos == 2 => Ok(n),
_ => self.err(K::BadBracketAtom("`%` 后需要两位数字"), pos),
}
}
fn push_atom(
&mut self,
atom: AtomData,
chiral: Option<(ChiralTag, u8)>,
pos: usize,
) -> Result<()> {
let aromatic = atom.flags.contains(AtomFlags::AROMATIC);
let idx = self.mol.add_atom_data(atom);
if let Some((tag, num_explicit_hs)) = chiral {
if tag != ChiralTag::Unspecified {
self.chiral_of.insert(idx, self.chiral.len());
self.chiral.push(ChiralRec {
atom: idx,
tag,
is_smiles_start: self.prev.is_none(),
num_explicit_hs,
});
}
}
if let Some(prev) = self.prev {
let pending = self.pending.take();
let order = match pending.and_then(|p| p.order) {
Some(o) => o,
None => self.default_order(prev, aromatic),
};
let (begin, end) = if pending.is_some_and(|p| p.swap_ends) {
(idx, prev)
} else {
(prev, idx)
};
let mut bd = BondData::new(begin, end, order);
bd.direction = pending.map_or(BondDirection::None, |p| p.dir);
mark_aromatic(&mut bd);
let src = self.src;
self.mol
.add_bond_data(bd)
.map_err(|_| ParseError::new(K::UnexpectedChar('?'), pos, src))?;
} else if let Some(p) = self.pending.take() {
return self.err(K::DanglingBond, p.pos);
}
self.prev = Some(idx);
Ok(())
}
fn default_order(&self, prev: u32, cur_aromatic: bool) -> BondOrder {
let prev_aromatic = self.mol.atoms()[prev as usize]
.flags
.contains(AtomFlags::AROMATIC);
if prev_aromatic && cur_aromatic {
BondOrder::Aromatic
} else {
BondOrder::Single
}
}
fn note_ring(&mut self, atom: u32, open_seq: u32) {
self.ring_seqs_of.entry(atom).or_default().push(open_seq);
}
fn ring_closure(&mut self, num: u32, pos: usize) -> Result<()> {
let cur = match self.prev {
Some(a) => a,
None => return self.err(K::UnexpectedChar(char::from(self.src[pos])), pos),
};
let pending = self.pending.take();
match self.rings.remove(&num) {
Some(open) => {
if open.atom == cur {
return self.err(K::RingBondToSelf(num), pos);
}
if self.bond_exists(open.atom, cur) {
return self.err(K::DuplicateRingBond(num), pos);
}
let close_order = pending.and_then(|p| p.order);
let close_explicit = pending.is_some_and(|p| p.explicit_order);
if open.explicit_order && close_explicit && open.order != close_order {
return self.err(K::ConflictingRingBondOrder(num), pos);
}
let order = match open.order.or(close_order) {
Some(o) => o,
None => {
let cur_arom = self.mol.atoms()[cur as usize]
.flags
.contains(AtomFlags::AROMATIC);
self.default_order(open.atom, cur_arom)
}
};
let open_is_begin = open.explicit_order;
let (begin, end) = if open_is_begin {
(open.atom, cur)
} else {
(cur, open.atom)
};
let swap_ends = if open.order.is_some() {
open.swap_ends
} else {
pending.is_some_and(|p| p.swap_ends)
};
let close_dir = pending.map_or(BondDirection::None, |p| p.dir);
let dir = if open_is_begin {
if open.dir != BondDirection::None {
open.dir
} else {
close_dir.flipped()
}
} else if close_dir != BondDirection::None {
close_dir
} else {
open.dir.flipped()
};
let (begin, end, dir) = if swap_ends {
(end, begin, dir.flipped())
} else {
(begin, end, dir)
};
let seq = u32::try_from(self.ring_bonds.len()).unwrap_or(u32::MAX);
self.pending_ring_pairs.insert(endpoint_pair(begin, end));
self.ring_bonds.push(RingBond {
number: num,
seq,
open_seq: open.seq,
begin,
end,
order,
dir,
});
self.note_ring(cur, open.seq);
}
None => {
let seq = self.ring_seq;
self.ring_seq += 1;
self.rings.insert(
num,
RingOpen {
atom: cur,
order: pending.and_then(|p| p.order),
explicit_order: pending.is_some_and(|p| p.explicit_order),
swap_ends: pending.is_some_and(|p| p.swap_ends),
dir: pending.map_or(BondDirection::None, |p| p.dir),
seq,
pos,
},
);
self.note_ring(cur, seq);
}
}
Ok(())
}
fn bond_exists(&self, a: u32, b: u32) -> bool {
self.mol.bond_between(a, b).is_some()
|| self.pending_ring_pairs.contains(&endpoint_pair(a, b))
}
fn ring_bonds_of(&self, atom: u32) -> Vec<u32> {
self.ring_seqs_of
.get(&atom)
.map(|seqs| {
seqs.iter()
.filter_map(|s| self.ring_bond_index.get(s).copied())
.collect()
})
.unwrap_or_default()
}
fn written_bond_order(&self, atom: u32) -> (Vec<u32>, usize) {
let ring_bonds = self.ring_bonds_of(atom);
let mut entries: Vec<(u32, Option<u32>)> = vec![(atom, None)];
for (i, b) in self.mol.bonds().iter().enumerate() {
let i = i as u32;
if ring_bonds.contains(&i) {
continue;
}
if let Some(other) = b.other_end(atom) {
entries.push((other, Some(i)));
}
}
entries.sort_by_key(|e| e.0);
let mut written = Vec::with_capacity(entries.len());
let mut self_pos = 0;
for (_, bond) in entries {
match bond {
None => {
self_pos = written.len();
written.extend(ring_bonds.iter().copied());
}
Some(i) => written.push(i),
}
}
(written, self_pos)
}
fn fix_chirality(&mut self) {
for rec in &self.chiral {
let atom = rec.atom;
let ring_bonds = self.ring_bonds_of(atom);
let stored = stored_bond_order(&self.mol, atom);
let (written, self_pos) = self.written_bond_order(atom);
if written.len() != stored.len() {
continue; }
if omgkit_core::polyhedron::ligand_count(rec.tag).is_some() {
if let Some(to) = coordination_ligands(&self.mol, atom, rec.tag) {
let mut from = written.clone();
if to.len() == from.len() + 1 {
from.insert(self_pos, VACANT_LIGAND);
}
let literal = self.mol.atoms()[atom as usize].stereo_perm;
if let Some(p) = omgkit_core::polyhedron::renumber(rec.tag, literal, &from, &to)
{
if let Some(a) = self.mol.atom_mut(atom) {
a.stereo_perm = p;
}
}
}
continue;
}
let on_allene_centre = allene_ends(&self.mol, atom).is_some();
if rec.tag == ChiralTag::Allene || (on_allene_centre && rec.tag.is_tetrahedral()) {
let literal = match rec.tag {
ChiralTag::Ccw => 1,
ChiralTag::Cw => 2,
_ => self.mol.atoms()[atom as usize].stereo_perm,
};
let perm = if on_allene_centre {
allene_renumber(
&self.mol,
atom,
literal,
&|a| self.written_bond_order(a),
&stored_order_at(&self.mol),
)
.unwrap_or(0)
} else {
0
};
if let Some(a) = self.mol.atom_mut(atom) {
a.chiral_tag = ChiralTag::Allene;
a.stereo_perm = perm;
}
continue;
}
if !rec.tag.is_tetrahedral() {
continue;
}
let mut odd = permutation_is_odd(&written, &stored).unwrap_or(false);
if stored.len() == 3 {
let has_fourth_valence = rec.num_explicit_hs == 1;
let unsaturated = self
.mol
.bonds()
.iter()
.filter(|b| b.other_end(atom).is_some())
.any(|b| b.order.as_double() > 1.0);
if (rec.is_smiles_start && rec.num_explicit_hs == 1)
|| (!has_fourth_valence && ring_bonds.len() == 1 && !unsaturated)
{
odd = !odd;
}
}
if odd {
if let Some(a) = self.mol.atom_mut(atom) {
a.chiral_tag = rec.tag.inverted();
}
}
}
}
}
fn mark_aromatic(bd: &mut BondData) {
if bd.order == BondOrder::Aromatic {
bd.flags.insert(omgkit_core::BondFlags::AROMATIC);
}
}
fn stored_bond_order(mol: &MolBuilder, atom: u32) -> Vec<u32> {
mol.bonds()
.iter()
.enumerate()
.filter(|(_, b)| b.other_end(atom).is_some())
.map(|(i, _)| i as u32)
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AlleneLigand {
Bond(u32),
Hydrogen(u32),
}
fn allene_ends(mol: &MolBuilder, centre: u32) -> Option<[u32; 2]> {
let bonds = stored_bond_order(mol, centre);
let [a, b] = <[u32; 2]>::try_from(bonds).ok()?;
let mut ends = [0u32; 2];
for (k, bond) in [a, b].into_iter().enumerate() {
let bd = &mol.bonds()[bond as usize];
if bd.order != BondOrder::Double {
return None;
}
ends[k] = bd.other_end(centre)?;
}
(ends[0] != ends[1]).then_some(ends)
}
fn allene_end_hydrogens(mol: &MolBuilder, end: u32) -> Option<u8> {
let a = mol.atoms()[end as usize];
if a.flags.contains(AtomFlags::NO_IMPLICIT) {
Some(a.num_explicit_hs)
} else {
omgkit_core::valence::implicit_hs_for_bare_form(mol, end)
}
}
fn allene_ligands(
mol: &MolBuilder,
centre: u32,
order_at: &dyn Fn(u32) -> (Vec<u32>, usize),
) -> Option<[AlleneLigand; 4]> {
let ends = allene_ends(mol, centre)?;
let (centre_bonds, _) = order_at(centre);
let mut out = Vec::with_capacity(4);
for chain in centre_bonds {
let end = mol.bonds()[chain as usize].other_end(centre)?;
if !ends.contains(&end) {
return None;
}
let hs = allene_end_hydrogens(mol, end)?;
if hs > 1 {
return None; }
let (bonds, self_pos) = order_at(end);
let mut ligands: Vec<AlleneLigand> = bonds.into_iter().map(AlleneLigand::Bond).collect();
if hs == 1 {
ligands.insert(self_pos, AlleneLigand::Hydrogen(end));
}
ligands.retain(|l| *l != AlleneLigand::Bond(chain));
if ligands.len() != 2 {
return None;
}
out.extend(ligands);
}
<[AlleneLigand; 4]>::try_from(out.as_slice()).ok()
}
fn stored_order_at(mol: &MolBuilder) -> impl Fn(u32) -> (Vec<u32>, usize) + '_ {
|a| (stored_bond_order(mol, a), 0)
}
fn allene_renumber(
mol: &MolBuilder,
centre: u32,
perm: u8,
from: &dyn Fn(u32) -> (Vec<u32>, usize),
to: &dyn Fn(u32) -> (Vec<u32>, usize),
) -> Option<u8> {
if perm != 1 && perm != 2 {
return None;
}
let a = allene_ligands(mol, centre, from)?;
let b = allene_ligands(mol, centre, to)?;
let odd = permutation_is_odd(&a, &b)?;
Some(if odd { 3 - perm } else { perm })
}
const VACANT_LIGAND: u32 = u32::MAX;
fn coordination_ligands(mol: &MolBuilder, atom: u32, tag: ChiralTag) -> Option<Vec<u32>> {
let n = omgkit_core::polyhedron::ligand_count(tag)?;
let mut ligands: Vec<u32> = mol.neighbors(atom).map(|(_, bond)| bond).collect();
match n.checked_sub(ligands.len())? {
0 => {}
1 => ligands.insert(0, VACANT_LIGAND),
_ => return None,
}
Some(ligands)
}
pub(crate) fn permutation_is_odd<T: PartialEq>(written: &[T], storage: &[T]) -> Option<bool> {
let n = written.len();
if n != storage.len() {
return None;
}
let mut used = vec![false; n];
let mut perm = Vec::with_capacity(n);
for w in written {
let j = storage
.iter()
.enumerate()
.position(|(j, s)| !used[j] && s == w)?;
used[j] = true;
perm.push(j);
}
let mut inversions = 0usize;
for i in 0..n {
for j in i + 1..n {
if perm[i] > perm[j] {
inversions += 1;
}
}
}
Some(inversions % 2 == 1)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::ParseErrorKind as K;
fn parse_ok(s: &str) -> MolBuilder {
parse(s).unwrap_or_else(|e| panic!("应能解析 {s:?}:\n{}", e.render()))
}
#[test]
fn shapes() {
for (smi, na, nb) in [
("C", 1, 0),
("CC", 2, 1),
("CCO", 3, 2),
("CC(=O)O", 4, 3),
("C1CCCCC1", 6, 6),
("CCO.CCN", 6, 4),
("c1ccccc1", 6, 6),
("C1CC2CCC1CC2", 8, 9),
] {
let m = parse_ok(smi);
assert_eq!((m.num_atoms(), m.num_bonds()), (na, nb), "{smi}");
}
}
#[test]
fn dot_keeps_one_molecule_with_two_fragments() {
let m = parse_ok("CCO.CCN");
assert_eq!(m.num_atoms(), 6);
assert_eq!(m.num_bonds(), 4);
}
#[test]
fn bracket_atom_fields() {
let m = parse_ok("[13CH4:7]");
let a = m.atoms()[0];
assert_eq!(a.atomic_num, 6);
assert_eq!(a.isotope, 13);
assert_eq!(a.num_explicit_hs, 4);
assert_eq!(a.atom_map, 7);
assert!(a.flags.contains(AtomFlags::NO_IMPLICIT));
}
#[test]
fn charge_forms_are_equivalent() {
assert_eq!(parse_ok("[Fe+2]").atoms()[0].formal_charge, 2);
assert_eq!(parse_ok("[Fe++]").atoms()[0].formal_charge, 2);
assert_eq!(parse_ok("[O-]").atoms()[0].formal_charge, -1);
assert_eq!(parse_ok("[O-2]").atoms()[0].formal_charge, -2);
assert_eq!(parse_ok("[O--]").atoms()[0].formal_charge, -2);
}
#[test]
fn wildcard_is_atomic_number_zero() {
assert_eq!(parse_ok("*").atoms()[0].atomic_num, 0);
assert_eq!(parse_ok("[*:1]").atoms()[0].atom_map, 1);
}
#[test]
fn two_char_element_beats_one_char() {
let m = parse_ok("ClCCBr");
let z: Vec<u8> = m.atoms().iter().map(|a| a.atomic_num).collect();
assert_eq!(z, vec![17, 6, 6, 35]);
}
#[test]
fn bracket_greedy_symbol_backtracks() {
assert_eq!(parse_ok("[C@](N)(O)(F)Cl").atoms()[0].atomic_num, 6);
assert_eq!(parse_ok("[Cl-]").atoms()[0].atomic_num, 17);
}
#[test]
fn lowercase_atoms_are_flagged_aromatic() {
let m = parse_ok("c1ccccc1");
assert!(m
.atoms()
.iter()
.all(|a| a.flags.contains(AtomFlags::AROMATIC)));
assert!(m.bonds().iter().all(|b| b.order == BondOrder::Aromatic));
}
#[test]
fn default_bond_is_single_when_either_end_is_not_aromatic() {
let m = parse_ok("[O-][N+](=O)c1ccccc1");
assert_eq!(m.bonds()[2].order, BondOrder::Single);
}
#[test]
fn direction_symbols_do_not_force_single_bond() {
let m = parse_ok("Cc1cs/c(=N\\C)/n1");
assert!(
m.bonds()
.iter()
.any(|b| b.order == BondOrder::Aromatic && b.direction != BondDirection::None),
"应存在既芳香又带方向的键"
);
}
#[test]
fn double_backslash_is_one_direction_bond() {
let a = parse_ok(r"F/C=C\F");
let b = parse_ok(r"F/C=C\\F");
assert_eq!(a.num_atoms(), b.num_atoms());
assert_eq!(a.num_bonds(), b.num_bonds());
assert_eq!(a.bonds()[2].direction, b.bonds()[2].direction);
}
#[test]
fn ring_bond_is_appended_last_with_closer_first() {
let m = parse_ok("C1CCCCC1");
let last = m.bonds()[5];
assert_eq!(
(last.begin, last.end),
(5, 0),
"环键端点应为 (闭合原子, 开环原子)"
);
}
#[test]
fn ring_bonds_sort_by_ring_number() {
let m = parse_ok("C2CC2C1CC1");
assert_eq!(m.num_bonds(), 7, "5 条链键 + 2 条环键");
let ring: Vec<(u32, u32)> = m.bonds()[m.num_bonds() - 2..]
.iter()
.map(|b| (b.begin, b.end))
.collect();
assert_eq!(ring, vec![(5, 3), (2, 0)]);
}
#[test]
fn explicit_order_at_ring_open_swaps_endpoints() {
assert_eq!(
(
parse_ok("C1CCCCC1").bonds()[5].begin,
parse_ok("C1CCCCC1").bonds()[5].end
),
(5, 0)
);
let m = parse_ok("C=1CCCCC1");
assert_eq!((m.bonds()[5].begin, m.bonds()[5].end), (0, 5));
let m = parse_ok("C/1CCCCC1");
assert_eq!((m.bonds()[5].begin, m.bonds()[5].end), (5, 0));
}
#[test]
fn multi_digit_ring_numbers() {
assert_eq!(parse_ok("C%10CCCCC%10").num_bonds(), 6);
assert_eq!(parse_ok("C%(123)CCCCC%(123)").num_bonds(), 6);
}
fn err_at(smi: &str) -> ParseError {
parse(smi).expect_err(&format!("{smi:?} 应当解析失败"))
}
#[test]
fn unclosed_ring_points_at_the_digit() {
let e = err_at("C1CC");
assert_eq!(e.kind, K::UnclosedRingBond(1));
assert_eq!(e.pos, 1, "应指向环标号所在位置");
assert!(e.render().contains('^'));
}
#[test]
fn ring_to_self() {
assert_eq!(err_at("C11").kind, K::RingBondToSelf(1));
}
#[test]
fn unbalanced_parens() {
assert_eq!(err_at("CC(").kind, K::UnbalancedParen);
assert_eq!(err_at("CC)").kind, K::UnbalancedParen);
}
#[test]
fn empty_branch() {
assert_eq!(err_at("CC()C").kind, K::EmptyBranch);
}
#[test]
fn dangling_bond() {
assert_eq!(err_at("C=").kind, K::DanglingBond);
assert_eq!(err_at("=C").kind, K::DanglingBond);
}
#[test]
fn unknown_element() {
assert!(matches!(err_at("[Xx]").kind, K::UnknownElement(_)));
}
#[test]
fn unclosed_bracket() {
assert_eq!(err_at("[C").kind, K::UnexpectedEnd);
}
#[test]
fn empty_input() {
assert_eq!(err_at("").kind, K::Empty);
}
#[test]
fn error_render_puts_caret_under_the_right_column() {
let e = err_at("CCC1CC");
let rendered = e.render();
let caret_line = rendered.lines().nth(1).unwrap();
assert_eq!(caret_line.find('^'), Some(e.pos), "插字号列号应等于 pos");
}
#[test]
fn dative_bond_direction_follows_the_arrow() {
let m = parse_ok("N->[Cu]");
let b = m.bonds()[0];
assert_eq!(
(b.begin, b.end, b.order),
(0, 1, BondOrder::Dative),
"N->[Cu]:给电子的 N 应在 begin"
);
let m = parse_ok("[Cu]<-N");
let b = m.bonds()[0];
assert_eq!(
(b.begin, b.end, b.order),
(1, 0, BondOrder::Dative),
"[Cu]<-N:给电子的 N 是后写的那个,端点要对调"
);
}
#[test]
fn single_bond_dash_is_not_swallowed_by_dative() {
let m = parse_ok("C-C");
assert_eq!(m.bonds()[0].order, BondOrder::Single);
assert_eq!(err_at("N-->[Cu]").kind, K::DanglingBond);
}
#[test]
fn lone_angle_bracket_is_an_error() {
assert_eq!(err_at("C<C").kind, K::UnexpectedChar('<'));
}
#[test]
fn dative_ring_closures() {
for (smi, expect) in [
("N->1CCCCC1", &[(0u32, 5u32)][..]),
("[Cu]<-1CCCC1", &[(4, 0)][..]),
("N->1CCCCC<-1", &[(0, 5)][..]),
("[Cu]1<-NCCN->1", &[(1, 0), (4, 0)][..]),
] {
let m = parse_ok(smi);
let mut got: Vec<(u32, u32)> = m
.bonds()
.iter()
.filter(|b| b.order == BondOrder::Dative)
.map(|b| (b.begin, b.end))
.collect();
got.sort_unstable();
let mut want = expect.to_vec();
want.sort_unstable();
assert_eq!(got, want, "{smi}");
}
}
#[test]
fn coordination_geometry_keeps_class_and_permutation() {
for (smi, tag, perm) in [
("[Pt@SP1](Cl)(Cl)(N)N", ChiralTag::SquarePlanar, 1u8),
("[Pt@SP3](Cl)(Cl)(N)N", ChiralTag::SquarePlanar, 3),
("F[P@TB15](Cl)(Br)(I)S", ChiralTag::TrigonalBipyramidal, 15),
("C[Co@OH25](N)(O)(S)(P)Cl", ChiralTag::Octahedral, 25),
] {
let m = parse_ok(smi);
let a = m
.atoms()
.iter()
.find(|a| a.chiral_tag != ChiralTag::Unspecified)
.unwrap_or_else(|| panic!("{smi}:应有立体标记"));
assert_eq!((a.chiral_tag, a.stereo_perm), (tag, perm), "{smi}");
}
}
#[test]
fn tetrahedral_leaves_permutation_zero() {
for (smi, tag) in [
("[C@](N)(O)(F)Cl", ChiralTag::Ccw),
("[C@@](N)(O)(F)Cl", ChiralTag::Cw),
("[C@TH1](N)(O)(F)Cl", ChiralTag::Ccw),
("[C@TH2](N)(O)(F)Cl", ChiralTag::Cw),
] {
let a = parse_ok(smi).atoms()[0];
assert_eq!((a.chiral_tag, a.stereo_perm), (tag, 0), "{smi}");
}
}
#[test]
fn allene_is_not_a_coordination_geometry() {
let m = parse_ok("N[C@AL1]=C=C(O)F");
assert_eq!(m.atoms()[1].chiral_tag, ChiralTag::Allene);
assert_eq!(m.atoms()[1].stereo_perm, 0);
let m = parse_ok("NC(Br)=[C@AL1]=C(O)F");
assert_eq!(m.atoms()[3].chiral_tag, ChiralTag::Allene);
assert_eq!(m.atoms()[3].stereo_perm, 1);
for (smi, want) in [("NC(Br)=[C@]=C(O)F", 1), ("NC(Br)=[C@@]=C(O)F", 2)] {
let m = parse_ok(smi);
assert_eq!(m.atoms()[3].chiral_tag, ChiralTag::Allene, "{smi}");
assert_eq!(m.atoms()[3].stereo_perm, want, "{smi}");
}
}
#[test]
fn coordination_permutation_is_normalised_to_storage_order() {
let m = parse_ok("[Co@OH5]1(N)(O)(S)(P)CCC1");
let a = m.atoms()[0];
assert_eq!(a.chiral_tag, ChiralTag::Octahedral, "类别要保住");
assert_eq!(a.stereo_perm, 11, "书写序 ≠ 存储序,序号要跟着换");
let m = parse_ok("[Co@OH5](N)(O)(S)(P)(C)F");
assert_eq!(m.atoms()[0].stereo_perm, 5);
let m = parse_ok("[Co@OH5](N)(O)(S)(P)C");
assert_eq!(m.atoms()[0].stereo_perm, 5, "对不上就原样保管");
}
#[test]
fn out_of_range_stereo_permutation_is_rejected() {
for (smi, geometry, got, max) in [
("[Pt@SP4](Cl)(Cl)(N)N", "SP", 4u32, 3u32),
("F[P@TB21](Cl)(Br)(I)S", "TB", 21, 20),
("C[Co@OH31](N)(O)(S)(P)Cl", "OH", 31, 30),
("[C@TH3](N)(O)(F)Cl", "TH", 3, 2),
("N[C@AL3]=C=C(O)F", "AL", 3, 2),
] {
assert_eq!(
err_at(smi).kind,
K::StereoPermOutOfRange { geometry, got, max },
"{smi} 应被拒绝"
);
}
assert_eq!(parse_ok("[Pt@SP0](Cl)(Cl)(N)N").atoms()[0].stereo_perm, 0);
assert_eq!(parse_ok("[Pt@SP](Cl)(Cl)(N)N").atoms()[0].stereo_perm, 0);
assert_eq!(
parse_ok("[Pt@SP](Cl)(Cl)(N)N").atoms()[0].chiral_tag,
ChiralTag::SquarePlanar
);
}
#[test]
fn conflicting_ring_bond_orders_are_rejected_on_purpose() {
for smi in [
"C=1CCCCC#1",
"N-1CCCCC<-1",
"N=1CCCCC<-1",
"N->1CCCCC-1",
"N->1CCCCC=1",
] {
assert_eq!(
err_at(smi).kind,
K::ConflictingRingBondOrder(1),
"{smi} 的两端键级矛盾,应当报错"
);
}
for smi in ["N->1CCCCC->1", "N->1CCCCC<-1", "C=1CCCCC=1"] {
let _ = parse_ok(smi);
}
}
#[test]
fn dative_ring_closure_flips_the_direction_too() {
for (smi, begin, end, dir) in [
("N/1CCCCC<-1", 0u32, 5u32, BondDirection::UpRight),
("N<-1CCCCC/1", 5, 0, BondDirection::UpRight),
("N\\1CCCCC<-1", 0, 5, BondDirection::DownRight),
] {
let m = parse_ok(smi);
let b = m
.bonds()
.iter()
.find(|b| b.order == BondOrder::Dative)
.unwrap_or_else(|| panic!("{smi}:应有一条配位键"));
assert_eq!((b.begin, b.end, b.direction), (begin, end, dir), "{smi}");
}
}
#[test]
fn parse_line_takes_name() {
let m = parse_line("CCO\tethanol").unwrap();
assert_eq!(m.num_atoms(), 3);
assert_eq!(m.name(), Some("ethanol"));
let m = parse_line("CCO").unwrap();
assert_eq!(m.name(), None);
}
}