2010-11-07 3 views
8

Я пытаюсь проанализировать химическую формулу (в формате, например: Al2O3 или O3 или C или C11H22O12) в C# из строки. Он отлично работает, если только один атом определенного элемента (например, атом кислорода в H2O). Как я могу исправить эту проблему, и, кроме того, есть ли лучший способ проанализировать строку химических формул, чем я делаю?Разбор химической формулы из строки в C#?

ChemicalElement - это класс, представляющий собой химический элемент. Он имеет свойства AtomicNumber (int), Name (string), Symbol (string). ChemicalFormulaComponent - это класс, представляющий собой химический элемент и количество атомов (например, часть формулы). Он имеет свойства Element (ChemicalElement), AtomCount (int).

Остальное должно быть достаточно ясно, чтобы понять (я надеюсь), но, пожалуйста, дайте мне знать с комментарием, если я могу прояснить что-либо, прежде чем вы ответите.

Вот мой текущий код:

/// <summary> 
    /// Parses a chemical formula from a string. 
    /// </summary> 
    /// <param name="chemicalFormula">The string to parse.</param> 
    /// <exception cref="FormatException">The chemical formula was in an invalid format.</exception> 
    public static Collection<ChemicalFormulaComponent> FormulaFromString(string chemicalFormula) 
    { 
     Collection<ChemicalFormulaComponent> formula = new Collection<ChemicalFormulaComponent>(); 

     string nameBuffer = string.Empty; 
     int countBuffer = 0; 

     for (int i = 0; i < chemicalFormula.Length; i++) 
     { 
      char c = chemicalFormula[i]; 

      if (!char.IsLetterOrDigit(c) || !char.IsUpper(chemicalFormula, 0)) 
      { 
       throw new FormatException("Input string was in an incorrect format."); 
      } 
      else if (char.IsUpper(c)) 
      { 
       // Add the chemical element and its atom count 
       if (countBuffer > 0) 
       { 
        formula.Add(new ChemicalFormulaComponent(ChemicalElement.ElementFromSymbol(nameBuffer), countBuffer)); 

        // Reset 
        nameBuffer = string.Empty; 
        countBuffer = 0; 
       } 

       nameBuffer += c; 
      } 
      else if (char.IsLower(c)) 
      { 
       nameBuffer += c; 
      } 
      else if (char.IsDigit(c)) 
      { 
       if (countBuffer == 0) 
       { 
        countBuffer = c - '0'; 
       } 
       else 
       { 
        countBuffer = (countBuffer * 10) + (c - '0'); 
       } 
      } 
     } 

     return formula; 
    } 
+0

Почему вы проверка, является ли первым символ формулы в верхнем регистре на каждой итерации 'for' loop ('! char.IsUpper (chemicalFormula, 0)')? Индекс здесь всегда равен 0. –

+0

Я думаю, что у вашей функции также есть проблема с чем-то вроде C4O2, это правда? –

+0

См. Также страницу http://stackoverflow.com/questions/2974362/parsing-a-chemical-formula/3742985. Он запрашивает одно в Java, с ответом в Python и связанными с более сложными решениями ANTLR и Python. –

ответ

10

Я переписал ваш парсер с помощью регулярных выражений. Регулярные выражения отлично подходят для счета за то, что вы делаете. Надеюсь это поможет.

public static void Main(string[] args) 
{ 
    var testCases = new List<string> 
    { 
     "C11H22O12", 
     "Al2O3", 
     "O3", 
     "C", 
     "H2O" 
    }; 

    foreach (string testCase in testCases) 
    { 
     Console.WriteLine("Testing {0}", testCase); 

     var formula = FormulaFromString(testCase); 

     foreach (var element in formula) 
     { 
      Console.WriteLine("{0} : {1}", element.Element, element.Count); 
     } 
     Console.WriteLine(); 
    } 

    /* Produced the following output 

    Testing C11H22O12 
    C : 11 
    H : 22 
    O : 12 

    Testing Al2O3 
    Al : 2 
    O : 3 

    Testing O3 
    O : 3 

    Testing C 
    C : 1 

    Testing H2O 
    H : 2 
    O : 1 
     */ 
} 

private static Collection<ChemicalFormulaComponent> FormulaFromString(string chemicalFormula) 
{ 
    Collection<ChemicalFormulaComponent> formula = new Collection<ChemicalFormulaComponent>(); 
    string elementRegex = "([A-Z][a-z]*)([0-9]*)"; 
    string validateRegex = "^(" + elementRegex + ")+$"; 

    if (!Regex.IsMatch(chemicalFormula, validateRegex)) 
     throw new FormatException("Input string was in an incorrect format."); 

    foreach (Match match in Regex.Matches(chemicalFormula, elementRegex)) 
    { 
     string name = match.Groups[1].Value; 

     int count = 
      match.Groups[2].Value != "" ? 
      int.Parse(match.Groups[2].Value) : 
      1; 

     formula.Add(new ChemicalFormulaComponent(ChemicalElement.ElementFromSymbol(name), count)); 
    } 

    return formula; 
} 
+0

Это выглядит отлично, спасибо большое. Sidenote, хотя - не должен * вблизи [A-Z] [a-z] быть +? –

+0

'*' применяется только к одной группе '[]'. Это означает, что '[A-Z]' должен отображаться ровно один раз (поскольку он не имеет '*' или '+'), а '[a-z]' должен появляться ноль или более раз. –

+0

Ах да, конечно. Не правильно читать мои скобки. Еще раз спасибо! –

2

Проблема с методом здесь:

  // Add the chemical element and its atom count 
      if (countBuffer > 0) 

Когда у вас нет номера, отсчет буфера будет 0, я думаю, что это будет работать

  // Add the chemical element and its atom count 
      if (countBuffer > 0 || nameBuffer != String.Empty) 

Это будет работать, когда для таких формул, как HO2, или что-то в этом роде. Я считаю, что ваш метод никогда не будет вставлять в коллекцию formula элемент las химической формулы.

Вы должны добавить последний элемент bufer в коллекции, прежде чем вернуть результат, как это:

formula.Add(new ChemicalFormulaComponent(ChemicalElement.ElementFromSymbol(nameBuffer), countBuffer)); 

    return formula; 
} 
1

первую очередь: я не использовал генератор парсеров в .net, но я Я уверен, что вы найдете что-то подходящее. Это позволит вам написать грамматику химических формул в гораздо более читаемой форме. См. Например, this question для первого запуска.

Если вы хотите сохранить свой подход: возможно ли, что вы не добавляете свой последний элемент независимо от того, имеет ли он число или нет? Возможно, вы захотите запустить свой цикл с i<= chemicalFormula.Length, а в случае i==chemicalFormula.Length также добавьте то, что у вас есть к вашей Формуле. Вы также должны удалить свое условие if (countBuffer > 0), потому что countBuffer может фактически быть нулевым!

0

Regex должен нормально работать с простой формулой, если вы хотите разделить что-то вроде:

(Zn2(Ca(BrO4))K(Pb)2Rb)3 

может быть проще использовать анализатор для него (из-за соединения вложенности). Любой анализатор должен иметь возможность обрабатывать его.

Я заметил эту проблему несколько дней назад. Я думал, что это будет хороший пример того, как можно писать грамматику для синтаксического анализатора, поэтому я включил простую грамматику химических формул в свой набор NLTключевых правила - для лексера:

"(" -> LPAREN; 
")" -> RPAREN; 

/[0-9]+/ -> NUM, Convert.ToInt32($text); 
/[A-Z][a-z]*/ -> ATOM; 

и для синтаксического анализа:

comp -> e:elem { e }; 

elem -> LPAREN e:elem RPAREN n:NUM? { new Element(e,$(n : 1)) } 
     | e:elem++ { new Element(e,1) } 
     | a:ATOM n:NUM? { new Element(a,$(n : 1)) } 
     ; 
Смежные вопросы