[英]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.