В данном посте я расскажу о парсинге химических формул с целью дальнейшей адаптации для отображения в HTML.
Формулы в химическом виде наиболее удобно хранить в БД, но в таком виде они не всегда удобны для чтения. Например на странице сайта длинные формулы, да и вообще формулы любой длинны, удобнее просматривать в их эмпирическом виде.
Для преобразования формул можно воспользоваться парсером аналогичным данному.
Для преобразования формул можно воспользоваться парсером аналогичным данному.
Модель
Функциональная схема может иметь следующий вид:
На входе имеется символьная строка химической формулы, хранящаяся в БД или заданная в явном виде пользователем.
Строка передается на обработку в модуль лексического анализатора. В процессе анализа из строки выделяются отдельные лексемы и заносятся в список лексем.
Далее начинает работу конструктор HTML-разметки. В нем производится обработка сформированного на предыдущем шаге списка лексем и построение соответствующей разметки в зависимости от класса той или иной лексемы.
На выходе получаем формулу в ее эмпирическом виде.
Разработка
В данном случае я приведу простой код с минимальными проверками, без всевозможных защит от "дурака", исключений и прочего - только сам функционал.
Написание кода стоит начать с определения класса сущности предметной области - лексемы.
Написание кода стоит начать с определения класса сущности предметной области - лексемы.
public
enum LexemClass
{ undefined, element, number, separator, factor, valence, atom };
public class Lexem
{
private
string str_set;
private
LexemClass lex_class;
public string Set
{
get
{ return this.str_set;
}
set
{ this.str_set = value;
}
}
public LexemClass Type
{
get
{ return lex_class; }
set { lex_class = value; }
}
}
Здесь LexemClass - перечисление, содержащее набор предопределенных классов лексем.
В процессе лексического анализа выделенным из входной последовательности лексемам присваивается определенный класс, который является идентификатором типа лексемы. Тип лексемы является ключевым "свойством", в прямом и переносном смыслах этого слова, класса С# Lexem, которое в дальнейшем используется непосредственно для преобразования набора лексем в HTML разметку.
В рамках семантики химических формул я выделил следующие классы лексем:
- element - химический элемент в составе формулы;
- number - количество молекул вещества;
- separator - разделитель (круглые и квадратные скобки, ",");
- factor - знак умножения;
- valence - валентность атомов элемента;
- atom - количество атомов;
- undefined - рабочий класс по умолчанию.
Класс Lexem определяет непосредственно лексему, как объект модели предметной области. Содержит два члена-переменных str_set и lex_class типа LexemClass, взаимно однозначно определяющих лексему и ее тип, и два свойства доступа к приватным переменным Set и Type.
Класс FormulaParser помимо свойств Formula, Lexems и членов-переменных содержит конструктор, который в качестве параметра получает объект типа String - исходную формулу для дальнейшей обработки. Словарь лексем будет содержаться во внутренней переменной LexemsList типа List<Lexem>. Обработка выполняется в методе ParseFormula().
Принцип работы лексического анализатора заключается в последовательной проверке каждого символа входной строки с поэтапным выделением и записью в словарь лексем.
Рассмотрим некоторые части внутренней реализации метода ParseFormula(). Итак, в методе выполняется проверка массива символов строки. Наиболее важные и емкие части кода - это проверка букв и цифр входной последовательности.
Если i-й символ является буквой - метод начинает формировать лексему с классом element заполняя внутреннюю член-переменную buffer. Решение о занесении лексемы в коллекцию принимается на основе изменения типа i+1-го символа входной последовательности или по достижению ее конца.
Как можно увидеть, данная реализация предусматривает выделение частей формулы в виде совокупности химических элементов, а не выделение каждого отдельного химического элемента в отдельную лексему словаря. Для достижения подобного результата можно доработать алгоритм выделения лексем опираясь на регистр символов входной строки.
Если i-й символ является цифрой - опять заполняется переменная buffer. Если следующий символ не является цифрой или знаком "," (для дробных значений) - принимается решение о занесении значения переменной buffer в список лексем. Тип лексемы определяется в зависимости от того, каким значением инициализирована вспомогательная переменная temp_defined_type.
Если i-й символ является символом ",". "," может быть либо разделителем между химическими элементами, либо - частью дробного числа. Здесь я использовал несколько иной подход к формированию списка лексем. Решение о занесении n-ой лексемы в список принимается на основе типа класса предыдущего n-1 элемента этого списка.
Последняя часть кода - если символ является знаком "+". В этом случае формируется лексема с классом valence. Как и прежде, решение о занесении лексемы в коллекцию принимается на основе изменения типа i+1-го символа входной последовательности. Перед инициализацией нового элемента коллекции LexemsList в переменной buffer формируется строка, которая состоит из длинны самой строки данных и символа "+".
public
class FormulaParser
{
private string str_formula,
buffer;
private char symbol;
private int i;
private LexemClass
temp_defined_type;
private List<Lexem> LexemsList;
public string Formula
{...}
public List<Lexem> Lexems
{...}
public FormulaParser(string
str_input)
{
str_formula
= str_input;
temp_defined_type
= LexemClass.undefined;
...
}
public void ParseFormula()
{
int
formula_length = Formula.Length;
while(i<formula_length)
{
//
Если буква
symbol = Convert.ToChar(Formula[i]);
if
(char.IsLetter(symbol))
{
buffer+=symbol.ToString();
if(i!=formula_length-1)
{
if(!char.IsLetter(Convert.ToChar(Formula[i+1])))
{
Lexems.Add(new Lexem{Set=buffer,
Type=LexemClass.element});
buffer=string.Empty;
}
}
else
Lexems.Add(new Lexem { Set =
buffer, Type=LexemClass.element});
}
//
Если цифра
else
if (char.IsDigit(symbol))
{
buffer+=symbol.ToString();
if
(i!=formula_length-1)
{
if
(!char.IsDigit(Convert.ToChar(Formula[i
+ 1])) &&
!char.Equals(Convert.ToChar(Formula[i + 1]), ','))
{
Lexems.Add(new Lexem{Set =
buffer,Type =
Enum.Equals(temp_defined_type, LexemClass.undefined)
?
LexemClass.atom : (temp_defined_type)});
temp_defined_type
= LexemClass.undefined;
buffer = string.Empty;
}
}
else
{
Lexems.Add(new Lexem{Set =
buffer,Type =
Enum.Equals(temp_defined_type, LexemClass.undefined)
?
LexemClass.number : (temp_defined_type)});
temp_defined_type
= LexemClass.undefined;
}
}
//
Если ","
else
if (char.Equals(symbol,
','))
{
// если предыдущая лекскма - элемент -> это
разделитель
if
(Lexems.Last().Type.Equals("element"))
Lexems.Add(new Lexem { Set =
symbol.ToString(),
Type = LexemClass.separator });
else
buffer +=
symbol.ToString();
}
//
Если "[" или "("
...
//
Если "]" или ")"
else
if (symbol.Equals(']')
|| symbol.Equals(')'))
...
//
Если "+"
else
if (symbol.Equals('+'))
{
buffer +=
symbol.ToString();
if
(i != formula_length - 1)
{
if
(!char.Equals(Convert.ToChar(Formula[i
+ 1]), '+'))
{
buffer =
buffer.Length>1?(buffer.Length.ToString() + "+"):("+");
Lexems.Add(new Lexem { Set =
buffer, Type = LexemClass.valence });
buffer = string.Empty;
}
}
}
// Если "∙"
...
// Не забываем об инкрементном счетчике
i++;
}
}
}
Класс FormulaParser помимо свойств Formula, Lexems и членов-переменных содержит конструктор, который в качестве параметра получает объект типа String - исходную формулу для дальнейшей обработки. Словарь лексем будет содержаться во внутренней переменной LexemsList типа List<Lexem>. Обработка выполняется в методе ParseFormula().
Принцип работы лексического анализатора заключается в последовательной проверке каждого символа входной строки с поэтапным выделением и записью в словарь лексем.
Рассмотрим некоторые части внутренней реализации метода ParseFormula(). Итак, в методе выполняется проверка массива символов строки. Наиболее важные и емкие части кода - это проверка букв и цифр входной последовательности.
Если i-й символ является буквой - метод начинает формировать лексему с классом element заполняя внутреннюю член-переменную buffer. Решение о занесении лексемы в коллекцию принимается на основе изменения типа i+1-го символа входной последовательности или по достижению ее конца.
Как можно увидеть, данная реализация предусматривает выделение частей формулы в виде совокупности химических элементов, а не выделение каждого отдельного химического элемента в отдельную лексему словаря. Для достижения подобного результата можно доработать алгоритм выделения лексем опираясь на регистр символов входной строки.
Если i-й символ является цифрой - опять заполняется переменная buffer. Если следующий символ не является цифрой или знаком "," (для дробных значений) - принимается решение о занесении значения переменной buffer в список лексем. Тип лексемы определяется в зависимости от того, каким значением инициализирована вспомогательная переменная temp_defined_type.
Если i-й символ является символом ",". "," может быть либо разделителем между химическими элементами, либо - частью дробного числа. Здесь я использовал несколько иной подход к формированию списка лексем. Решение о занесении n-ой лексемы в список принимается на основе типа класса предыдущего n-1 элемента этого списка.
Последняя часть кода - если символ является знаком "+". В этом случае формируется лексема с классом valence. Как и прежде, решение о занесении лексемы в коллекцию принимается на основе изменения типа i+1-го символа входной последовательности. Перед инициализацией нового элемента коллекции LexemsList в переменной buffer формируется строка, которая состоит из длинны самой строки данных и символа "+".
public
static class HTMLBuilder
{
public static string GenerateHTML(List<Lexem> LexemsList)
{
string str_HTML="<span
style=\"color:Black;font-size:11pt;\">";
foreach
(var lexem in
LexemsList)
{
switch
(lexem.Type)
{
case
LexemClass.atom:
str_HTML += "<sub>" + lexem.Set + "</sub>";
break;
case
LexemClass.element:
case
LexemClass.separator:
case
LexemClass.number:
case
LexemClass.factor:
str_HTML += lexem.Set;
break;
case
LexemClass.valence:
str_HTML += "<sup>" + lexem.Set + "</sup>";
break;
}
}
str_HTML += "</span></p>";
return str_HTML;
}
}
В завершении самая простая часть работы - обработка коллекции лексем.
Для генерации HTML-разметки можно использовать специальные классы C#, как, например, класс TagBuilder (http://msdn.microsoft.com/en-us/library/system.web.mvc.tagbuilder.aspx). В данном случае я делал разметку вручную в виду ее тривиальности.
Статический класс HTMLBuilder имеет один метод - GenerateHTML(), который принимает в качестве аргумента коллекцию типа List<Lexem> и возвращает строку со сформированной разметкой.
Работа метода GenerateHTML() заключается в последовательной проверке свойства Type всех элементов коллекции List<Lexem>. В зависимости от значения этого свойства для каждого элемента из списка лексем формируется свой вариант HTML-разметки.
В данном случае для перевода атомного числа и валентности в подстрочный и соответственно надстрочный регистры используются теги HTML <sub></sub> и <sup></sup>.
Для генерации HTML-разметки можно использовать специальные классы C#, как, например, класс TagBuilder (http://msdn.microsoft.com/en-us/library/system.web.mvc.tagbuilder.aspx). В данном случае я делал разметку вручную в виду ее тривиальности.
Статический класс HTMLBuilder имеет один метод - GenerateHTML(), который принимает в качестве аргумента коллекцию типа List<Lexem> и возвращает строку со сформированной разметкой.
Работа метода GenerateHTML() заключается в последовательной проверке свойства Type всех элементов коллекции List<Lexem>. В зависимости от значения этого свойства для каждого элемента из списка лексем формируется свой вариант HTML-разметки.
В данном случае для перевода атомного числа и валентности в подстрочный и соответственно надстрочный регистры используются теги HTML <sub></sub> и <sup></sup>.
Пример