Module:SMILES2IUPAC/Parser

.mw-parser-output .documentation,.mw-parser-output .documentation-metadata{border:1px solid var(--border-color-base,#a2a9b1);background-color:#ecfcf4;color:inhe

Module:SMILES2IUPAC/Parser
-- Module:SMILES2IUPAC/Parser
-- Parses a SMILES string into a plain graph:
--   { atoms = {...}, bonds = {...} }
--
-- PHASE 1 SUPPORT:
--   * organic-subset atoms without brackets:
--       B C N O P S F Cl Br I
--   * single/double/triple bonds:
--       - = #
--   * branches:
--       ( )
--
-- NOT YET SUPPORTED:
--   * bracket atoms [ ... ]
--   * ring closures / %nn
--   * aromatic atoms
--   * / \ : bond symbols
--
-- The parser is responsible only for converting SMILES into
-- a molecular graph. Naming rules belong in separate modules.

local Data = require('Module:SMILES2IUPAC/Data')

local p = {}

-- Bond orders used throughout the graph.
local BOND_ORDER = {
	single = 1,
	double = 2,
	triple = 3,

	['-'] = 1,
	['='] = 2,
	['#'] = 3
}

-- Try to read one atom symbol starting at position i.
-- Returns symbol, newIndex or nil.
local function readAtomSymbol(str, i)
	for _, sym in ipairs(Data.twoLetterSymbols) do
		if str:sub(i, i + #sym - 1) == sym then
			return sym, i + #sym
		end
	end

	local one = str:sub(i, i)

	if Data.organicSubset[one] then
		return one, i + 1
	end

	return nil, i
end

--- Parses a SMILES string.
-- @return graph table on success, or nil, errorMessage on failure.
function p.parse(smiles)
	if type(smiles) ~= 'string' or smiles == '' then
		return nil, 'empty SMILES string'
	end

	-- Strip surrounding whitespace.
	smiles = smiles:match('^%s*(.-)%s*$')

	if smiles == '' then
		return nil, 'empty SMILES string'
	end

	local atoms = {}
	local bonds = {}

	local pos = 1
	local len = #smiles

	-- Atom index to return to after a branch closes.
	local branchStack = {}

	local currentAtom = nil

	-- Bond order waiting to be applied to the next atom.
	local pendingBondOrder = nil

	-- Whether the previous token was an atom.
	-- This lets us validate where bond symbols and branches occur.
	local previousWasAtom = false

	while pos <= len do
		local c = smiles:sub(pos, pos)

		-- Branch opening.
		if c == '(' then
			if not currentAtom then
				return nil, ("branch '(' at position %d has no preceding atom"):format(pos)
			end

			if pendingBondOrder then
				return nil, ("branch '(' at position %d follows a dangling bond symbol"):format(pos)
			end

			table.insert(branchStack, currentAtom)

			previousWasAtom = false
			pos = pos + 1

		-- Branch closing.
		elseif c == ')' then
			if #branchStack == 0 then
				return nil, ("unmatched ')' at position %d"):format(pos)
			end

			if pendingBondOrder then
				return nil, ("branch ends after a dangling bond symbol at position %d"):format(pos)
			end

			currentAtom = table.remove(branchStack)

			previousWasAtom = true
			pos = pos + 1

		-- Explicit bond.
		elseif BOND_ORDER[c] then
			if not currentAtom then
				return nil, ("bond symbol '%s' at position %d has no preceding atom"):format(c, pos)
			end

			if pendingBondOrder then
				return nil, ("two bond symbols in a row at position %d"):format(pos)
			end

			pendingBondOrder = BOND_ORDER[c]

			previousWasAtom = false
			pos = pos + 1

		-- Unsupported stereochemical/aromatic bond symbols.
		elseif c:match('[/\\:]') then
			return nil, ("bond symbol '%s' (stereo/aromatic bond) not supported yet"):format(c)

		-- Unsupported bracket atoms.
		elseif c == '[' then
			return nil, 'bracket atoms ([...]) are not supported yet'

		-- Unsupported ring closures.
		elseif c:match('%d') or c == '%' then
			return nil, 'ring closures are not supported yet'

		-- Unsupported aromatic atoms.
		elseif c:match('[a-z]') then
			return nil, ("aromatic atom '%s' not supported yet"):format(c)

		-- Atom.
		else
			local sym, nextPos = readAtomSymbol(smiles, pos)

			if not sym then
				return nil, ("unrecognised character '%s' at position %d"):format(c, pos)
			end

			-- A new atom may follow another atom directly,
			-- in which case the bond is implicitly single.
			local newIdx = #atoms + 1

			table.insert(atoms, {
				element = sym
			})

			if currentAtom then
				local order = pendingBondOrder or BOND_ORDER.single

				table.insert(bonds, {
					a = currentAtom,
					b = newIdx,
					order = order
				})
			elseif pendingBondOrder then
				-- This should normally be caught by the bond-symbol
				-- validation above, but keep this guard here.
				return nil, 'bond has no starting atom'
			end

			pendingBondOrder = nil
			currentAtom = newIdx
			previousWasAtom = true
			pos = nextPos
		end
	end

	-- A branch must always be closed.
	if #branchStack > 0 then
		return nil, "unmatched '(' — branch never closed"
	end

	-- A bond symbol must always be followed by an atom.
	if pendingBondOrder then
		return nil, 'SMILES ends with a dangling bond symbol'
	end

	if #atoms == 0 then
		return nil, 'no atoms found'
	end

	return {
		atoms = atoms,
		bonds = bonds
	}
end

return p

Content Disclaimer

Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.

  1. The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
  2. There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
  3. It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
  4. Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
  5. Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.