1. 程式人生 > 實用技巧 >用C++實現的有理數(分數)四則混合運算計算器

用C++實現的有理數(分數)四則混合運算計算器

實現目標

用C++實現下圖所示的一個console程式:

其中:

1、加減乘除四種運算子號分別用+、-、*、/表示, + 和 - 還分別用於表示正號和負號。

2、分數的分子和分母以符號 / 分隔。

3、支援括號和括號套括號的情形。

4、支援輸入小數(小數點用符號 . 表示)和迴圈小數(迴圈部分起始位置用符號 ` 指定,比如:1.12`345表達的是1.12 345 345 345 ...)。

5、輸入中:允許任意新增空白字元;數字中允許任意新增逗號( , )字元;小數點前的整數為0時允許省略0。

5、輸出中間運算過程和最終運算結果。

6、計算出的最後結果如果不是整數,則採用既約分數形式表達,分子大於分母則採用帶分數形式表達,帶分數的分數部分用中括號( [ 和 ] )括起來。

構建基礎資料結構

有理數的四則運算的本質就是兩個分數(整數是分母為1的分數)的相加和相乘運算,以及分數化簡運算。因此,首先定義出如下表達分數的資料結構:

 1 struct SFraction
 2 {
 3     u64 numerator;
 4     u64 denominator;
 5     bool bNegative;
 6 
 7     SFraction() {
 8         numerator = 0;
 9         denominator = 1;
10         bNegative = false;
11     }
12 
13     std::string
toStr(bool bFinal = false) const 14 { 15 std::ostringstream oStream; 16 if (bNegative) 17 { 18 oStream << "-"; 19 } 20 if (denominator == 1) 21 { 22 oStream << numerator; 23 return oStream.str(); 24 }
25 if (!bFinal || numerator < denominator) 26 { 27 oStream << numerator << "/" << denominator; 28 return oStream.str(); 29 } 30 u64 quotient = numerator / denominator; 31 u64 remainder = numerator % denominator; 32 oStream << quotient << "[" << remainder << "/" << denominator << "]"; 33 return oStream.str(); 34 } 35 };

SFraction定義很簡單,只有三個分量。numerator表示分子,denominator表示分母,bNegative表示該分數的正負符號。SFraction的toStr介面用於輸出對應的分數,介面引數bool bFinal指示該分數是否為最終的運算結果,bFinal為true時,要把分子大於分母的分數以帶分數形式輸出。

兩個分數的四則運算實現

 1 EnumError doOper(const SFraction& oL, const SFraction& oR, char cSign, SFraction& oResult)
 2 {
 3     if (cSign == '+')
 4     {
 5         oResult = fractAdd(oL, oR);
 6     }
 7     else if (cSign == '-')
 8     {
 9         oResult = fractAdd(oL, minusByZero(oR));
10     }
11     else if (cSign == '*')
12     {
13         oResult = fractMultiply(oL, oR);
14     }
15     else if (cSign == '/')
16     {
17         if (oR.numerator == 0)
18         {
19             return E_ERR_ZERO_DENOMINATOR;
20         }
21         oResult = fractMultiply(oL, reciprocal(oR));
22     }
23     else
24     {
25         return E_ERR_INVALID;
26     }
27     return E_ERR_OK;
28 }

函式doOper實現兩個分數(oL和oR)的加減乘除運算,運算結果由oResult帶出。從上面的程式碼可以看出減法運算是轉化成加法運算的(oL加oR的相反數),同樣除法運算也是轉化成乘法運算的(oL乘oR的倒數)。

doOper的返回值為列舉型別,具體定義為:

enum EnumError {E_ERR_OK, E_ERR_EMPTY, E_ERR_ZERO_DENOMINATOR, E_ERR_INVALID, E_ERR_PAREN_UNMATCHED};

求相反數和求倒數的函式實現如下:

 1 SFraction minusByZero(const SFraction& oVal)
 2 {
 3     SFraction oRet = oVal;
 4     oRet.bNegative = (!oRet.bNegative);
 5     return oRet;
 6 }
 7 
 8 SFraction reciprocal(const SFraction& oVal)
 9 {
10     SFraction oRet;
11     oRet.numerator = oVal.denominator;
12     oRet.denominator = oVal.numerator;
13     oRet.bNegative = oVal.bNegative;
14     return oRet;
15 }

分數相加和相乘的函式實現如下:

 1 SFraction fractAdd(const SFraction& oL, const SFraction& oR)
 2 {
 3     SFraction oRslt;
 4     /// having same denominator
 5     if (oL.denominator == oR.denominator)
 6     {
 7         oRslt.denominator = oL.denominator;
 8         if (oL.bNegative == oR.bNegative)
 9         {
10             oRslt.numerator = (oL.numerator + oR.numerator);
11             oRslt.bNegative = oL.bNegative;
12             simplifyFraction(oRslt);
13             return oRslt;
14         }
15         bool bCmp = oL.numerator >= oR.numerator;
16         oRslt.numerator = (bCmp ? oL.numerator - oR.numerator : oR.numerator - oL.numerator);
17         oRslt.bNegative = (bCmp ? oL.bNegative : oR.bNegative);
18         simplifyFraction(oRslt);
19         return oRslt;
20     }
21     /// having different denominator
22     u64 lcm = calcLeastCommonMultiple(oL.denominator, oR.denominator);
23     oRslt.denominator = lcm;
24     u64 numL = oL.numerator * (lcm / oL.denominator);
25     u64 numR = oR.numerator * (lcm / oR.denominator);
26     if (oL.bNegative == oR.bNegative)
27     {
28         oRslt.numerator = numL + numR;
29         oRslt.bNegative = oL.bNegative;
30         simplifyFraction(oRslt);
31         return oRslt;
32     }
33     bool bCmp = (numL >= numR);
34     oRslt.numerator = (bCmp ? numL - numR : numR - numL);
35     oRslt.bNegative = (bCmp ? oL.bNegative : oR.bNegative);
36     simplifyFraction(oRslt);
37     return oRslt;
38 }
39 
40 SFraction fractMultiply(const SFraction& oL, const SFraction& oR)
41 {
42     SFraction oRslt;
43     oRslt.numerator = oL.numerator * oR.numerator;
44     oRslt.denominator = oL.denominator * oR.denominator;
45     oRslt.bNegative = (oL.bNegative != oR.bNegative);
46     simplifyFraction(oRslt);
47     return oRslt;
48 }

其中用到的化簡分數的函式實現如下:

 1 void simplifyFraction(SFraction& oFract)
 2 {
 3     if (oFract.denominator == 1)
 4     {
 5         return;
 6     }
 7     if (oFract.numerator == 0)
 8     {
 9         oFract.denominator = 1;
10         return;
11     }
12     u64 gcd = calcGreatestCommonDivisor(oFract.numerator, oFract.denominator);
13     if (gcd != 1)
14     {
15         oFract.numerator /= gcd;
16         oFract.denominator /= gcd;
17     }
18 }

求兩個整數的最大公約數和最小公倍數的函式實現如下:

 1 u64 calcGcdInn(u64 valBig, u64 valSmall)
 2 {
 3     u64 remainder = valBig % valSmall;
 4     if (remainder == 0)
 5     {
 6         return valSmall;
 7     }
 8     return calcGcdInn(valSmall, remainder);
 9 }
10 
11 u64 calcGreatestCommonDivisor(u64 valA, u64 valB)
12 {
13     if (valA == valB)
14     {
15         return valA;
16     }
17     return (valA > valB ? calcGcdInn(valA, valB) : calcGcdInn(valB, valA));
18 }
19 
20 u64 calcLeastCommonMultiple(u64 valA, u64 valB)
21 {
22     return valA / calcGreatestCommonDivisor(valA, valB) * valB;
23 }

其中求最大公約數用到了歐幾里得演算法,正是Donald E. Knuth在他所著《The Art of Computer Programming》系列的卷1《Fundamental Algorithms》中開篇介紹的演算法,在初等數論中也稱為輾轉相除法。

有理數四則混合運算解析器實現

有理數四則混合運算解析器要完成的任務是:對輸入的一個有理數四則混合運算表示式,從左至右遍歷一遍,提取出參與運算的運算單元(運算數和運算子),按要求的運算順序執行運算,輸出中間運算過程和最終的運算結果。為此,需要用到一個堆疊,提取出的運算單元在不滿足運算條件時需要壓入棧頂(移進),而在滿足運算條件時則需要做歸約操作,即從棧頂彈出參與運算的單元,完成相應運算後再把運算結果壓入棧頂。

為運算單元構建資料結構

 1 struct SOperItem
 2 {
 3     EnumOperItemType eOpItemType;
 4     char cSign;
 5     SFraction oFract;
 6 
 7     SOperItem(EnumOperItemType eType) {
 8         eOpItemType = eType;
 9     }
10     SOperItem(EnumOperItemType eType, char cVal) {
11         eOpItemType = eType;
12         cSign = cVal;
13     }
14     SOperItem(EnumOperItemType eType, const SFraction& oVal) {
15         eOpItemType = eType;
16         oFract = oVal;
17     }
18 
19     std::string toStr() const
20     {
21         if (eOpItemType == E_OP_ITEM_NUM) {
22             return oFract.toStr();
23         }
24         else if (eOpItemType == E_OP_ITEM_LPAREN) {
25             return "(";
26         }
27         return std::string() + cSign;
28     }
29 };

分量 EnumOperItemType eOpItemType指示運算單元的型別。EnumOperItemType 的定義如下:

enum EnumOperItemType {E_OP_ITEM_LPAREN, E_OP_ITEM_NUM, E_OP_ITEM_SIGN};

E_OP_ITEM_NUM 和 E_OP_ITEM_SIGN 分別對應上面提到的運算數單元和運算子單元,而 E_OP_ITEM_LPAREN 用於指代一種特殊的運算單元:左括號。左括號和右括號配對使用,用於控制運算順序。當解析器提取到一個左括號,需要把它壓入堆疊,等待隨後配對的右括號的出現。

每當解析器提取到一個右括號,總是可以執行(一次或多次)運算,一直到把堆疊中配對的左括號彈出,左右括號相抵消的地步,因此,右括號是無需壓入堆疊的。

狀態機狀態設定

在解析器對輸入的有理數四則混合運算表示式做解析的過程中,會涉及到如下幾個狀態:

1 enum EnumState {
2     E_STATE_EXPECT_NUM = 0, // expecting a rational number (or left paren)
3     E_STATE_EXPLICIT,       // with explicit + or - ahead of number (or left paren)
4     E_STATE_MEET_NUM,       // part of a number been met
5     E_STATE_EXPECT_SIGN,    // expecting one of operation signs (+-*/) or right paren
6 };

E_STATE_EXPECT_NUM 為初始狀態,進入該狀態時,期望從剩餘的表示式頭部提取到一個運算數單元或者左括號單元。

當期望一個運算數單元時,碰到了一個 + 號或 - 號(即顯式的正負符號),則進入到 E_STATE_EXPLICIT 狀態,隨後繼續期望從剩餘的表示式頭部提取到一個運算數單元或者左括號單元。

當期望一個運算數單元時,碰到了一個數字元號或小數點符號,則進入到E_STATE_MEET_NUM 狀態。

當提取到一個運算數單元后,進入到E_STATE_EXPECT_SIGN 狀態,隨後期望從剩餘的表示式頭部提取到一個運算子單元或者一個右括號。

有理數四則混合運算解析器類(CRationalCalcor)的對外介面

class CRationalCalcor
{
public:
    CRationalCalcor(const std::string& strVal) {
        m_strExpression = strVal;
        m_eState = E_STATE_EXPECT_NUM;
        m_nParenLevel = 0;
        resetNum();
    }
    EnumError calcIt();
    EnumError getResult(SFraction& oVal);

    ...
};

建構函式的 strVal 引數用於傳入要解析和求值的有理數四則混合運算表示式字串,該串存入內部成員m_strExpression 中;m_eState 為狀態機狀態,初始狀態為E_STATE_EXPECT_NUM;m_nParenLevel 為括號巢狀層數,初始值為0。

main函式對CRationalCalcor的使用

 1 int main(int argc, char* argv[])
 2 {
 3     printf("Rational Calculator version 1.0 by Read Alps\n\n");
 4     printf("Hint: ` denotes the position where the repeating part of a recurring decimal starts.\n\n");
 5     while (true)
 6     {
 7         printf("Please input a rational expression to calculate its value or input q to quit:\n\n ");
 8         std::string strInput;
 9         std::getline(std::cin, strInput);
10         if (strInput == "q")
11         {
12             break;
13         }
14         CRationalCalcor oCalc(strInput);
15         EnumError eErr = oCalc.calcIt();
16         if (eErr == E_ERR_OK)
17         {
18             SFraction oVal;
19             eErr = oCalc.getResult(oVal);
20             if (eErr == E_ERR_OK)
21             {
22                 outputFract(oVal);
23                 continue;
24             }
25         }
26         showErrInfo(eErr);
27     }
28     return 0;
29 }

main 函式的實現邏輯就是迴圈接受互動輸入的表示式字串,然後以得到的字串為輸入引數例項化一個CRationalCalcor 物件,隨後呼叫該物件的 calcIt 介面實施對輸入表示式的遍歷解析和求值運算,然後再呼叫該物件的 getResult 介面獲取最終的運算結果,再呼叫 outputFract 函式輸出最終的運算結果。

CRationalCalcor 類的內部介面和成員

 1 class CRationalCalcor
 2 {
 3 public:
 4     ...12 
13 private:
14     EnumError dealLeftParen();
15     EnumError dealCharWhenExplicit(char ch);
16     EnumError dealCharWhenExpectingNum(char ch);
17     EnumError dealCharWhenExpectingSign(char ch, size_t idx);
18     EnumError dealCharWhenMeetNum(char ch, size_t idx);
19 
20     SFraction currentNum2Fraction();
21     EnumError try2ReduceStack(char ch, size_t idx);
22     EnumError reduceStack(char ch, size_t idx);
23     void outputCalcDetail(size_t idx = 0);
24 
25     std::string m_strExpression;
26     EnumState m_eState;
27     int m_nParenLevel;
28     std::stack<SOperItem> m_stkOpItem;
29     SFraction m_oResult;
30 
31     bool isDecimal() {return (m_nDigitSumAftDot != 0 || m_nDigitSumAftRec != 0);}
32     void resetNum() {
33         m_bHavingVal = false;
34         m_bNegative = false;
35         m_ullIntPart = 0;
36         m_bWithDot = false;
37         m_bWithRecur = false;
38         m_nDigitSumAftDot = 0;
39         m_nDigitSumAftRec = 0;
40         m_ullValAftDot = 0;
41         m_ullValAftRec = 0;
42     }
43 
44     bool m_bHavingVal;
45     bool m_bNegative;
46     u64 m_ullIntPart;
47     bool m_bWithDot;
48     bool m_bWithRecur;
49     int m_nDigitSumAftDot; // sum of digits after dot char(.)
50     int m_nDigitSumAftRec; // sum of digits after recurring char(`)
51     u64 m_ullValAftDot;
52     u64 m_ullValAftRec;
53 };

這裡簡單說明一下CRationalCalcor 類的內部成員:

28     std::stack<SOperItem> m_stkOpItem; 就是上文提及的那個堆疊。
29     SFraction m_oResult; 存放表示式最終的運算結果。
44     bool m_bHavingVal;
45     bool m_bNegative;
46     u64 m_ullIntPart;
47     bool m_bWithDot;
48     bool m_bWithRecur;
49     int m_nDigitSumAftDot; // sum of digits after dot char(.)
50     int m_nDigitSumAftRec; // sum of digits after recurring char(`)
51     u64 m_ullValAftDot;
52     u64 m_ullValAftRec;

這9個成員用來表示當前正從表示式中提取出的運算數,因為允許輸入小數以及迴圈小數,表示式中提取出的數最多可由三部分組成:有限位整數部分、有限位小數部分和無限迴圈小數部分。比如,12.345`6789所對應的這三部分分別是12、345、6789。

m_bHavingVal 指示當前是否正提取到一個有效的運算數;m_bNegative 指示運算數的正負符號;m_ullIntPart 指代運算數的整數部分(即上例中的12);m_bWithDot 表示運算數是否帶有小數點;m_bWithRecur 表示運算數是否帶有迴圈小數標記符;m_nDigitSumAftDot 表示運算數的有效位小數部分所佔的位數;m_nDigitSumAftRec 表示運算數的迴圈小數部分的迴圈體所佔的位數;m_ullValAftDot 指代運算數的有限位小數部分(即上例中的345);m_ullValAftRec 指代運算數的無限迴圈小數部分(即上例中的6789)。

resetNum 介面用來對這個運算數做清除處理,以便後續提取新的運算數。isDecimal 介面判斷當前提取到的運算數是否帶有小數部分。

CRationalCalcor::calcIt 介面實現

 1 EnumError CRationalCalcor::calcIt()
 2 {
 3     trimString(m_strExpression);
 4     if (m_strExpression.empty())
 5         return E_ERR_EMPTY;
 6     EnumError eRet = E_ERR_OK;
 7     for (size_t idx = 0; idx < m_strExpression.length(); ++idx)
 8     {
 9         char ch = m_strExpression[idx];
10         if (ch == ' ' || ch == '\t')
11             continue;
12         if (ch == '(')
13         {
14             if ((eRet = dealLeftParen()) != E_ERR_OK)
15                 return eRet;
16             continue;
17         }
18         switch (m_eState)
19         {
20         case E_STATE_EXPECT_SIGN:
21             if ((eRet = dealCharWhenExpectingSign(ch, idx)) != E_ERR_OK)
22                 return eRet;
23             break;
24         case E_STATE_EXPECT_NUM:
25             if ((eRet = dealCharWhenExpectingNum(ch)) != E_ERR_OK)
26                 return eRet;
27             break;
28         case E_STATE_EXPLICIT:
29             if ((eRet = dealCharWhenExplicit(ch)) != E_ERR_OK)
30                 return eRet;
31             break;
32         case E_STATE_MEET_NUM:
33             if ((eRet = dealCharWhenMeetNum(ch, idx)) != E_ERR_OK)
34                 return eRet;
35             break;
36         default:
37             return E_ERR_INVALID;
38         }
39     } // end of loop
40     return E_ERR_OK;
41 }

其中的 for 迴圈實現對互動輸入的表示式字串做遍歷,提取運算單元,根據狀態進行移進或歸約操作。

CRationalCalcor::dealLeftParen 介面:對左括號的處理

 1 EnumError CRationalCalcor::dealLeftParen()
 2 {
 3     if (m_eState != E_STATE_EXPECT_NUM && m_eState != E_STATE_EXPLICIT)
 4     {
 5         return E_ERR_INVALID;
 6     }
 7     if (m_eState == E_STATE_EXPLICIT && m_bNegative)
 8     {
 9         m_bNegative = false;
10         SFraction oZero;
11         SOperItem oItemZero(E_OP_ITEM_NUM, oZero);
12         m_stkOpItem.push(oItemZero);
13         SOperItem oItemMinus(E_OP_ITEM_SIGN, '-');
14         m_stkOpItem.push(oItemMinus);
15     }
16     m_eState = E_STATE_EXPECT_NUM;
17     SOperItem oItem(E_OP_ITEM_LPAREN);
18     m_stkOpItem.push(oItem);
19     ++m_nParenLevel;
20     return E_ERR_OK;
21 }

狀態變換處理介面:CRationalCalcor::dealCharWhenExpectingNum

 1 EnumError CRationalCalcor::dealCharWhenExpectingNum(char ch)
 2 {
 3     if (!isAddOrMinus(ch) && !isNumChar(ch) && ch != '.')
 4     {
 5         return E_ERR_INVALID;
 6     }
 7     if (isAddOrMinus(ch))
 8     {
 9         m_bNegative = (ch == '-');
10         m_eState = E_STATE_EXPLICIT;
11         return E_ERR_OK;
12     }
13     if (ch == '.')
14     {
15         m_bWithDot = true;
16         m_bHavingVal = true;
17         m_eState = E_STATE_MEET_NUM;
18         return E_ERR_OK;
19     }
20     m_ullIntPart = (u64)(ch - '0');
21     m_bHavingVal = true;
22     m_eState = E_STATE_MEET_NUM;
23     return E_ERR_OK;
24 }

介面引數 ch 指代對錶達式字串當前正遍歷到的那個字元。

狀態變換處理介面:CRationalCalcor::dealCharWhenExplicit

 1 EnumError CRationalCalcor::dealCharWhenExplicit(char ch)
 2 {
 3     if (!isNumChar(ch) && ch != '.')
 4     {
 5         return E_ERR_INVALID;
 6     }
 7     if (ch == '.')
 8     {
 9         m_bWithDot = true;
10         m_bHavingVal = true;
11         m_eState = E_STATE_MEET_NUM;
12         return E_ERR_OK;
13     }
14     m_ullIntPart = m_ullIntPart * 10 + (u64)(ch - '0');
15     m_bHavingVal = true;
16     m_eState = E_STATE_MEET_NUM;
17     return E_ERR_OK;
18 }

狀態變換處理介面:CRationalCalcor::dealCharWhenMeetNum

 1 EnumError CRationalCalcor::dealCharWhenMeetNum(char ch, size_t idx)
 2 {
 3     if (ch == ',')
 4         return E_ERR_OK;
 5     if (ch == '.')
 6     {
 7         if (m_bWithDot)
 8             return E_ERR_INVALID;
 9         m_bWithDot = true;
10         return E_ERR_OK;
11     }
12     if (ch == '`')
13     {
14         if (!m_bWithDot || m_bWithRecur)
15             return E_ERR_INVALID;
16         m_bWithRecur = true;
17         return E_ERR_OK;
18     }
19     if (isNumChar(ch))
20     {
21         if (!m_bWithDot)
22         {
23             m_ullIntPart = m_ullIntPart * 10 + (u64)(ch - '0');
24             return E_ERR_OK;
25         }
26         if (!m_bWithRecur)
27         {
28             m_ullValAftDot = m_ullValAftDot * 10 + (u64)(ch - '0');
29             ++m_nDigitSumAftDot;
30             return E_ERR_OK;
31         }
32         m_ullValAftRec = m_ullValAftRec * 10 + (u64)(ch - '0');
33         ++m_nDigitSumAftRec;
34         return E_ERR_OK;
35     }
36     /// ch does not belong to the current rational number
37     SFraction oFract = currentNum2Fraction();
38     SOperItem oItem(E_OP_ITEM_NUM, oFract);
39     m_stkOpItem.push(oItem);
40     if (isDecimal())
41     {
42         outputCalcDetail(idx);
43     }
44     resetNum();
45     if (ch == ')' || isAddOrMinus(ch) || isAsteriskOrSlash(ch))
46     {
47         EnumError eErr = try2ReduceStack(ch, idx);
48         if (eErr != E_ERR_OK)
49         {
50             return eErr;
51         }
52     }
53     else
54     {
55         return E_ERR_INVALID;
56     }
57     return E_ERR_OK;
58 }

36行之後的邏輯,這裡解釋一下。此時,ch 不是數字符號,不是逗號,也不是小數點或迴圈小數標記符號,說明運算數已經提取完成,因此可以呼叫介面currentNum2Fraction 把當前提取到的運算數轉成 SFraction 格式的表示形式,並壓堆疊頂。40-43行的程式碼邏輯是:如果新提取的運算數帶有小數部分,則呼叫 outputCalcDetail 介面把小數化分數的處理作為中間運算結果輸出到互動介面上。45-47行的程式碼邏輯是:如果 ch 是右括號或者是+-*/之一,則呼叫 try2ReduceStack 介面試圖對當前堆疊做歸約操作。

狀態變換處理介面:CRationalCalcor::dealCharWhenExpectingSign

 1 EnumError CRationalCalcor::dealCharWhenExpectingSign(char ch, size_t idx)
 2 {
 3     if (ch != ')' && !isAddOrMinus(ch) && !isAsteriskOrSlash(ch))
 4     {
 5         return E_ERR_INVALID;
 6     }
 7     EnumError eErr = try2ReduceStack(ch, idx);
 8     if (eErr != E_ERR_OK)
 9     {
10         return eErr;
11     }
12     return E_ERR_OK;
13 }

試圖歸約介面:CRationalCalcor::try2ReduceStack

 1 EnumError CRationalCalcor::try2ReduceStack(char ch, size_t idx)
 2 {
 3     if (m_stkOpItem.empty())
 4     {
 5         return E_ERR_INVALID;
 6     }
 7     if (m_stkOpItem.size() == 1)
 8     {
 9         if (ch == ')')
10         {
11             return E_ERR_PAREN_UNMATCHED;
12         }
13         SOperItem oItem(E_OP_ITEM_SIGN, ch);
14         m_stkOpItem.push(oItem);
15         m_eState = E_STATE_EXPECT_NUM;
16         return E_ERR_OK;
17     }
18     if (m_stkOpItem.size() == 2)
19     {
20         if (ch == ')' && m_nParenLevel == 0)
21         {
22             return E_ERR_PAREN_UNMATCHED;
23         }
24         return reduceStack(ch, idx);
25     }
26     if (isAsteriskOrSlash(ch))
27     {
28         SOperItem oItemNumLast = m_stkOpItem.top();
29         m_stkOpItem.pop();
30         SOperItem oItemSign = m_stkOpItem.top();
31         m_stkOpItem.push(oItemNumLast);
32         if (isAddOrMinus(oItemSign.cSign))
33         {
34             SOperItem oItem(E_OP_ITEM_SIGN, ch);
35             m_stkOpItem.push(oItem);
36             m_eState = E_STATE_EXPECT_NUM;
37             return E_ERR_OK;
38         }
39     }
40     return reduceStack(ch, idx);
41 }

13-16行的程式碼邏輯實際是移進,這是因為堆疊裡只有一個單元(即只有一個運算數單元),還不滿足歸約條件,因而需要把當前運算子單元入棧並把狀態切換到E_STATE_EXPECT_NUM。

18-25行的程式碼邏輯是:如果堆疊裡有兩個單元,只要不是左右括號不匹配的情形,就呼叫 reduceStack 介面做歸約操作。

26-40行的程式碼邏輯是:堆疊裡有三個或更多個單元,如果當前運算子是乘除之一,則進一步考察堆疊頂部之下的運算子是否為加減之一,是則做移進操作,因為乘除運算優先於加減運算;其它情形,則呼叫 reduceStack 介面做歸約操作。

歸約介面:CRationalCalcor::reduceStack

 1 EnumError CRationalCalcor::reduceStack(char ch, size_t idx)
 2 {
 3     SOperItem oItemNum2nd = m_stkOpItem.top();
 4     m_stkOpItem.pop();
 5     SOperItem oItemSign = m_stkOpItem.top();
 6     m_stkOpItem.pop();
 7     if (oItemSign.eOpItemType == E_OP_ITEM_LPAREN)
 8     {
 9         if (ch == ')')
10         {
11             m_stkOpItem.push(oItemNum2nd);
12             m_nParenLevel--;
13             m_eState = E_STATE_EXPECT_SIGN;
14         }
15         else
16         {
17             m_stkOpItem.push(oItemSign);
18             m_stkOpItem.push(oItemNum2nd);
19             SOperItem oItemNew(E_OP_ITEM_SIGN, ch);
20             m_stkOpItem.push(oItemNew);
21             m_eState = E_STATE_EXPECT_NUM;
22         }
23         return E_ERR_OK;
24     }
25     SOperItem oItemNum1st = m_stkOpItem.top();
26     m_stkOpItem.pop();
27     SFraction oVal;
28     EnumError eErr = doOper(oItemNum1st.oFract, oItemNum2nd.oFract, oItemSign.cSign, oVal);
29     if (eErr != E_ERR_OK)
30     {
31         return eErr;
32     }
33     SOperItem oItemNum(E_OP_ITEM_NUM, oVal);
34     m_stkOpItem.push(oItemNum);
35     outputCalcDetail(idx);
36 
37     if (ch == ')')
38     {
39         return reduceStack(ch, idx);
40     }
41     return try2ReduceStack(ch, idx);
42 }

3-6行邏輯是:從棧頂依次彈出兩個單元,即第二運算數和運算子。

7-24行邏輯是:如果彈出的運算子實際只是個左括號,則進一步考察 ch 是否為右括號,是則左右括號相抵消,只把之前彈出的第二運算數再壓入棧頂;否則,依次把左括號和第二運算數再依次壓回棧頂,並把 ch 對應的運算子壓入棧頂,並把狀態切換至E_STATE_EXPECT_NUM(這種情形下實際做的是移進操作)。

25-35行的主要邏輯是:從棧頂彈出第一運算子,呼叫 doOper 函式實施二元運算,並把運算結果壓入棧頂,完成一次歸約處理。

37-41行的邏輯是:如果 ch 是右括號則呼叫 reduceStack 介面做進一步歸約處理;否則,呼叫 try2ReduceStack 介面做試圖歸約處理。

CRationalCalcor::currentNum2Fraction介面實現

 1 SFraction CRationalCalcor::currentNum2Fraction()
 2 {
 3     SFraction oFract;
 4     oFract.numerator = m_ullIntPart;
 5     if (m_bWithDot && m_nDigitSumAftDot)
 6     {
 7         SFraction oDec;
 8         oDec.numerator = m_ullValAftDot;
 9         oDec.denominator = powerBase10(m_nDigitSumAftDot);
10         oFract = fractAdd(oFract, oDec);
11     }
12     if (m_bWithRecur && m_nDigitSumAftRec)
13     {
14         SFraction oRec;
15         oRec.numerator = m_ullValAftRec;
16         oRec.denominator = (powerBase10(m_nDigitSumAftRec) - 1) * powerBase10(m_nDigitSumAftDot);
17         oFract = fractAdd(oFract, oRec);
18     }
19     oFract.bNegative = m_bNegative;
20     return oFract;
21 }

currentNum2Fraction 介面把當前提取到的運算數轉成 SFraction 格式的表示形式,其中涉及把運算數的兩個小數部分轉化成分數形式並和整數部分相加的邏輯實現。裡面用到的 powerBase10 函式實現如下:

1 u64 powerBase10(int num)
2 {
3     u64 ret = 1;
4     for (int idx = 0; idx < num; ++idx)
5     {
6         ret = ret * 10;
7     }
8     return ret;
9 }

CRationalCalcor::getResult 介面實現

CRationalCalcor::calcIt 介面一執行完,輸入的表示式字串就遍歷完成了。當表示式的最後一個字元不是右括號時,執行完 calcIt 介面,堆疊裡有可能還有多個運算單元,需要進一步做歸約操作。CRationalCalcor::getResult 介面完成這項收尾工作,並把最終的運算結果通過輸出引數帶出來,具體實現如下:

 1 EnumError CRationalCalcor::getResult(SFraction& oVal)
 2 {
 3     if (m_bHavingVal)
 4     {
 5         SFraction oFract = currentNum2Fraction();
 6         SOperItem oItem(E_OP_ITEM_NUM, oFract);
 7         m_stkOpItem.push(oItem);
 8         if (m_stkOpItem.size() != 1 && isDecimal())
 9         {
10             outputCalcDetail();
11         }
12         resetNum();
13     }
14     while (m_stkOpItem.size() >= 3)
15     {
16         SFraction oR = m_stkOpItem.top().oFract;
17         m_stkOpItem.pop();
18         char ch = m_stkOpItem.top().cSign;
19         m_stkOpItem.pop();
20         SFraction oL = m_stkOpItem.top().oFract;
21         m_stkOpItem.pop();
22         SFraction oVal;
23         EnumError eErr = doOper(oL, oR, ch, oVal);
24         if (eErr != E_ERR_OK)
25         {
26             return eErr;
27         }
28         SOperItem oItem(E_OP_ITEM_NUM, oVal);
29         m_stkOpItem.push(oItem);
30         if (m_stkOpItem.size() != 1)
31         {
32             outputCalcDetail();
33         }
34     }
35     if (m_stkOpItem.empty() || m_stkOpItem.size() == 2)
36     {
37         return E_ERR_INVALID;
38     }
39     oVal = m_stkOpItem.top().oFract;
40     simplifyFraction(oVal);
41     return E_ERR_OK;
42 }

運算過程和運算結果輸出

運算過程輸出在CRationalCalcor::outputCalcDetail 介面實現,具體如下:

 1 void CRationalCalcor::outputCalcDetail(size_t idx)
 2 {
 3     std::string strDetail;
 4     std::stack<SOperItem> stkOpItem = m_stkOpItem;
 5     while (!stkOpItem.empty())
 6     {
 7         SOperItem item = stkOpItem.top();
 8         strDetail = item.toStr() + " " + strDetail;
 9         stkOpItem.pop();
10     }
11     if (idx != 0)
12     {
13         strDetail += m_strExpression.substr(idx);
14     }
15     printf(" = %s\n", strDetail.c_str());
16 }

運算結果輸出在 函式裡實現,如下:

1 void outputFract(const SFraction& oVal)
2 {
3     printf(" = %s\n\n", oVal.toStr(true).c_str());
4 }

完整程式碼檔案

完整程式碼檔案可以從如下位置提取:

https://github.com/readalps/RationalCalculator

三個檔案(RationalCalcor.h, RationalCalcor.cpp, main.cpp),總計約700行程式碼。