[英]Chemical formula parser C++
我目前正在开发一个程序,可以解析化学式并返回分子量和百分比组成。 以下代码适用于H 2 O,LiOH,CaCO 3甚至C 12 H 22 O 11等化合物。 然而,它不能理解具有位于括号内的多原子离子的化合物,例如(NH 4 ) 2 SO 4 。
我不是在寻找一个必须为我编写程序的人,而只是给我一些关于如何完成这项任务的技巧。
目前,程序遍历输入的字符串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;
}
这是我的第一篇文章,所以如果我收录的信息太少(或者说太多),请原谅我。
您的解析器了解某些事情。 它知道当它看到N
时,这意味着“氮原子类型”。 当它看到O
,它意味着“氧气类型原子”。
这与C ++中的标识符概念非常相似。 当编译器看到int someNumber = 5;
它说,“存在一个名为someNumber
的int
类型的变量,其中存储了数字5
”。 如果以后使用的名称someNumber
,它知道你在谈论的是 someNumber
(只要你在正确的范围是)。
回到你的原子解析器。 当你的解析器看到一个后跟一个数字的原子时,它知道将该数字应用于那个原子。 所以O2
意思是“2种氧原子”。 N2
表示“2种氮原子”。
这对你的解析器意味着什么。 这意味着看到一个原子是不够的 。 这是一个良好的开端,但仅知道分子中存在多少原子是不够的。 它需要阅读下一件事。 因此,如果它看到O
后跟N
,则它知道O
表示“1氧原子类型”。 如果它看到O
后面没有任何东西(输入结束),那么它再次表示“1氧气类型原子”。
这就是你目前所拥有的。 但这是错的 。 因为数字并不总是修改原子; 有时,它们会修改原子组 。 如(NH4)2SO4
。
所以现在,您需要更改解析器的工作方式 。 当它看到O
,它需要知道这不是“氧气类型原子”。 它是“含氧组 ”。 O2
是“含氧的2个基团”。
一个组可以包含一个或多个原子。 所以当你看到(
你知道你正在创建一个组 。因此,当你看到(...)3
,你会看到“3个包含...的组”。
那么, (NH4)2
什么? 它是“含有[1个含氮的基团,然后含有4个含氢基团]的2个基团”。
这样做的关键是理解我刚写的内容。 组可以包含其他组 。 有小组嵌套。 你如何实现嵌套?
好吧,你的解析器目前看起来像这样:
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()
直到输入运行干,将每个atom + count存储在一个数组中。 显然你在那里有一些错误检查,但是现在让我们忽略它。
你需要做的是停止解析原子。 您需要解析组 ,这些组可以是单个原子,也可以是由()
对表示的一组原子。
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
函数ParseAtom
)将调用ParseGroupSequence
。 这将调用ParseGroup
。 哪个可以调用ParseGroupSequence
。 等等。一个Group
可以包含原子或一Group
S(如NH4
),存储为vector<Group>
当函数可以自己调用(直接或间接)时,它被称为递归 。 哪个好,只要不能无限递归。 并且没有机会,因为它只会在每次看到时递归(
。
那么这是如何工作的呢? 好吧,让我们考虑一些可能的输入:
ParseGroupSequence
。 它不在输入或)
的末尾,所以它调用ParseGroup
。
ParseGroup
看到一个N
,它是一个原子。 它将此原子添加到Group
。 然后它看到一个H
,这不是一个数字。 因此,它将Group
的计数设置为1,然后返回Group
。 ParseGroupSeqeunce
,我们将返回的组存储在序列中,然后在循环中迭代。 我们没有看到输入结束或)
,所以它调用ParseGroup
:
ParseGroup
看到一个H
,它是一个原子。 它将此原子添加到Group
。 然后它看到一个3
,这是一个数字。 因此,它读取此数字,将其设置为Group
的计数,并返回该Group
。 ParseGroupSeqeunce
,我们将返回的Group
存储在序列中,然后在循环中迭代。 我们没有看到)
,但我们确实看到了输入的结束。 所以我们返回当前vector<Group>
。 ParseGroupSequence
。 它不在输入或)
的末尾,所以它调用ParseGroup
。
ParseGroup
看到一个(
它是一个Group
的开头。它吃掉这个字符(从输入中删除它)并在Group
上调用ParseGroupSequence
。
ParseGroupSequence
不在输入或)
的末尾,因此它调用ParseGroup
。
ParseGroup
看到一个N
,它是一个原子。 它将此原子添加到Group
。 然后它看到一个H
,这不是一个数字。 因此,它将组的计数设置为1,然后返回Group
。 ParseGroupSeqeunce
,我们将返回的组存储在序列中,然后在循环中迭代。 我们没有看到输入结束或)
,所以它调用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 if .. else或switch'test',它表示输入必须匹配其中一个 { ... }
被读作0或更多的重复,并且将是程序中的while循环 例如,实现“数量”规则的函数只需读取一个或多个数字字符,并将它们转换为整数。 实现Atom
规则的函数读取足够的字符以确定它是哪个原子,并将其存储起来。
关于递归下降解析器的一个好处是错误消息可能非常有用,并且形式为“期望一个Atom名称,但得到%c”或“期待一个”)但是到达了字符串的结尾“。 在发生错误后恢复有点复杂,因此您可能希望在第一个错误时抛出异常。
那么多原子离子只是括号的一个层次吗? 如果是这样,语法可能是:
Compound: Component { Component }
Component: Atom [Quantity] | '(' Component { Component } ')' [Quantity];
Atom: 'H' | 'He' | 'Li' ...
Quantity: Digit { Digit }
Digit: '0' | '1' | ... '9'
或者它更复杂,并且符号必须允许嵌套括号。 一旦清楚,您就可以找到解析的方法。
我不知道你的问题的整个范围,但递归下降解析器编写相对简单,并且看起来足够你的问题。
也许你可以在解析之前摆脱括号。 你需要找到多少“括号中的括号”(对不起我的英语),然后重写它就像从“最深”开始的那样:
(NH 4 (Na 2 H 4 ) 3 Zn) 2 SO 4 (这个公式并不意味着任何,实际......)
(NH 4 Na 6 H 12 Zn) 2 SO 4
NH 8 Na 12 H 24 Zn 2 SO 4
没有括号,让我们用NH 8 Na 12 H 24 Zn 2 SO 4运行您的代码
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.