用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行代码。