(c#)計算字串的值,可用於實現計算器。採用後綴表達式(逆波蘭式)。

2020-08-16 09:09:32

 直接看程式碼,裏面有註釋。

using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;

namespace Scan_demo
{
	/*(一)類名:CalculateString
	 * 功能:計算表達式字串的值。當前支援的運算子包括加(+)減(-)乘(*)除(/)冪(^),支援圓括號(),支援小數。
	 * 例如:輸入字串" -(369.9+(- (- 55   *6))) + 2.5*3^2",計算後得到數位-677.2
	 * 
	 *(二)使用的工具類及其功能:
	 *	string: 存放輸入的字串;
	 *	Regex: 檢查語法;
	 *	StringBuilder:對字串進行預處理,如去掉空格、新增分隔符。再利用string類的Split方法把字串分解爲字串陣列。
	 *	Stack:利用其記憶功能,處理計算中的優先順序問題。
	 *	double:把字串轉成double數值
	 *	
	 *(三)主要演算法:
	 *	1、生成後綴表達式;
	 *	2、計算後綴表達式;	
	 * 
	 */

	class CalculateString
	{
		/// <summary>
		/// 計算字串表達式的值。
		/// </summary>
		/// <param name="exp">輸入的表達式字串</param>
		/// <returns>返回計算值,爲double型別</returns>
		public static double Compute(string exp)
		{
			if (!checkRules(exp))
			{				
				throw new FormatException("字串爲空或不合法");
			}

			//先把字串轉換成後綴表達式字串陣列
			string[] rpn = toRPN(toStrings(exp));

			//再計算後綴表達式
			Stack<double> stack = new Stack<double>(); //存放參與計算的數值、中間值、結果	
			
			//演算法:利用foreach來掃描後綴表達式字串陣列,得到數值則直接壓入棧中,
			//得到運算子則從棧頂取出兩個數值進行運算,並把結果壓入棧中。最終棧中留下一個數值,爲計算結果。
			foreach (string oprator in rpn)  
			{
				//爲什麼總是彈出兩個數值?因爲都是雙目運算。
				//先彈出的是運算子右邊的數,彈出兩個數值後注意運算順序。				
				switch (oprator)
				{
					case "+":
						//如果讀取到運算子,則從stack中取出兩個數值進行運算,並把運算結果壓入stack。下同。
						stack.Push(stack.Pop() + stack.Pop());  
						break;
					case "-":
						stack.Push(-stack.Pop() + stack.Pop());
						break;
					case "*":
						stack.Push(stack.Pop() * stack.Pop());
						break;
					case "/":
						{
							double right = stack.Pop();
							try
							{
								stack.Push(stack.Pop() / right); 
							}
							catch (Exception e)
							{
								throw e;   //除數爲0時產生異常。
							}
							
							break;
						}
					case "^":
						{
							double right = stack.Pop();
							stack.Push(Math.Pow(stack.Pop(),right));
							break;
						}

					default: //後綴表達式陣列中只有運算子和數值,沒有圓括號。除了運算子,剩下的就是數值了
						stack.Push(double.Parse(oprator));  //如果讀取到數值,則壓入stack中
						break;
				}
			}

			//彈出最後的計算值並返回
			return stack.Pop();
		}

		/// <summary>
		/// 檢查字串,判斷是否滿足表達式的語法要求。
		/// </summary>
		/// <param name="exp">表達式字串</param>
		/// <returns>字串爲空或不滿足表達式語法要求時返回false,否則返回true</returns>
		public static bool checkRules(string exp)
		{
			if (string.IsNullOrWhiteSpace(exp))
			{
				return false;
			}

			//去掉空格
			string noBlank = Regex.Replace(exp, " ", "");

			//Console.WriteLine(noBlank);
			
			//表達式字串規則。規則之間有配合關係,以避免重疊。
			string no0 = @"[^\d\*\^\(\)+-/.]";  //規則0:不能出現運算子+-*/^、圓括號()、數位、小數點.之外的字元
			string no1 = @"^[^\d\(-]";          //規則1:開頭不能是數位、左圓括號(、負號- 以外的字元
			string no2 = @"[^\d\)]$";           //規則2:結束不能是數位、右圓括號) 以外的字元
			string no3 = @"[\*\^+-/]{2}";       //規則3:+-*/^不能連續出現
			string no4 = @"[\D][.]|[.]\D";      //規則4:小數點前面或後面不能出現非數位字元
			string no5 = @"\)[\d\(]|[^\d\)]\)"; //規則5:右圓括號)後面不能出現數字、左圓括號(,前面不能出現除數位或右圓括號)以外的字元
			string no6 = @"\([^\d\(-]|[\d]\(";  //規則6:左圓括號(後面不能出現除數位、左圓括號(、負號以外的字元,前面不能出現數字

			string pattern = no0 + "|" + no1 + "|" + no2 + "|" + no3 + "|" + no4 + "|" + no5 + "|" + no6;
			if (Regex.IsMatch(noBlank, pattern))
			{
				return false;
			}

			//規則7:左圓括號(和右圓括號)必須成對出現
			int count = 0;
			foreach (char c in noBlank)
			{
				if (c == ')')
				{
					count++;
					continue;
				}

				if (c == '(')
				{
					count--;
					continue;
				}
			}
			if (count != 0)
			{
				Console.WriteLine("左右括號不匹配");
				return false;
			}

			return true;
		}


		/// <summary>
		/// 將表達式字串轉換成字串陣列,如"-2*57+6"轉換成["-2","*","57","+","6"]。請自行確保表達式字串正確。
		/// </summary>
		/// <param name="exp">表達式字串</param>
		/// <returns>字串陣列</returns>
		public static string[] toStrings(string exp)
		{
			//通過新增分割符','把數位和其它字元分隔開。

			StringBuilder sb = new StringBuilder(exp);

			//去掉空格
			sb.Replace(" ","");
			
			

			//負號與減號相同,不能與數位分開,需要特許處理:
			//1、字串第一個"-"是負號
			//2、緊跟在"("後面的"-"是負號
			//3、如果負號後面直接跟着"(",把負號改爲"-1*"。目的是把取負運算(單目運算)變成乘法運算(雙目運算),免得以後要區分減法和取負。
			//4、把負號統一改爲"?"			
			if (sb[0] == '-' )
			{
				sb[0] = '?';
			}

			sb.Replace("(-", "(?");

			sb.Replace("?(", "?1*(");

			//新增分割符','把數位和其它字元分隔開。
			sb.Replace("+", ",+,");
			sb.Replace("-", ",-,");
			sb.Replace("*", ",*,");
			sb.Replace("/", ",/,");
			sb.Replace("(", "(,");
			sb.Replace(")", ",)");
			sb.Replace("^", ",^,");

			//分割之後,把'?'恢復成減號 '-'
			sb.Replace('?', '-');			

			return sb.ToString().Split(',');			
		}

		/// <summary>
		/// 生成後波蘭表達式(後綴表達式)
		/// </summary>
		/// <param name="expStrings">字串陣列(中綴表達式)</param>
		/// <returns>字串陣列(後綴表達式)</returns>
		public static string[] toRPN(string[] expStrings)
		{
			Stack<string> stack = new Stack<string>();
			List<string> rpn = new List<string>();

			//基本思路:
			//遍歷expStrings中的字串:
			//1、如果不是字元(即數位)就直接放到列表rpn中;
			//2、如果是字元:
			//2.1、如果stack爲空,把字元壓入stack中;
			//2.2、如果stack不爲空,把棧中優先順序大於等於該字元的運算子全部彈出(直到碰到'('或stack爲空),放到rpn中;
			//2.2 如果字元是'(',直接壓入
			//2.3 如果是')',依次彈出stack中的字串放入rpn中,直到碰到'(',彈出並拋棄'(';
			foreach (string item in expStrings)
			{
				//1、處理"("
				if (item == "(")
				{
					stack.Push(item);
					continue;
				}

				//2、處理運算子 +-*/^
				if ("+-*/^".Contains(item))
				{
					if (stack.Count == 0 )
					{
						stack.Push(item);
						continue;
					}

					if (getOrder(item[0]) > getOrder(stack.Peek()[0]))
					{
						stack.Push(item);
						continue;
					}
					else
					{
						while (stack.Count > 0 && getOrder(stack.Peek()[0])>= getOrder(item[0]) && stack.Peek() != "(")
						{
							rpn.Add(stack.Pop());
						}
						stack.Push(item);
						continue;
					}					
				}

				//3、處理")"
				if (item == ")")
				{
					while (stack.Peek() != "(")
					{
						rpn.Add(stack.Pop());
					}
					stack.Pop();//拋棄"("
					continue;
				}

				//4、數據,直接放入rpn中
				rpn.Add(item);
			}

			//最後把stack中的運算子全部輸出到rpn
			while (stack.Count > 0)
			{
				rpn.Add(stack.Pop());
			}			


			//把字串鏈表轉換成字串陣列,並輸出。
			return rpn.ToArray();

		}

		/// <summary>
		/// 獲取運算子的優先順序
		/// </summary>
		/// <param name="oprator">運算子</param>
		/// <returns>運算子的優先順序。</returns>
		private static int getOrder(char oprator)
		{
			switch (oprator)
			{
				case '+':
				case '-':
					return 1;
				case '*':
				case '/':
					return 3;
				case '^':				
					return 5;
				//case '(':
				//	return 10;					
				default:
					return -1;					
			}
		}
	}
}