понедельник, 14 мая 2012 г.

Парсер-анализатор химических формул. C#

    В данном посте я расскажу о парсинге химических формул с целью дальнейшей адаптации для отображения в 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 разметку.
    В рамках семантики химических формул я выделил следующие классы лексем:
  1. element - химический элемент в составе формулы;
  2. number - количество молекул вещества;
  3. separator - разделитель (круглые и квадратные скобки, ",");
  4. factor - знак умножения;
  5. valence - валентность атомов элемента;
  6. atom - количество атомов;
  7. undefined - рабочий класс по умолчанию.
    Класс Lexem определяет непосредственно лексему, как объект модели предметной области. Содержит два члена-переменных str_set и lex_class типа LexemClass, взаимно однозначно определяющих лексему и ее тип, и два свойства доступа к приватным переменным Set и Type.



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>.
Пример


Комментариев нет:

Отправить комментарий

Примечание. Отправлять комментарии могут только участники этого блога.