Edge Rewrite
// HTMLRewriter · presentation

This page was redesigned at the edge.

Cloudflare fetched the original article and streamed it through HTMLRewriter to apply an entirely new visual system without rebuilding the source page.

// request.cf · coarse context

A page that knows where it met you.

Only coarse request metadata is shown. This demo does not display or persist visitor IP addresses.

Country
US
Cloudflare location
CMH
Connection
HTTP/2
Language
Not provided

Ray ID: a3ff15735a303564

Jump to content

Module:SMILES2IUPAC/Functional

From Wikipedia, the free encyclopedia
-- Module:SMILES2IUPAC/Functional
--
-- Functional-group naming for simple acyclic saturated compounds.
--
-- Supported:
--   alcohols
--   ethers
--   haloalkanes
--   alkyl hypochlorites

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

local p = {}

local halogens = {
	Cl = 'chloro',
	Br = 'bromo',
	F = 'fluoro',
	I = 'iodo'
}

local halogenElements = {
	chloro = true,
	bromo = true,
	fluoro = true,
	iodo = true
}

local function isHalogen(element)
	return halogens[element] ~= nil
end

local function getAtom(graph, index)
	return graph.atoms[index]
end

local function neighbours(graph, atomIndex)
	local result = {}

	for _, bond in ipairs(graph.bonds) do
		if bond.a == atomIndex then
			table.insert(result, bond.b)
		elseif bond.b == atomIndex then
			table.insert(result, bond.a)
		end
	end

	return result
end

local function heavyNeighbours(graph, atomIndex)
	local result = {}

	for _, node in ipairs(neighbours(graph, atomIndex)) do
		if getAtom(graph, node).element ~= 'H' then
			table.insert(result, node)
		end
	end

	return result
end

local function getCarbons(graph)
	local result = {}

	for i, atom in ipairs(graph.atoms) do
		if atom.element == 'C' then
			table.insert(result, i)
		end
	end

	return result
end

local function pathSet(path)
	local result = {}

	for _, node in ipairs(path) do
		result[node] = true
	end

	return result
end

local function inPath(path, node)
	for _, n in ipairs(path) do
		if n == node then
			return true
		end
	end

	return false
end

local function getRoot(n)
	if Data.chainRoot and Data.chainRoot[n] then
		return Data.chainRoot[n]
	end

	if Data.chainExceptions and Data.chainExceptions[n] then
		return Data.chainExceptions[n]
	end

	return nil
end

----------------------------------------------------------------------
-- Find a longest chain containing a required carbon
----------------------------------------------------------------------

local function longestChainContaining(carbons, adj, required)
	local candidates = Chain.findLongestChains(carbons, adj)
	local best = nil

	for _, chain in ipairs(candidates) do
		if inPath(chain, required) then
			if not best or #chain > #best then
				best = chain
			end
		end
	end

	return best
end

----------------------------------------------------------------------
-- Orient a chain so the functional group gets the lowest locant
----------------------------------------------------------------------

local function orientPathForNode(path, node)
	local position = nil

	for i, n in ipairs(path) do
		if n == node then
			position = i
			break
		end
	end

	if not position then
		return path, nil
	end

	local reversePosition = #path - position + 1

	if reversePosition < position then
		local reversed = {}

		for i = #path, 1, -1 do
			table.insert(reversed, path[i])
		end

		return reversed, reversePosition
	end

	return path, position
end

----------------------------------------------------------------------
-- Prefix helpers
----------------------------------------------------------------------

local function multiplierForCount(count)
	if count == 2 then
		return 'di'
	elseif count == 3 then
		return 'tri'
	elseif count == 4 then
		return 'tetra'
	end

	return ''
end

local function buildSubstituentPrefix(name, locants)
	if not locants or #locants == 0 then
		return nil
	end

	table.sort(locants)

	local values = {}

	for _, locant in ipairs(locants) do
		table.insert(values, tostring(locant))
	end

	return table.concat(values, ',')
		.. '-'
		.. multiplierForCount(#locants)
		.. name
end

local function buildSubstituentPrefixList(substituents)
	local names = {}

	for name in pairs(substituents) do
		table.insert(names, name)
	end

	table.sort(names)

	local parts = {}

	for _, name in ipairs(names) do
		local part = buildSubstituentPrefix(
			name,
			substituents[name]
		)

		if part then
			table.insert(parts, part)
		end
	end

	return parts
end

----------------------------------------------------------------------
-- Carbon substituents
----------------------------------------------------------------------

local function collectCarbonSubstituents(graph, path, adj)
	local result = {}
	local pathNodes = pathSet(path)

	for index, carbon in ipairs(path) do
		for _, neighbour in ipairs(adj[carbon] or {}) do
			if not pathNodes[neighbour] then
				if getAtom(graph, neighbour).element == 'C' then
					local subtree = Chain.substituentSubtree(
						adj,
						neighbour,
						pathNodes
					)

					if #subtree == 1 then
						if not result.methyl then
							result.methyl = {}
						end

						table.insert(result.methyl, index)
					end
				end
			end
		end
	end

	return result
end

----------------------------------------------------------------------
-- Halogen substituents
----------------------------------------------------------------------

local function collectHalogenSubstituents(graph, path)
	local result = {}

	for index, carbon in ipairs(path) do
		for _, atomIndex in ipairs(neighbours(graph, carbon)) do
			local atom = getAtom(graph, atomIndex)

			if isHalogen(atom.element) then
				local name = halogens[atom.element]

				if not result[name] then
					result[name] = {}
				end

				table.insert(result[name], index)
			end
		end
	end

	return result
end

local function mergeSubstituents(target, source)
	for name, locants in pairs(source) do
		if not target[name] then
			target[name] = {}
		end

		for _, locant in ipairs(locants) do
			table.insert(target[name], locant)
		end
	end
end

----------------------------------------------------------------------
-- ALCOHOL
----------------------------------------------------------------------

local function nameAlcohol(graph, carbons, adj)
	local oxygen = nil

	for i, atom in ipairs(graph.atoms) do
		if atom.element == 'O' then
			oxygen = i
			break
		end
	end

	if not oxygen then
		return nil
	end

	local oxygenNeighbours = heavyNeighbours(graph, oxygen)

	-- Alcohol: C-O-H
	if #oxygenNeighbours ~= 1 then
		return nil
	end

	local alcoholCarbon = oxygenNeighbours[1]

	if getAtom(graph, alcoholCarbon).element ~= 'C' then
		return nil
	end

	local chain = longestChainContaining(
		carbons,
		adj,
		alcoholCarbon
	)

	if not chain then
		return nil
	end

	chain, alcoholLocant = orientPathForNode(
		chain,
		alcoholCarbon
	)

	local length = #chain
	local root = getRoot(length)

	if not root then
		return nil
	end

	local substituents = collectCarbonSubstituents(
		graph,
		chain,
		adj
	)

	mergeSubstituents(
		substituents,
		collectHalogenSubstituents(
			graph,
			chain
		)
	)

	local parts = buildSubstituentPrefixList(substituents)

	if length == 1 and #parts == 0 then
		return 'methanol'
	end

	if length == 2 and #parts == 0 then
		return 'ethanol'
	end

	local name = root
		.. 'an-'
		.. tostring(alcoholLocant)
		.. '-ol'

	if #parts > 0 then
		name = table.concat(parts, '-') .. name
	end

	return name
end

----------------------------------------------------------------------
-- HALOALKANE
----------------------------------------------------------------------

local function nameHaloalkane(graph, carbons, adj)
	local hasHalogen = false

	for _, atom in ipairs(graph.atoms) do
		if isHalogen(atom.element) then
			hasHalogen = true
		elseif atom.element ~= 'C'
			and atom.element ~= 'H'
		then
			return nil
		end
	end

	if not hasHalogen then
		return nil
	end

	local halogenCarbons = {}

	for i, atom in ipairs(graph.atoms) do
		if isHalogen(atom.element) then
			for _, neighbour in ipairs(neighbours(graph, i)) do
				if getAtom(graph, neighbour).element == 'C' then
					halogenCarbons[neighbour] = true
				end
			end
		end
	end

	if not next(halogenCarbons) then
		return nil
	end

	local candidates = Chain.findLongestChains(carbons, adj)

	if #candidates == 0 then
		return nil
	end

	local best = nil
	local bestHalogenCount = -1
	local bestLocants = nil

	for _, candidate in ipairs(candidates) do
		local halogenCount = 0

		for _, carbon in ipairs(candidate) do
			if halogenCarbons[carbon] then
				halogenCount = halogenCount + 1
			end
		end

		-- Scribunto Lua has no "continue", so simply skip
		-- candidates with no halogen by wrapping the rest.
		if halogenCount > 0 then

			local function halogenLocants(reverse)
				local locants = {}

				for i, carbon in ipairs(candidate) do
					local locant = i

					if reverse then
						locant = #candidate - i + 1
					end

					for _, atomIndex in ipairs(neighbours(graph, carbon)) do
						if isHalogen(
							getAtom(graph, atomIndex).element
						) then
							table.insert(locants, locant)
						end
					end
				end

				table.sort(locants)
				return locants
			end

			local forward = halogenLocants(false)
			local backward = halogenLocants(true)
			local locants = forward

			if Chain.compareLocantLists(
				backward,
				forward
			) < 0 then
				locants = backward
			end

			if halogenCount > bestHalogenCount
				or (
					halogenCount == bestHalogenCount
					and (
						not bestLocants
						or Chain.compareLocantLists(
							locants,
							bestLocants
						) < 0
					)
				)
			then
				best = candidate
				bestHalogenCount = halogenCount
				bestLocants = locants
			end
		end
	end

	if not best then
		return nil
	end

	local chain = best
	local length = #chain
	local root = getRoot(length)

	if not root then
		return nil
	end

	------------------------------------------------------------------
	-- Choose numbering direction.
	------------------------------------------------------------------

	local function getHalogenLocants(reverse)
		local locants = {}

		for i, carbon in ipairs(chain) do
			local locant = i

			if reverse then
				locant = length - i + 1
			end

			for _, atomIndex in ipairs(neighbours(graph, carbon)) do
				if isHalogen(
					getAtom(graph, atomIndex).element
				) then
					table.insert(locants, locant)
				end
			end
		end

		table.sort(locants)
		return locants
	end

	local forwardHalogens = getHalogenLocants(false)
	local reverseHalogens = getHalogenLocants(true)

	local reversed = false

	if Chain.compareLocantLists(
		reverseHalogens,
		forwardHalogens
	) < 0 then
		reversed = true
	end

	------------------------------------------------------------------
	-- Collect substituents.
	------------------------------------------------------------------

	local substituents = {}

	for i, carbon in ipairs(chain) do
		local locant = i

		if reversed then
			locant = length - i + 1
		end

		for _, atomIndex in ipairs(neighbours(graph, carbon)) do
			local atom = getAtom(graph, atomIndex)

			if isHalogen(atom.element) then
				local name = halogens[atom.element]

				if not substituents[name] then
					substituents[name] = {}
				end

				table.insert(
					substituents[name],
					locant
				)
			end
		end

		for _, branch in ipairs(adj[carbon] or {}) do
			if not inPath(chain, branch) then
				local subtree = Chain.substituentSubtree(
					adj,
					branch,
					pathSet(chain)
				)

				if #subtree == 1 then
					if not substituents.methyl then
						substituents.methyl = {}
					end

					table.insert(
						substituents.methyl,
						locant
					)
				end
			end
		end
	end

	------------------------------------------------------------------
	-- Methane/ethane halogen names do not require locants.
	----------------------------------------------------------------------

	if length <= 2 then
		local parts = {}

		for name, locants in pairs(substituents) do
			if halogenElements[name]
				and #locants == 1
			then
				table.insert(parts, name)
			else
				local part = buildSubstituentPrefix(
					name,
					locants
				)

				if part then
					table.insert(parts, part)
				end
			end
		end

		table.sort(parts)

		return table.concat(parts, '-')
			.. root
			.. 'ane'
	end

	local parts = buildSubstituentPrefixList(
		substituents
	)

	if #parts == 0 then
		return root .. 'ane'
	end

	return table.concat(parts, '-')
		.. root
		.. 'ane'
end

----------------------------------------------------------------------
-- ETHER
----------------------------------------------------------------------

local function nameEther(graph)
	local oxygen = nil

	for i, atom in ipairs(graph.atoms) do
		if atom.element == 'O' then
			oxygen = i
			break
		end
	end

	if not oxygen then
		return nil
	end

	local oxygenNeighbours = heavyNeighbours(graph, oxygen)

	-- Ether: C-O-C
	if #oxygenNeighbours ~= 2 then
		return nil
	end

	if getAtom(
		graph,
		oxygenNeighbours[1]
	).element ~= 'C'
		or getAtom(
			graph,
			oxygenNeighbours[2]
		).element ~= 'C'
	then
		return nil
	end

	local function countSide(start)
		local visited = {
			[oxygen] = true
		}

		local queue = { start }
		local qi = 1
		local count = 0

		while qi <= #queue do
			local current = queue[qi]
			qi = qi + 1

			if not visited[current] then
				visited[current] = true

				if getAtom(
					graph,
					current
				).element ~= 'C'
				then
					return nil
				end

				count = count + 1

				for _, neighbour in ipairs(
					neighbours(graph, current)
				) do
					if not visited[neighbour]
						and neighbour ~= oxygen
					then
						local element =
							getAtom(
								graph,
								neighbour
							).element

						if element == 'C' then
							table.insert(
								queue,
								neighbour
							)
						elseif element ~= 'H' then
							return nil
						end
					end
				end
			end
		end

		return count
	end

	local left = countSide(
		oxygenNeighbours[1]
	)

	local right = countSide(
		oxygenNeighbours[2]
	)

	if not left or not right then
		return nil
	end

	local larger = math.max(left, right)
	local smaller = math.min(left, right)

	local largerRoot = getRoot(larger)
	local smallerRoot = getRoot(smaller)

	if not largerRoot or not smallerRoot then
		return nil
	end

	return smallerRoot
		.. 'oxy'
		.. largerRoot
		.. 'ane'
end

----------------------------------------------------------------------
-- ALKYL HYPOCHLORITE
----------------------------------------------------------------------

local function nameHypochlorite(graph)
	local oxygen = nil

	for i, atom in ipairs(graph.atoms) do
		if atom.element == 'O' then
			oxygen = i
			break
		end
	end

	if not oxygen then
		return nil
	end

	local oxygenNeighbours = heavyNeighbours(
		graph,
		oxygen
	)

	-- R-O-Cl
	if #oxygenNeighbours ~= 2 then
		return nil
	end

	local carbon = nil
	local chlorine = nil

	for _, neighbour in ipairs(oxygenNeighbours) do
		local element =
			getAtom(
				graph,
				neighbour
			).element

		if element == 'C' then
			if carbon then
				return nil
			end

			carbon = neighbour

		elseif element == 'Cl' then
			if chlorine then
				return nil
			end

			chlorine = neighbour

		else
			return nil
		end
	end

	if not carbon or not chlorine then
		return nil
	end

	local visited = {
		[oxygen] = true
	}

	local queue = { carbon }
	local qi = 1
	local count = 0

	while qi <= #queue do
		local current = queue[qi]
		qi = qi + 1

		if not visited[current] then
			visited[current] = true

			if getAtom(
				graph,
				current
			).element ~= 'C'
			then
				return nil
			end

			count = count + 1

			for _, neighbour in ipairs(
				neighbours(graph, current)
			) do
				if not visited[neighbour]
					and neighbour ~= oxygen
				then
					local element =
						getAtom(
							graph,
							neighbour
						).element

					if element == 'C' then
						table.insert(
							queue,
							neighbour
						)
					elseif element ~= 'H' then
						return nil
					end
				end
			end
		end
	end

	local root = getRoot(count)

	if not root then
		return nil
	end

	return root .. 'yl hypochlorite'
end

----------------------------------------------------------------------
-- MAIN
----------------------------------------------------------------------

function p.name(graph)

	-- Functional module only handles single bonds.
	for _, bond in ipairs(graph.bonds) do
		if bond.order ~= 1 then
			return nil,
				'double/triple bonds present'
		end
	end

	local carbons = getCarbons(graph)

	if #carbons == 0 then
		return nil, 'no carbon skeleton'
	end

	local adjacency = {}

	for _, carbon in ipairs(carbons) do
		adjacency[carbon] = {}
	end

	for _, bond in ipairs(graph.bonds) do
		if adjacency[bond.a]
			and adjacency[bond.b]
		then
			table.insert(
				adjacency[bond.a],
				bond.b
			)

			table.insert(
				adjacency[bond.b],
				bond.a
			)
		end
	end

	------------------------------------------------------------------
	-- Functional groups are checked BEFORE carbon-tree validation.
	--
	-- This is important for ethers such as CCOCC because the oxygen
	-- separates the carbon graph into two components.
	----------------------------------------------------------------------

	local alcohol = nameAlcohol(
		graph,
		carbons,
		adjacency
	)

	if alcohol then
		return alcohol
	end

	local hypochlorite = nameHypochlorite(graph)

	if hypochlorite then
		return hypochlorite
	end

	local ether = nameEther(graph)

	if ether then
		return ether
	end

	------------------------------------------------------------------
	-- Ordinary carbon skeleton.
	----------------------------------------------------------------------

	local isTree, err = Chain.isTree(
		carbons,
		adjacency
	)

	if not isTree then
		return nil,
			err .. ' — rings not supported'
	end

	local halo = nameHaloalkane(
		graph,
		carbons,
		adjacency
	)

	if halo then
		return halo
	end

	return nil,
		'functional group not supported yet'
end

return p