化学式解析器c++
Chemical formula parser C++
我目前正在研究一个程序,它可以解析化学式并返回分子量和组成百分比。下面的代码可以很好地处理化合物,如H2O、LiOH、CaCO3,甚至C12H22O11。然而,它不能理解括号内多原子离子的化合物,如(NH4)2SO4。
我不是在找一个人来为我写程序,只是给我一些提示,告诉我如何完成这样的任务。
当前,程序遍历输入的字符串raw_molecule
,首先找到每个元素的原子序数,并将其存储在一个向量中(我使用map<string, int>
来存储名称和原子#)。然后查找每个元素的数量。
bool Compound::parseString() {
map<string,int>::const_iterator search;
string s_temp;
int i_temp;
for (int i=0; i<=raw_molecule.length(); i++) {
if ((isupper(raw_molecule[i]))&&(i==0))
s_temp=raw_molecule[i];
else if(isupper(raw_molecule[i])&&(i!=0)) {
// New element- so, convert s_temp to atomic # then store in v_Elements
search=ATOMIC_NUMBER.find (s_temp);
if (search==ATOMIC_NUMBER.end())
return false;// There is a problem
else
v_Elements.push_back(search->second); // Add atomic number into vector
s_temp=raw_molecule[i]; // Replace temp with the new element
}
else if(islower(raw_molecule[i]))
s_temp+=raw_molecule[i]; // E.g. N+=a which means temp=="Na"
else
continue; // It is a number/parentheses or something
}
// Whatever's in temp must be converted to atomic number and stored in vector
search=ATOMIC_NUMBER.find (s_temp);
if (search==ATOMIC_NUMBER.end())
return false;// There is a problem
else
v_Elements.push_back(search->second); // Add atomic number into vector
// --- Find quantities next --- //
for (int i=0; i<=raw_molecule.length(); i++) {
if (isdigit(raw_molecule[i])) {
if (toInt(raw_molecule[i])==0)
return false;
else if (isdigit(raw_molecule[i+1])) {
if (isdigit(raw_molecule[i+2])) {
i_temp=(toInt(raw_molecule[i])*100)+(toInt(raw_molecule[i+1])*10)+toInt(raw_molecule[i+2]);
v_Quantities.push_back(i_temp);
}
else {
i_temp=(toInt(raw_molecule[i])*10)+toInt(raw_molecule[i+1]);
v_Quantities.push_back(i_temp);
}
}
else if(!isdigit(raw_molecule[i-1])) { // Look back to make sure the digit is not part of a larger number
v_Quantities.push_back(toInt(raw_molecule[i])); // This will not work for polyatomic ions
}
}
else if(i<(raw_molecule.length()-1)) {
if (isupper(raw_molecule[i+1])) {
v_Quantities.push_back(1);
}
}
// If there is no number, there is only 1 atom. Between O and N for example: O is upper, N is upper, O has 1.
else if(i==(raw_molecule.length()-1)) {
if (isalpha(raw_molecule[i]))
v_Quantities.push_back(1);
}
}
return true;
}
这是我的第一个帖子,所以如果我包含的信息太少(或可能太多),请原谅我。
虽然您可能能够做一个类似于临时扫描程序的事情来处理一级父级,但用于此类事情的规范技术是编写一个真正的解析器。
有两种常见的方法…
递归下降- 机器生成的基于语法规范文件的自底向上解析器。
(从技术上讲,还有第三类,PEG,它是机器自顶向下生成的。)
无论如何,对于情形1,当您看到(
时,您需要编写对解析器的递归调用,然后从此递归级别返回)
标记。
通常创建一个树状的内部表示;这被称为语法树,但在您的情况下,您可能可以跳过它,只返回递归调用的原子量,并添加到将从第一个实例返回的级别。
对于情形2,您需要使用像yacc这样的工具来将语法转换为解析器。
您的解析器可以理解某些事情。它知道当它看到N
时,这意味着"氮原子类型"。当它看到O
时,它意味着"氧型原子"。
这与c++中标识符的概念非常相似。当编译器看到int someNumber = 5;
时,它说:"存在一个int
类型的名为someNumber
的变量,其中存储了数字5
"。如果您稍后使用名称someNumber
,它知道您在谈论someNumber
(只要您在正确的范围内)。
回到原子解析器。当解析器看到一个原子后面跟着一个数字时,它就知道将这个数字应用到那个原子上。所以O2
的意思是2个氧原子。N2
表示"2个氮原子型"。
这对解析器意味着一些东西。这意味着仅仅看到一个原子是不够的。这是一个很好的开始,但是仅仅知道分子中有多少原子是不够的。它需要读取下一个内容。因此,如果它看到O
后面跟着N
,它就知道O
表示"1个氧原子"。如果它看到O
后面什么都没有(输入的末尾),那么它再次表示"1个氧原子"。
这是你目前拥有的。但是是错的。因为数字并不总能改变原子;有时,它们修饰原子的基团。如(NH4)2SO4
.
O
时,它需要知道这不是"氧原子类型"。它是一个">组含氧"。O2
为"2 Groups containing Oxygen"
一个基团可以包含一个或多个原子。所以当你看到(
时,你知道你正在创建一个组。因此,当您看到(...)3
时,您看到的是"3 Groups containing…"。
那么(NH4)2
是什么?它是"2个基团含[1个基团含氮,后面跟着4个基团含氢]"。
这样做的关键是理解我刚刚写的内容。组可以包含其他组。它们成群筑巢。如何实现嵌套?
你的解析器现在看起来是这样的:
NumericAtom ParseAtom(input)
{
Atom = ReadAtom(input); //Gets the atom and removes it from the current input.
if(IsNumber(input)) //Returns true if the input is looking at a number.
{
int Count = ReadNumber(input); //Gets the number and removes it from the current input.
return NumericAtom(Atom, Count);
}
return NumericAtom(Atom, 1);
}
vector<NumericAtom> Parse(input)
{
vector<NumericAtom> molecule;
while(IsAtom(input))
molecule.push_back(ParseAtom(input));
return molecule;
}
您的代码调用ParseAtom()
,直到输入耗尽,将每个原子+计数存储在数组中。显然,这里有一些错误检查,但我们现在先忽略它。
您需要做的是停止解析原子。您需要解析组,它们可以是单个原子,也可以是由()
对表示的一组原子。
Group ParseGroup(input)
{
Group myGroup; //Empty group
if(IsLeftParen(input)) //Are we looking at a `(` character?
{
EatLeftParen(input); //Removes the `(` from the input.
myGroup.SetSequence(ParseGroupSequence(input)); //RECURSIVE CALL!!!
if(!IsRightParen(input)) //Groups started by `(` must end with `)`
throw ParseError("Inner groups must end with `)`.");
else
EatRightParen(input); //Remove the `)` from the input.
}
else if(IsAtom(input))
{
myGroup.SetAtom(ReadAtom(input)); //Group contains one atom.
}
else
throw ParseError("Unexpected input."); //error
//Read the number.
if(IsNumber(input))
myGroup.SetCount(ReadNumber(input));
else
myGroup.SetCount(1);
return myGroup;
}
vector<Group> ParseGroupSequence(input)
{
vector<Group> groups;
//Groups continue until the end of input or `)` is reached.
while(!IsRightParen(input) and !IsEndOfInput(input))
groups.push_back(ParseGroup(input));
return groups;
}
这里最大的区别是ParseGroup
(类似于ParseAtom
函数)将调用ParseGroupSequence
。它将调用ParseGroup
。可以调用ParseGroupSequence
。等。Group
既可以包含一个原子,也可以包含Group
s序列(如NH4
),存储为vector<Group>
当函数可以直接或间接调用自己时,称为递归。只要不是无限递归,这是可以的。这是不可能的,因为它只会在每次看到(
时递归。
这是如何工作的呢?那么,让我们考虑一些可能的输入:
NH3
调用ParseGroupSequence
。它不在input或)
的末尾,所以它调用ParseGroup
。ParseGroup
看到N
,它是一个原子。它把这个原子加到Group
上。然后它看到一个H
,它不是一个数字。所以它设置Group
的计数为1,然后返回Group
。
在ParseGroup
看到一个H
,它是一个原子。它把这个原子加到Group
上。然后它看到一个3
,这是一个数字。因此它读取这个数字,将其设置为Group
的计数,并返回Group
。- 回到
ParseGroupSeqeunce
,我们将返回的Group
存储在序列中,然后在循环中迭代。我们没有看到)
,但是我们看到了输入的结束。所以我们返回当前的vector<Group>
。
ParseGroupSeqeunce
中,我们将返回的组存储在序列中,然后在循环中迭代。我们没有看到input或)
的结束,所以它调用ParseGroup
:(NH3) 2
调用ParseGroupSequence
。它不在input或)
的末尾,所以它调用ParseGroup
。ParseGroup
看到一个(
,这是一个Group
的开始。它吃掉这个字符(从输入中删除它)并在Group
上调用ParseGroupSequence
。ParseGroupSequence
不在input或)
的末尾,所以它调用ParseGroup
。ParseGroup
看到N
,这是一个原子。它把这个原子加到Group
上。然后它看到一个H
,它不是一个数字。所以它设置组的计数为1,然后返回Group
。
- 回到
ParseGroupSeqeunce
,我们将返回的组存储在序列中,然后在循环中迭代。我们没有看到input或)
的结束,所以它调用ParseGroup
:ParseGroup
看到H
,这是一个原子。它把这个原子加到Group
上。然后它看到一个3
,这是一个数字。因此它读取这个数字,将其设置为Group
的计数,并返回Group
。
在
ParseGroupSeqeunce
中,我们将返回的组存储在序列中,然后在循环中迭代。我们没有看到输入的结尾,但是我们看到看到)
。所以我们返回当前的vector<Group>
。
- 回到对
ParseGroup
的第一个调用中,我们得到vector<Group>
。我们把它作为序列插入到当前的Group
中。我们检查下一个字符是否为)
,吃掉它,然后继续。我们看到一个2
,它是一个数字。因此它读取这个数字,将其设置为Group
的计数,并返回Group
。
ParseGroupSequence
调用,我们将返回的Group
存储在序列中,然后在循环中迭代。我们没有看到)
,但是我们看到了输入的结束。所以我们返回当前的vector<Group>
。该解析器使用递归"下降"到每个组。因此,这种解析器被称为"递归下降解析器"(对于这种东西有一个正式的定义,但这是对概念的一个很好的外行理解)。
为您想要读取和识别的字符串写下语法规则通常是有帮助的。语法就是一堆规则,这些规则规定了哪些字符序列是可以接受的,哪些是不可接受的。它有助于在编写程序之前和编写程序时使用语法,并且可以将其输入解析器生成器(如DigitalRoss所描述的)
例如,没有多原子离子的简单化合物的规则如下:
Compound: Component { Component };
Component: Atom [Quantity]
Atom: 'H' | 'He' | 'Li' | 'Be' ...
Quantity: Digit { Digit }
Digit: '0' | '1' | ... '9'
[...]
被读取为可选的,并且将在程序中进行if
测试(要么存在,要么缺失)|
是备选项,if也是备选项。否则如果…else或switch 'test',表示输入必须匹配以下 中的一个{ ... }
被读取为0或更多的重复,并且将在程序 中作为while循环。- 引号之间的字符是字符串中的文字字符。所有其他单词都是规则的名称,对于递归下降解析器来说,最终都是被调用来分割和处理输入的函数的名称。
例如,实现'Quantity'规则的函数只需要读取一个或多个数字字符,并将其转换为整数。实现Atom
规则的函数读取足够的字符以确定它是哪个原子,并将其存储起来。
递归下降解析器的一个优点是错误消息可能非常有用,其形式为"期望一个Atom名称,但得到%c",或"期望一个')',但到达字符串的末尾"。发生错误后恢复有点复杂,因此您可能希望在第一个错误时抛出异常。
那么多原子离子只是一层括号吗?如果是,语法可能是:
Compound: Component { Component }
Component: Atom [Quantity] | '(' Component { Component } ')' [Quantity];
Atom: 'H' | 'He' | 'Li' ...
Quantity: Digit { Digit }
Digit: '0' | '1' | ... '9'
还是更复杂,符号必须允许嵌套括号。一旦弄清楚了这一点,您就可以找出一种解析方法。
我不知道你的问题的全部范围,但是递归下降解析器编写起来相对简单,并且看起来适合你的问题。
考虑将你的程序重构为一个简单的递归下降解析器。
首先,您需要更改parseString
函数,使其接受要解析的string
,以及通过引用传递的要开始解析的当前位置。
这样你可以构造你的代码,当你看到一个(
你调用相同的函数在下一个位置得到一个Composite
返回,并消耗关闭的)
。当您看到单独的)
时,您返回而不使用它。这使您可以使用(
和)
无限嵌套的公式,尽管我不确定是否有必要(距离我上次看到化学式已经有20多年了)。
这样,您只需编写一次解析复合的代码,并根据需要多次重用它。这将很容易补充您的阅读器使用的公式与破折号等,因为您的解析器将只需要处理基本的构建块。
也许您可以在解析之前去掉括号。你需要找出有多少"括号中的括号"(抱歉我的英语不好),然后重写成以"最深的"开头:
-
(NH 4<子>子>(Na<子>2 H<子>子>4子>)<子>3子>锌)2<子>子><子>4子>(这个公式并不意味着什么事,实际上…)
-
(NH 4<子>子>Na<子>6 H<子>子>12子>锌)2<子>子><子>4子>
-
NH<子>8子> 12子>24 H<子>子>锌<子>2子><子>4子>
-
没有括号,让我们运行NH8Na12H24Zn2SO4