Наверное, название этой статьи слишком громкое для того, о чем я собираюсь рассказать, но как говорится за неимением лучшего, воспользуемся тем, что есть под рукой в данный момент…
Итак, одно время я работал лаборантом на кафедре общей и физической химии в ЯРГУ им. П.Г. Демидова (как видите, я не программист по специальности) и тогда приходилось довольно часто заниматься рутинными расчетами, наиболее часто из которых встречался расчет массы некоторого вещества для приготовления раствора с заданной концентрацией в колбе определенного объема. Надо сказать, что в этой небольшой лаборатории имелся персональный компьютер под нужды преподавателей и лаборантом, а меж тем, при наличии такого средства автоматизации никому в голову не приходила мысль о переносе расчетов в компьютер и облегчения собственной жизни на работе. Мне такая перспектива не нравилась, а кроме того, постоянные расчеты (записи об уже проделанных, понятное дело, никем не велись, ибо считалось, что это не столь необходимо) съедали довольно значительное количество времени, которого и так не хватало на подготовку лабораторных…
В общем, мне надоело мириться с проблемой (а студентов заставлять проводить такие расчеты просто бессмысленно) и я решил что-то предпринять, дабы облегчить себе жизнь. Конечно, у меня существовало уже готовое решение по подсчетам на Python, но внезапно, в один из рабочих дней была обнаружена его весьма некорректная работа, а с учетом того, что реализация была сделана 2 года назад (и по совсем другому поводу) и того факта, что я давно отошел от Python, передо мной встала задача пересоздания плохо работающего (но работающего!) инструмента.
Тогда (это было около года назад), я только начал изучать D, но я твердо знал, что если хочешь освоить язык программирования как инструмент, то ты должен решать на нем те задачи, которые у тебя имеются — так, собственно, и родилась идея сделать свой обработчик брутто-формул для расчетов.
Итак, если формулировать задачу, то получается, что нужно разработать ряд функций, вычисляющих молярную массу, массу навески по брутто-формулам веществ, и, если повезет преуспеть в этом, сделать приятный вывод результатов на консоль и еще что-нибудь. Как правило, расчет крутится вокруг молярной массы и некоторых заданных величин, что подразумевает реализацию расчета молярной массы по брутто-формуле некоторого химического соединения, а это означает, что нужно сделать выделение некоторых фрагментов из строки, которое обычно делается с помощью регулярных выражений.
Регулярные выражения на тот момент времени еще не были освоены (по крайней мере, в D я их применять не умел) и тогда созрела идея делать разбор брутто-формулы (формула, которая не содержит ни круглых, ни квадратных скобок) самостоятельно, что является довольно крутой и интересной задачей…
Первым делом, необходимо построить предикаты, проверяющие наличие определенных элементов в строке: нам очень нужны для реализации наших целей такие предикаты, как проверка на наличие только заглавных английских букв, проверка на наличие только строчных английских букв и проверка на наличие только цифр в строке. Таких предикатов всего три, а схема их построения одна и та же — строка на проверку и строка, содержащая ограничивающее множество (множество символов на соответствие которому и производим проверку), что навевает мысль о том, что неплохо бы здесь применить шаблоны, например, вот так:
import std.conv; import std.stdio; // шаблон для создания предикатов, проверяющих вхождение символа во множество символов template stringPredicate(string name, string set) { const char[] stringPredicate = "bool " ~ name ~ "(T = string)(T source) { bool flag = false; string set = \"" ~ set ~ "\"; string tmp = to!string(source); foreach (elem; tmp) { foreach (sym; set) { if (elem == sym) { flag = true; break; } else flag = false; } } return flag; }"; }
Создание трех предикатов уже дело абсолютно простое и занимает ровно три строки (не считая комментариев):
// строка только из цифр ? mixin(stringPredicate!("isDigit", "0123456789")); // строка только из английских букв в нижнем регистре ? mixin(stringPredicate!("isLower", "abcdefghijklmnopqrstuvwxyz")); // строка только из английских букв в верхнем регистре ? mixin(stringPredicate!("isUpper", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
Эти предикаты пригодятся нам для поиска и опознания в формуле химических элементов и коэффициентов соответственно.
Как видно по коду, был использован параметризованный контекст (он же шаблон) с достаточно широкими полномочиями (в частности, обработка любых типов, для которых определен метод toString()) в сочетании с использованием примесей (mixin) для автоматической кодогенерации, однако, при внешней сложности, все довольно таки просто, если переписать содержимое шаблона и рассмотреть это как самостоятельный код (несмотря на то, что я еще только осваивал D к тому моменту, я уже провел пару экспериментов с шаблонами, чтобы немного пользоваться ими).
Теперь можно приступать к разбору строки, содержащей брутто-формулу.
Первым делом, можно построить функцию выделения символа элемента из строки, для чего можно воспользоваться тем фактом, что символ химического элемента всегда начинается с большой буквы. Следовательно, для выделения символа химического элемента (потребуется нам в последствии для подсчета молярной массы по формуле) нам нужно иметь функцию, которая принимает строку формулы и предполагаемый номер позиции, где начинается искомый элемент; и которая действует весьма просто: если символ на предполагаемой позиции не является большой буквой, то химического элемента на этой позиции нет (точнее, химический элемент с этой позиции не начинается, так как возможна ситуация, когда строчная буква может принадлежать к символу элемента, а заглавная его буква находиться на позицию раньше); если символ на предполагаемой позиции является большой буквой, то это значит, что его нужно поместить в строку-аккумулятор (накапливает символы составляющие символ химического элемента) и накапливать в нее дальше строчные буквы, прекратив накопление в том случае, если встретится цифровой символ или большая буква:
// Ищем элемент в строке начиная с некоторой позиции string findElement(string s, int pos) { string acc = ""; if (isUpper(s[pos])) { acc ~= s[pos]; for (int i = pos + 1; i < s.length; i++) { if (isUpper(s[i])) break; else { if (!isDigit(s[i])) acc ~= s[i]; } } } return acc; }
Помимо символов химических элементов, в формулах встречаются еще и коэффициенты, обозначающие количество каждого химического элемента, при этом, принято по умолчанию, что если после элемента отсутствует коэффициент, то количество элемента равно единице (пожалуйста, запомните этот факт, он потребуется в дальнейшей реализации!). Функция извлечения коэффициентов из строковой репрезентации формулы значительно проще, ведь для того, чтобы выделить коэффициент начиная с некоторой позиции, необходимо просто накапливать в строку-аккумулятор только те символы, которые соответствуют цифрам, а условие окончания накопления - нахождение любого символа, отличного от цифрового:
// Ищем коэффициент начиная с некоторой строки string findCoeff(string s, int pos) { string acc = ""; foreach (elem; s[pos .. $]) { if (isDigit(elem)) acc ~= elem; else break; } return acc; }
Для разбора брутто-формулы с целью нахождения молярной массы соединения (представляет собой сумму молярных масс химических элементов, умноженных на их количество) у нас уже есть почти все, что нужно за исключением таблицы химических элементов с их молярными весами. Для того, чтобы сделать такую таблицу нужно воспользоваться одной замечательной вещью, реализованной в D - ассоциативными массивами (которые, я, по привычке, до сих пор называю словарями).
Итак, простейший вариант словаря под нашу задачу выглядит так:
float[string] tableOfElements; tableOfElements["H"] = 1.0; tableOfElements["N"] = 14.0; tableOfElements["C"] = 12.0; tableOfElements["O"] = 16.0; tableOfElements["S"] = 32.0; tableOfElements["Cl"] = 35.5; tableOfElements["Na"] = 23.0;
Теперь можно реализовать нахождение молярной массы по брутто-формуле:
// Молярная масса float molarMass(string formula, float[string] tableOfElements) { float index, molar_mass = 0.0; string elem, coeff, tmp; for (int i = 0; i < formula.length; i++) { elem = findElement(formula, i); if (elem != "") { try { tmp = formula[i + elem.length .. $]; coeff = findCoeff(tmp, 0); } catch { coeff = ""; } if (coeff != "") index = to!int(coeff); else index = 1; molar_mass += index * tableOfElements[elem]; } } return molar_mass; }
Работает это так: задаем две переменные index (хранит текущий коэффициент элемента) и molar_mass (хранит молярную массу, которая последовательно вычисляется для формулы), а затем проходим по всем символам формулы. Если встреченный символ представляет собой элемент (строка elem не является пустой), то следующим шагом является поиск коэффициента для элемента, начиная с позиции на которой был найден символ химического элемента плюс длина этого символа. Если коэффициент после элемента нашелся (строка coeff не пуста), то нужно преобразовать найденный коэффициент в число и присвоить его переменной index; в противном случае (вспомните, тот факт, который я упоминал в поиске коэффициентов) переменной index нужно присвоить единицу. Далее, воспользовавшись ассоциативным массивом с элементами и их молярными массами, добавляем в молярную массу вклад, вносимый найденным элементом и его количеством.
Как видите, это весьма просто, хотя на это я убил три дня напряженных размышлений и экспериментов!
Переходим, к ... приготовлению растворов.
Известно, что раствор готовится в колбе определенного объема и имеет определенную молярную концентрацию (а также определенную химическую формулу, того соединения, которое растворено), что на D можно записать примерно так:
// Описываем раствор struct Solution { string formula; float molarConcentration; float solutionVolume; }
Теперь же можно рассчитывать массу навески сухого вещества, исходя из описания, по довольно известной формуле из университетского курса химии:
/* масса навески, необходимая для приготовления заданного объема раствора заданной молярной концентрации */ float molarConcentration(Solution s, float[string] tableOfElements) { return molarMass(s.formula, tableOfElements) * s.molarConcentration * s.solutionVolume; }
Представим, что растворов достаточно много - тогда, даже расчет с применением описанных в программе формул является рутиной, чего можно избежать, если создать функцию, которая бы обрабатывала массив описаний растворов и печатала бы все это в форме таблички:
// Повтор строки string repl(T = string)(T source, uint n) { string tmp = to!string(source); string acc = tmp; if (n == 0) acc = tmp; else { while (n >= 1) { acc ~= tmp; n--; } } return acc; } // Таблица для приготовления растворов по известной молярной концентрации void mcTable(Solution[] s, float[string] tableOfElements) { string sep = repl("-",77); foreach (solution; s) { float molar_mass = molarMass(solution.formula, tableOfElements); writeln(sep); writefln("| %20s | m = %6.2f | Cm = %6.2f | M = %6.2f | V = %6.2f |", solution.formula, molarConcentration(solution, tableOfElements), solution.molarConcentration, molar_mass, solution.solutionVolume); } writeln(sep); }
Для этого, нам потребовалась вспомогательная функция repl, повторяющая некоторую строку заданное количество раз и функция writefln, печатающая текст в заданном формате.
Таким образом, можно избавиться от химической рутины:
void main() { float[string] tableOfElements; tableOfElements["H"] = 1.0; tableOfElements["N"] = 14.0; tableOfElements["C"] = 12.0; tableOfElements["O"] = 16.0; tableOfElements["S"] = 32.0; tableOfElements["Cl"] = 35.5; tableOfElements["Na"] = 23.0; tableOfElements["K"] = 39.0; Solution[] solutions = [ Solution("NaCl", 0.5, 0.250), Solution("KSCN", 0.1, 0.250), Solution("Na2S", 1.0, 0.1), Solution("N2H8S2O8", 0.5, 0.5), Solution("KNO2", 0.1, 0.5), Solution("N2H4SO4", 0.25, 0.5), ]; mcTable(solutions, tableOfElements); }
Что выглядит примерно так:
Как видите, D очень мне помог на работе 🙂