Модуль:Химия

Материал из свободной русской энциклопедии «Традиция»
Перейти к: навигация, поиск

Модуль для расчётов, касающихся физической химии.

Функции:

группа 
По атомному номеру элемента возвращает группу по ИЮПАК, которой он принадлежит
молярная масса 
Функция для вывода молярной массы вещества по формуле
оформить формулу 
Функция для оформления химической формулы
период 
По атомному номеру элемента возвращает период периодической системы, которому он принадлежит
электронная конфигурация 
Функция для вывода электронной конфигурации элемента с данным атомным номером

local m = {}
 
local chemistryData = {
 
    -- API:
    apiElectronicConfiguration = "электронная конфигурация",
    argAtomicNumber            = 1,
    argStart                   = 2,
    argShells                  = 3,
    argSep                     = "разделитель",

    apiMolarMass               = "молярная масса",
    argFormula                 = 1,

    apiPeriod                  = "период",
    apiGroup                   = "группа",
    
    apiShowFormula             = "оформить формулу",
    argSort                    = "sort",
    argCharge                  = "charge",

    -- Ошибки:
    errorMissingNumber    = "Передайте атомный номер",
    errorNonNumeric       = "Все параметры должны быть числами",
    errorNonPositive      = "Атомный номер должен быть положителен",
    errorNonInteger       = "Все параметры должны быть целыми",
    errorShellsNegative   = "Число показываемых оболочек должно быть неотрицательным",
    errorMissingFormula   = "Передайте химическую формулу",
    errorWrongFormula     = "Недопустимый символ в химической формуле",
    errorUnknownElement   = "Неизвестный элемент",
    errorTooManyOP        = "Слишком много открывающих скобок в формуле",
    errorTooManyCP        = "Слишком много закрывающих скобок в формуле",
    errorNonNumericAtoms  = "Количество атомов должно быть числом",
    errorNonPositiveAtoms = "Количество атомов должно быть положительно",
    errorUnexpectedNumber = "Здесь не ожидается число",
    errorWrongCharge      = "Неверное значение заряда иона",
 
    -- Орбитальные квантовые числа:
    l = { [0] = "s"
        , [1] = "p"
        , [2] = "d"
        , [3] = "f"
        , [4] = "g"
        , [5] = "h"
        , [6] = "i" },

    -- Исключения из правила Клечковского (разреженные массивы с перемещениями электронов):
    exceptions = {
     -- Cr, Cu, Nb, Mo, Ru, Rh, Pd, Ag, Pt, Au (6s -> 5d):
        [24] = {[5] = {[2] = 1}, [6] = {[0] = -1}} -- Cr
      , [29] = {[5] = {[2] = 1}, [6] = {[0] = -1}} -- Cu
      , [41] = {[5] = {[2] = 1}, [6] = {[0] = -1}} -- Nb
      , [42] = {[5] = {[2] = 1}, [6] = {[0] = -1}} -- Mo
      , [44] = {[5] = {[2] = 1}, [6] = {[0] = -1}} -- Ru
      , [45] = {[5] = {[2] = 1}, [6] = {[0] = -1}} -- Rh
      , [46] = {[5] = {[2] = 1}, [6] = {[0] = -1}} -- Pd
      , [47] = {[5] = {[2] = 1}, [6] = {[0] = -1}} -- Ag
      , [78] = {[5] = {[2] = 1}, [6] = {[0] = -1}} -- Pt 
      , [79] = {[5] = {[2] = 1}, [6] = {[0] = -1}} -- Au
     -- Ac, Th, Pa, U, Np, Cm (5f -> 6d):
      , [89] = {[6] = {[2] = 1}, [5] = {[3] = -1}} -- Ac
      , [90] = {[6] = {[2] = 2}, [5] = {[3] = -2}} -- Th
      , [91] = {[6] = {[2] = 1}, [5] = {[3] = -1}} -- Pa
      , [92] = {[6] = {[2] = 1}, [5] = {[3] = -1}} -- U
      , [93] = {[6] = {[2] = 1}, [5] = {[3] = -1}} -- Np
      , [96] = {[6] = {[2] = 1}, [5] = {[3] = -1}} -- Cm
     -- Lr
      ,[103] = {[7] = {[1] = 1}, [6] = {[2] = -1}} -- Lr
    } ,

    -- Атомные массы, названия, цвета и порядок вывода (позже брать из БД):
    elements = {
         ["H"] = {["mass"] = 1.00794, ["name"] = "Водород", ["color"] = "777", ["order"] = 120}
       , ["He"] = {["mass"] = 4.002602, ["name"] = "Гелий", ["color"] = "30602e", ["order"] = 130}
       , ["Li"] = {["mass"] = 6.941, ["name"] = "Литий", ["color"] = "800080", ["order"] = 140}
       , ["Be"] = {["mass"] = 9.012182, ["name"] = "Бериллий", ["color"] = "30602e", ["order"] = 150}
       , ["B"] = {["mass"] = 10.811, ["name"] = "Бор (элемент)", ["color"] = "30602e", ["order"] = 160}
       , ["C"] = {["mass"] = 12.0107, ["name"] = "Углерод", ["color"] = "111", ["order"] = 110}
       , ["N"] = {["mass"] = 14.00674, ["name"] = "Азот", ["color"] = "0000cc", ["order"] = 170}
       , ["O"] = {["mass"] = 15.9994, ["name"] = "Кислород", ["color"] = "f00000", ["order"] = 180}
       , ["F"] = {["mass"] = 18.9984032, ["name"] = "Фтор", ["color"] = "30602e", ["order"] = 190}
       , ["Ne"] = {["mass"] = 20.1797, ["name"] = "Неон", ["color"] = "30602e", ["order"] = 200}
       , ["Na"] = {["mass"] = 22.98976928, ["name"] = "Натрий", ["color"] = "800080", ["order"] = 330}
       , ["Mg"] = {["mass"] = 24.3050, ["name"] = "Магний", ["color"] = "4E1563", ["order"] = 420}
       , ["Al"] = {["mass"] = 26.9815386, ["name"] = "Алюминий", ["color"] = "800080", ["order"] = 380}
       , ["Si"] = {["mass"] = 28.0855, ["name"] = "Кремний", ["color"] = "694E19", ["order"] = 430}
       , ["P"] = {["mass"] = 30.973762, ["name"] = "Фосфор", ["color"] = "d2701e", ["order"] = 220}
       , ["S"] = {["mass"] = 32.066, ["name"] = "Сера", ["color"] = "ff8800", ["order"] = 210}
       , ["Cl"] = {["mass"] = 35.4527, ["name"] = "Хлор", ["color"] = "228b22", ["order"] = 230}
       , ["Ar"] = {["mass"] = 39.948, ["name"] = "Аргон", ["color"] = "30602e", ["order"] = 205}
       , ["K"] = {["mass"] = 39.0983, ["name"] = "Калий", ["color"] = "48206A", ["order"] = 310}
       , ["Ca"] = {["mass"] = 40.078, ["name"] = "Кальций", ["color"] = "48206A", ["order"] = 315}
       , ["Sc"] = {["mass"] = 44.955912, ["order"] = 1021}
       , ["Ti"] = {["mass"] = 47.867, ["name"] = "Титан (элемент)", ["color"] = "800080", ["order"] = 410}
       , ["V"] = {["mass"] = 50.9415, ["order"] = 1023}
       , ["Cr"] = {["mass"] = 51.9961, ["name"] = "Криптон", ["color"] = "48206A", ["order"] = 207}
       , ["Mn"] = {["mass"] = 54.938045, ["name"] = "Марганец", ["color"] = "4E1563", ["order"] = 320}
       , ["Fe"] = {["mass"] = 55.845, ["name"] = "Железо", ["color"] = "800080", ["order"] = 400}
       , ["Co"] = {["mass"] = 58.933195, ["name"] = "Кобальт", ["color"] = "353A5A", ["order"] = 270}
       , ["Ni"] = {["mass"] = 58.6934, ["order"] = 1028}
       , ["Cu"] = {["mass"] = 63.546, ["name"] = "Медь", ["color"] = "800080", ["order"] = 280}
       , ["Zn"] = {["mass"] = 65.39, ["name"] = "Цинк", ["color"] = "800080", ["order"] = 390}
       , ["Ga"] = {["mass"] = 69.723, ["order"] = 1031}
       , ["Ge"] = {["mass"] = 72.61, ["order"] = 1032}
       , ["As"] = {["mass"] = 74.92160, ["name"] = "Мышьяк", ["color"] = "654778", ["order"] = 250}
       , ["Se"] = {["mass"] = 78.96, ["name"] = "Селен", ["color"] = "694E19", ["order"] = 350}
       , ["Br"] = {["mass"] = 79.904, ["name"] = "Бром", ["color"] = "8b4513", ["order"] = 240}
       , ["Kr"] = {["mass"] = 83.80, ["order"] = 1036}
       , ["Rb"] = {["mass"] = 85.4678, ["order"] = 1037}
       , ["Sr"] = {["mass"] = 87.62, ["name"] = "Стронций", ["color"] = "0E430E", ["order"] = 360}
       , ["Y"] = {["mass"] = 88.90585, ["order"] = 1039}
       , ["Zr"] = {["mass"] = 91.224, ["order"] = 1040}
       , ["Nb"] = {["mass"] = 92.90638, ["order"] = 1041}
       , ["Mo"] = {["mass"] = 95.94, ["order"] = 1042}
       , ["Tc"] = {["mass"] = 97.9072, ["name"] = "Технеций", ["color"] = "1E4F54", ["order"] = 370}
       , ["Ru"] = {["mass"] = 101.07, ["order"] = 1044}
       , ["Rh"] = {["mass"] = 102.90550, ["order"] = 1045}
       , ["Pd"] = {["mass"] = 106.42, ["order"] = 1046}
       , ["Ag"] = {["mass"] = 107.8682, ["order"] = 1047}
       , ["Cd"] = {["mass"] = 112.411, ["order"] = 1048}
       , ["In"] = {["mass"] = 114.818, ["order"] = 1049}
       , ["Sn"] = {["mass"] = 118.710, ["order"] = 1050}
       , ["Sb"] = {["mass"] = 121.760, ["order"] = 1051}
       , ["Te"] = {["mass"] = 127.60, ["order"] = 1052}
       , ["I"] = {["mass"] = 126.90447, ["name"] = "Иод", ["color"] = "4A0B4A", ["order"] = 300}
       , ["Xe"] = {["mass"] = 131.29, ["order"] = 1054}
       , ["Cs"] = {["mass"] = 132.9054519, ["order"] = 1055}
       , ["Ba"] = {["mass"] = 137.327, ["order"] = 1056}
       , ["Hf"] = {["mass"] = 178.49, ["order"] = 1057}
       , ["Ta"] = {["mass"] = 180.94788, ["order"] = 1058}
       , ["W"] = {["mass"] = 183.84, ["order"] = 1059}
       , ["Re"] = {["mass"] = 186.207, ["order"] = 1060}
       , ["Os"] = {["mass"] = 190.23, ["order"] = 1061}
       , ["Ir"] = {["mass"] = 192.217, ["order"] = 1062}
       , ["Pt"] = {["mass"] = 195.084, ["name"] = "Платина", ["color"] = "15354F", ["order"] = 340}
       , ["Au"] = {["mass"] = 196.966569, ["name"] = "Золото", ["color"] = "800080", ["order"] = 260}
       , ["Hg"] = {["mass"] = 200.59, ["order"] = 1065}
       , ["Tl"] = {["mass"] = 204.3833, ["order"] = 1066}
       , ["Pb"] = {["mass"] = 207.2, ["order"] = 1067}
       , ["Bi"] = {["mass"] = 208.98040, ["order"] = 1068}
       , ["Po"] = {["mass"] = 208.9824, ["order"] = 1069}
       , ["At"] = {["mass"] = 209.9871, ["order"] = 1070}
       , ["Rn"] = {["mass"] = 222.0176, ["order"] = 1071}
       , ["La"] = {["mass"] = 138.90547, ["order"] = 1072}
       , ["Ce"] = {["mass"] = 140.116, ["order"] = 1073}
       , ["Pr"] = {["mass"] = 140.90765, ["order"] = 1074}
       , ["Nd"] = {["mass"] = 144.242, ["order"] = 1075}
       , ["Pm"] = {["mass"] = 144.9127, ["order"] = 1076}
       , ["Sm"] = {["mass"] = 150.36, ["order"] = 1077}
       , ["Eu"] = {["mass"] = 151.964, ["order"] = 1078}
       , ["Gd"] = {["mass"] = 157.25, ["name"] = "Гадолиний", ["color"] = "400040", ["order"] = 290}
       , ["Tb"] = {["mass"] = 158.92535, ["order"] = 1080}
       , ["Dy"] = {["mass"] = 162.500, ["order"] = 1081}
       , ["Ho"] = {["mass"] = 164.93032, ["order"] = 1082}
       , ["Er"] = {["mass"] = 167.259, ["order"] = 1083}
       , ["Tm"] = {["mass"] = 168.93421, ["order"] = 1084}
       , ["Yb"] = {["mass"] = 173.04, ["order"] = 1085}
       , ["Lu"] = {["mass"] = 174.967, ["order"] = 1086}
       , ["Fr"] = {["mass"] = 223.0197, ["order"] = 1087}
       , ["Ra"] = {["mass"] = 226.0254, ["order"] = 1088}
       , ["Rf"] = {["mass"] = 263.1125, ["order"] = 1089}
       , ["Db"] = {["mass"] = 262.1144, ["order"] = 1090}
       , ["Sg"] = {["mass"] = 266.1219, ["order"] = 1091}
       , ["Bh"] = {["mass"] = 264.1247, ["order"] = 1092}
       , ["Hs"] = {["mass"] = 269.1341, ["order"] = 1093}
       , ["Mt"] = {["mass"] = 268.1388, ["order"] = 1094}
       , ["Ds"] = {["mass"] = 272.1463, ["order"] = 1095}
       , ["Rg"] = {["mass"] = 272.1535, ["order"] = 1096}
       , ["Cn"] = {["mass"] = 277.0, ["order"] = 1097}
       , ["Uut"] = {["mass"] = 284.0, ["order"] = 1098}
       , ["Fl"] = {["mass"] = 289.0, ["order"] = 1099}
       , ["Uup"] = {["mass"] = 288.0, ["order"] = 1100}
       , ["Lv"] = {["mass"] = 292.0, ["order"] = 1101}
       , ["Uus"] = {["mass"] = 292.0, ["order"] = 1102}
       , ["Uuo"] = {["mass"] = 101.3, ["order"] = 1103}
       , ["Ac"] = {["mass"] = 227.0277, ["order"] = 1104}
       , ["Th"] = {["mass"] = 232.03806, ["order"] = 1105}
       , ["Pa"] = {["mass"] = 231.03588, ["order"] = 1106}
       , ["U"] = {["mass"] = 238.02891, ["order"] = 1107}
       , ["Np"] = {["mass"] = 237.0482, ["order"] = 1108}
       , ["Pu"] = {["mass"] = 244.0642, ["order"] = 1109}
       , ["Am"] = {["mass"] = 243.0614, ["order"] = 1110}
       , ["Cm"] = {["mass"] = 247.0703, ["order"] = 1111}
       , ["Bk"] = {["mass"] = 247.0703, ["order"] = 1112}
       , ["Cf"] = {["mass"] = 251.0796, ["order"] = 1113}
       , ["Es"] = {["mass"] = 252.0830, ["order"] = 1114}
       , ["Fm"] = {["mass"] = 257.0951, ["order"] = 1115}
       , ["Md"] = {["mass"] = 258.0984, ["order"] = 1116}
       , ["No"] = {["mass"] = 259.1011, ["order"] = 1117}
       , ["Lr"] = {["mass"] = 262.110, ["order"] = 1118}
    }

  , T_ELEM = 0  -- token types
  , T_NUM = 1
  , T_O = 2    -- open '('
  , T_C = 3    -- close ')'
}
 
local function formatChemistryError (message, ...)
    if select('#', ... ) > 0 then
        message = string.format (message, ...)
    end
    return "<span class=\"error\">" .. message .. "</span>"
end
 
-- Получение и санация параметров функций «электронная конфигурация», «период» и «группа»:
-- 1: Атомный номер:
local function loadAtomicNumber (no)
    if not no then
        return false, formatChemistryError (chemistryData.errorMissingNumber)
    end
    local result = tonumber (no)
    if not result then
        return false, formatChemistryError (chemistryData.errorNonNumeric, no)
    end
    if math.floor (no) ~= result then
        return false, formatChemistryError (chemistryData.errorNonInteger, no)
    end
    if result <= 0 then
        return false, formatChemistryError (chemistryData.errorNonPositive, no)
    end
 
    return true, result
end

-- 2: Начать с оболочки:
local function loadStart (no)
    -- по умолчанию, 1:
    if not no or no == 0 then
        return true, 1
    end
    local result = tonumber (no)
    if not result then
        return false, formatChemistryError (chemistryData.errorNonNumeric, no)
    end
    if math.floor (no) ~= result then
        return false, formatChemistryError (chemistryData.errorNonInteger, no)
    end
 
    return true, result
end

-- 3: Число показываемых оболочек:
local function loadShells (no)
    -- по умолчанию, 0:
    if not no then
        return true, 0
    end
    local result = tonumber (no)
    if not result then
        return false, formatChemistryError (chemistryData.errorNonNumeric, no)
    end
    if math.floor (no) ~= result then
        return false, formatChemistryError (chemistryData.errorNonInteger, no)
    end
    if result < 0 then
        return false, formatChemistryError (chemistryData.errorShellsNegative, no)
    end
 
    return true, result
end

-- разделитель слоёв:
local function loadSep (str)
    -- по умолчанию, пустая строка:
    if not str then
        return true, ""
    else
        return true, str
    end
end

-- Период и группа элемента:
-- Период и группа (по ИЮПАК, 1988). Параметр -- санированный атомный номер:
local function implementPeriodAndGroup (no)
    local period = 0
    local group
    local n = no
    while n > 0 do
        period = period + 1
        -- Предполагаемая группа:
        group = n
        -- Число элементов в периоде :
        n = n - 2 * (math.floor (period / 2) + 1) ^ 2
    end

    if period == 1 and group == 2 then
        group = 18             -- He.
    elseif period == 2 or period == 3 then
        if group > 2 then
            group = group + 10 -- нет d-электронов.
        end
    elseif period == 6 or period == 7 then
        -- Все лантаноиды и актиноиды в одной группе 3:
        if group > 3 and group <= 17 then
            group = 3
        elseif group > 17 then
            group = group - 14
        end
    end

    return period, group
end

-- Функция для периода и группы до санации параметра:
local function periodAndGroup (args)
    -- Извлечение параметра «Атомный номер» (первого):
    local ok
    local number
    ok, number = loadAtomicNumber (args [chemistryData.argAtomicNumber])
    if not ok then
        return number
    end
    return implementPeriodAndGroup (number)
end

-- Регистрация эспортируемых функций:
-- «период»:
m [chemistryData.apiPeriod] = function (frame)
    local period, group = periodAndGroup (frame.args)
    return period
end

-- «группа»:
m [chemistryData.apiGroup] = function (frame)
    local period, group = periodAndGroup (frame.args)
    return group
end

-- Вспомогательные функции для функции «электронная конфигурация»:
-- Создание пустых электронных оболочек:
local function createElectronShells ()
    local electrons = {}
    for n = 1, 7 do
        electrons [n] = {}
        for l = 0, n - 1 do
            electrons [n] [l] = 0
        end
        electrons [n].total  = 0
        electrons [n].free_l = 0
    end
    return electrons
end

-- Находит первое свободное место с n + l = energy
local function findSubshellForNplusL (electrons, energy)
    local found = false
    local l = 0
    -- Поскольку l < n, на меньших n требуемого n + l не найти:
    local n = math.ceil (energy / 2) - 1
    while not found and n <= math.min (energy, #electrons - 1) do
        n = n + 1
        if electrons [n].free_l < n           -- допустимо (n > l).
       and electrons [n].free_l + n == energy -- найден подходящий уровень.
        then
            l = electrons [n].free_l
            found = true -- нашли.
        end
    end
    return found, n, l
end

-- Заселение электронных оболочек элемента № no:
local function populateElectronShells (no)

    -- Создание пустых электронных оболочек:
    local electrons = createElectronShells ()

    -- оставшееся число электронов:
    local el = no
    -- Текущий слой и подслой:
    local n = 1    -- главное квантовое число.
    local l = 0    -- орбитальное квантовое число.

    while el > 0 do
        -- -l <= m <= l; s = -1/2 or 1/2:
        if electrons [n] [l] >= 2 * (2 * l + 1) then
            -- подслой полон; нужно найти другой:
            -- отметить его как полный:
            electrons [n].free_l = electrons [n].free_l + 1
            -- Выбрать слой и подслой для·размещения электрона по·правилу Клечковского:
            -- Выбор слоя, где есть место с теми же n + l:
            local found
            local new_n
            local new_l
            found, new_n, new_l = findSubshellForNplusL (electrons, n + l)
            if found then
                -- место найдено:
                n = new_n
                l = new_l
            else
                -- надо увеличить n + l:
                found, n, l = findSubshellForNplusL (electrons, n + l + 1)
            end
        end
        -- Подселяем электрон на выбранный подслой
        electrons [n] [l] = electrons [n] [l] + 1
        electrons [n].total = electrons [n].total + 1
        el = el - 1
    end

    -- Внесение исключений из правила Клечковского:
    if chemistryData.exceptions [no] then
        -- Перемещение электронов:
        for n, shell in pairs (chemistryData.exceptions [no]) do
            for l, delta in pairs (shell) do
                electrons [n] [l] = electrons [n] [l] + delta
            end
        end
    end

    return electrons
end

-- Электронная конфигурация (санированные параметры):
local function printElectronicConfiguration (no, start, shells, sep)

    -- Получить электронную кофигурацию:
    local electrons = populateElectronShells (no)

    -- Оформление электронной конфигурации в виде строки:
    local ret = ""

    -- Выявление первого и последнего показываемого слоя с учётом их заполнения и переданных параметров:
    -- Последний заполненный:
    local last_full = #electrons
    while electrons [last_full].total == 0 and last_full > 0 do
        last_full = last_full - 1
    end
    -- Первый требуемый (с помощью отрицательных значений вывод с конца):
    local first = start
    if start < 0 then
        first = last_full + start + 1
    end
    -- Последний требуемый (0 -- все):
    local last
    if shells ~= 0 then
        last = first + shells - 1
    else
        last = last_full
    end

    -- Обход слоёв:
    for n = first, last do
        local shell = electrons [n]
        -- Разделитель:
        if n > first then
            ret = ret .. sep
        end
        ret = ret .. tostring (n)
        -- Обход подслоёв:
        for l = 0, #shell do
            if shell [l] > 0 then
                -- если на подслое есть электроны, добавить:
                ret = ret .. "<em>"  .. chemistryData.l [l]  .. "</em>"
                          .. "<sup>" .. tostring (shell [l]) .. "</sup>"
            end
        end
    end
    return ret
end

-- Электронная конфигурация (нечистые параметры):
local function electronicConfiguration (args)
    -- Извлечение параметра «Атомный номер» (первого):
    local ok
    local number
    ok, number = loadAtomicNumber (args [chemistryData.argAtomicNumber])
    if not ok then
        return number
    end

    -- Извлечение параметра «Начать с оболочки» (второго):
    local start
    ok, start = loadStart (args [chemistryData.argStart])
    if not ok then
        return start
    end

    -- Извлечение параметра «Число оболочек» (третьего):
    local shells
    ok, shells = loadShells (args [chemistryData.argShells])
    if not ok then
        return shells
    end

    -- Извлечение параметра «разделитель»:
    local sep
    ok, sep = loadSep (args [chemistryData.argSep])
    if not ok then
        return sep
    end
 
    return printElectronicConfiguration (number, start, shells, sep)
end

-- Регистрация эспортируемых функций:
-- «электронная конфигурация»:
m [chemistryData.apiElectronicConfiguration] = function (frame)
    return electronicConfiguration (frame.args)
end

-- Получение молярной массы по формуле:
-- H2O, NH3, CuSO4, Si(OH)4 и т.п.: 
-- Получить один токен формулы f в виде итератора:
local function item (f)
    local i = 1
    return function ()
     	local t, x = nil, nil
	    if i <= mw.ustring.len (f) then
        	x = mw.ustring.match (f, '^%u%l*', i);
        	t = chemistryData.T_ELEM     -- элемент
	    	if not x then x = mw.ustring.match (f, '^%d+', i); t = chemistryData.T_NUM; end -- индекс
        	if not x then x = mw.ustring.match (f, '^%(', i);  t = chemistryData.T_O;   end -- (
        	if not x then x = mw.ustring.match (f, '^%)', i);  t = chemistryData.T_C;   end -- )
        	if     x then i = i + mw.ustring.len (x)
        	else
        		i = i + 999
        		return false, formatChemistryError (chemistryData.errorWrongFormula .. " " .. f)
        	end
        end
     	return t, x
    end
end

-- Преобразовать строковую формулу в массив:
local function parseFormula (str)
    local stack = {{}}
    local f = mw.ustring.gsub (mw.ustring.gsub (str, "</?sub>", ""), "_", "")
    local t, x
    for t, x in item (f) do
    	if     t == chemistryData.T_ELEM then
            if chemistryData.elements [x] then
                -- Кладём после последнего элемента последнего элемента стэка:
                stack [#stack] [#stack [#stack] + 1] = {x, 1}
            else
            	return false, formatChemistryError (chemistryData.errorUnknownElement .. " " .. x)
            end
        elseif t == chemistryData.T_NUM then
        	-- Исправляем количество в последнем элементе последнего элемента стэка:
        	stack [#stack] [#stack [#stack]] [2] = tonumber (x)
	    elseif t == chemistryData.T_O then
            -- (:
            -- поместить в стэк:
            stack [#stack + 1] = {}
	    elseif t == chemistryData.T_C then
	    	-- ):
            -- вытолкнуть из стека (переместить на уровень ниже, превратив предыдущий уровень в иерархию):
            if #stack > 1 then
                stack [#stack - 1] [#stack [#stack - 1] + 1] = {stack [#stack], 1}
                stack [#stack] = nil
            else
                return false, formatChemistryError (chemistryData.errorTooManyCP .. " " .. str)
            end
	    else
        	return false, formatChemistryError (chemistryData.errorWrongFormula .. " " .. str)
        end
   end
   if #stack == 1 then
   	   -- единственный штатный возврат. Скобки точно сбалансированы:
   	   return true, stack [1]
   else
   	   return false, formatChemistryError (chemistryData.errorTooManyOP .. " " .. str)
   end
end

-- Молярная масса. Получает уже разобранную формулу:
local function molarMass (parsed)
	local ret = 0
	local mass = 0
    for i = 1, table.maxn (parsed) do
    	if parsed [i] then
    		element = parsed [i] [1]
    		if type (element) == "string" then
    			-- похоже на элемент:
    			if chemistryData.elements [element] then
    				-- элемент найден:
    				mass = chemistryData.elements [element] ["mass"]
    			else
    	            -- нет такого элемента. Это, вообще, возможно?:
    	            return formatChemistryError (chemistryData.errorUnknownElement .. " " .. element)
    	        end
    	    else
    	    	-- радикал:
    	    	mass = molarMass (element)
    	    end
    	    ret = ret + mass * parsed [i] [2]
    	end
    end
    return ret
end
 
-- Молярная масса. Функция получает неразобранную формулу в виде строки:
local function molarMassString (str)
	ok, parsed = parseFormula (str)
	if not ok then
		return parsed -- уже содержит оформленную ошибку.
	end
	return molarMass (parsed)
end

-- Молярная масса. Функция получает сырые параметры:
local function molarMassRaw (args)
	if args [chemistryData.argFormula] and args [chemistryData.argFormula] ~= "" then
		return molarMassString (args [chemistryData.argFormula])
	else
		-- Параметр "формула" не передан:
		return formatChemistryError (chemistryData.errorMissingFormula)
	end
end

-- Регистрация эспортируемых функций:
-- «молярная масса»:
m [chemistryData.apiMolarMass] = function (frame)
    return molarMassRaw (frame.args)
end
 
-- оформить формулу (санированные параметры):
local function formula (elements, charge)
    local ret = ""
    local offset_charge
    for i = 1, table.maxn (elements) do
    	if elements [i] then
	    	local radical = ""
	    	if type (elements [i] [1]) == "string" then
	    		-- one-atom radical:
	    		local color = "000000"
	    		local name = "Химический элемент"
	    		if chemistryData.elements [elements [i] [1]] then
	    			color = chemistryData.elements [elements [i] [1]] ["color"]
	    			name = chemistryData.elements [elements [i] [1]] ["name"]
	    		end
			    radical = '[[' .. name .. '|<span style="color: #' .. color .. '; font-weight: bold">' .. elements [i] [1] .. '</span>]]'
			elseif type (elements [i] [1]) == "table" then
				-- many-atom radical:
				radical = formula (elements [i] [1])
				if elements [i] [2] > 1 then
					-- () needed:
					radical = "(" .. radical .. ")"
				end
	        end
			ret = ret .. radical    
	        -- Index is only needed when it is greater than 1:
	        if elements [i] [2] > 1 then
			    ret = ret .. "<sub>" .. tostring (elements [i] [2]) .. "</sub>"
			    offset_charge = true
			else
				offset_charge = false
			end
		end
    end
    -- Заряд иона:
    if charge and charge ~= "" then
    	local charge_string = "<sup"
        -- Заряд иона над последним индексом:
        if offset_charge then
    	    charge_string = charge_string .. ' style="margin-left:-0.5em"'
    	end
    	charge_string = charge_string .. ">" .. tostring (charge) .. "</sup>"
    	ret = ret .. charge_string
    end
    return ret
end

-- оформить формулу (нечистые параметры):
local function showFormula (args)
	local elements = {}
	local counter = 1
    local position = 1
	local number_expected = false
	-- Принудительная сортировка:
	local force_sort = false
	-- Заряд иона:
	local charge = ""
	-- Именно так, более простой способ не работает:
	local params = {}
	for key, val in pairs (args) do
	    -- Принудительная сортировка:
	    if key == chemistryData.argSort then
            force_sort = true
        -- Заряд иона:
        elseif key == chemistryData.argCharge then
        	if mw.ustring.find (val, "^%d*[+-]$") then
        	    charge = val
        	else
        		return formatChemistryError (chemistryData.errorWrongCharge .. ": " .. tostring (val))
        	end
        -- Элемент:
        else
        	params [key] = val
        end
	end
    for key, val in pairs (params) do
		-- Cast types:
		if tonumber (key) then key = tonumber (key) end
		if tonumber (val) then val = tonumber (val) end

        -- Different types of the current parametre:
	    if type (key) == "number" then
	    	if type (val) == "string" then
	    		if number_expected then
	    			counter = counter + 1
	    			number_expected = false
	    		end
                if force_sort then
                	-- принудительная сортировка:
            	    -- Принятая позиция элемента:
    	            if mw.ustring.match (val, "^%u%l*$") then
    			        -- Размещаем элемент где принято:
    			        position = chemistryData.elements [val] ["order"]
    			    else
    			    	-- радикалы в конце:
    			    	position = 2000 + counter
    			    end
                else
                	-- пользовательская сортировка:
                	position = counter
                end
	    		if not mw.ustring.match (val, "^%u%l*$") then
	    			-- it's a whole formula:
	    		    ok, val = parseFormula (val)
	    		    if not ok then
	    		    	-- разбор формулы не удался:
	    		    	return val -- здесь теперь сообщение об ошибке.
	    		    end
                end
	            elements [position] = {val, 1}
	    		number_expected = true -- also way to show ().
	    	elseif type (val) == "number" then
	    		if number_expected then
	    		    elements [position] [2] = val
	    		    counter = counter + 1
	    		    number_expected = false
	    		else
	    			return formatChemistryError (chemistryData.errorUnexpectedNumber .. ": " .. tostring (val))
	    		end
	    	end
        elseif type (key) == "string" then
    	    -- Принятая позиция элемента:
    	    if mw.ustring.match (key, "^%u%l*$") then
    			-- Размещаем элемент где принято:
    			position = chemistryData.elements [key] ["order"]
            else
                -- Это многоатомный радикал:
    		    ok, key = parseFormula (key)
    		    if not ok then
    		    	-- разбор формулы не удался:
    		    	return key -- здесь теперь сообщение об ошибке.
    		    end
    		    -- Ставим его в конец:
    		    position = 2000 + counter
    		end
    		if number_expected then
    			counter = counter + 1
    			number_expected = false
    		end
    	    if type (val) == "number" then
    	    	if val <= 0 then
    	    		return formatChemistryError (chemistryData.errorNonPositiveAtoms .. ": " .. tostring (key) .. " = " .. tostring (val))
    	    	end
            elseif val == "" or val == nil then
        	    -- А сюда управление хоть раз передастся?
   	    	    val = 1
   	    	    number_expected = true
	        else
    	    	return formatChemistryError (chemistryData.errorNonNumericAtoms .. ": " .. tostring (key) .. " = " .. val)
    	    end
    	    elements [position] = {key, val}
    	end
	end
	return formula (elements, charge)
end

-- Регистрация эспортируемых функций:
-- «оформить формулу»:
m [chemistryData.apiShowFormula] = function (frame)
    local nextfunc, static, cur = pairs (frame.args)
    if nextfunc (static, cur) == nil then
	    -- аргументы не переданы. Использовать аргументы шаблона:
        return showFormula (frame:getParent ().args)
    else
        -- переданы аргументы:
    	return showFormula (frame.args)
    end
end

-- Последняя строка. Экспорт функций из модуля:
return m