来源: http://msdn.microsoft.com/zh-tw/ee854988.aspx
數學運算式的愛恨情仇
在撰寫像工程運算或是商業統計類型的應用程式時,有時候都會需要撰寫一些處理運算式(expression)的程式,以處理自訂的運算或是評估執行結果等等工作,像是銷售統計、客戶效益評估、三角函數運算以及其他的數學計算等等,這些運算式通常都是用這個方式呈現:
z = sin(x) + sin(y) amount = amount + (0.05 * amount) + bonus_value
若是要做到由使用者自行輸入的運算式,則要撰寫的程式會複雜的多,一般都會使用堆疊(stack)或是二元運算樹(binary expression tree)為基礎實作,還要配合執行順序的變化來安排運算子的優先順序(例如乘除比加減要高,括號內的運算要比一般的運算優先序要高),若是要加上判斷邏輯的話,要考量的可就更多了。如果在大學有上過資料結構的課程的話,應該會對運算式不陌生,前序(Preorder)、中序(Inorder)與後序(Postorder)運算等是基本的運算式知識。除了一般的數學運算式以外,運算式的知識還可以廣泛用在很多地方,像是 XML 的 XPath,或是大量用在字串解析的規則運算式(Regular Expression)都是特殊類型的運算式,其規則都是由開發者自訂的。不過一般的開發人員比較不太需要撰寫到此類較高深的剖析運算式,只要知道如何撰寫一般的數學運算式就可以了。
然而,就算是撰寫一般的數學運算式,難度仍然不會比寫一般的程式要低,這時很多人都會想到可愛的 JavaScript 的 eval() 方法,這是一個可以自動評估運算式的小函式,簡單的加減乘除運算式是可以計算出來的,但是在 .NET Framework 中反而就沒有類似這樣的函式,雖然有一些方法可以實作出類似於 eval() 方法的功能,不過大多數的開發人員都不知道如何做。因此網路上也有一些開發人員分享出自己處理運算式的處理方式,其中 NCalc 就是在 Codeplex 上,處理數學運算式的工具類別。
NCalc 簡介
NCalc 由 Evaluant 公司的研發團隊開發,是在 .NET Framework 中處理運算式字串(expression string)並回傳結果的一種工具類別庫,它支援數種數學運算、邏輯運算以及自訂函數運算的功能,例如這樣的程式碼:
Expression e = new Expression("2 + 3 * 5"); Debug.Assert(17 == e.Evaluate());
NCalc 能夠直接剖析運算式字串,並且求出其結果,這對經常要撰寫自訂運算式的開發人員來說,可以有效的減輕負擔(尤其是可以省去撰寫評估函式的工作負擔),像是要使用公司自己的薪水計算方式,或是獎金的計算方式的程式,都可以利用 NCalc 來協助開發,舉例來說,假設公司有這樣的一條規則:
「業務人員可以得到公司總銷售額的 0.5% 再乘以個人等級的比例的獎金」
如果具有 Database 開發能力的開發人員,可以很容易的將它以 SQL 來發展,不過如果是無法碰觸到資料庫的話,那麼上列的計算獎金公式即是:
- y 為奬金金額。
- t 為公司總銷售額。
- c 為個人的獎金比例。
以上的算式若使用一般的作法的話,可能會是這樣寫:
static void CalcSalaryWithoutNCalc(DataTable RawTable) { int totalAmount = 500000000; foreach (DataRow row in RawTable.Rows) { double levelFactor = 0.0; switch ((SalaryAwardLevel)row[1]) { case SalaryAwardLevel.Normal: levelFactor = 0.01; break; case SalaryAwardLevel.Silver: levelFactor = 0.02; break; case SalaryAwardLevel.WhiteSilver: levelFactor = 0.03; break; case SalaryAwardLevel.Gold: levelFactor = 0.04; break; case SalaryAwardLevel.WhiteGold: levelFactor = 0.05; break; } // 將公式寫死在程式中。 Console.WriteLine("{0}'s salary award: {1:$###,###,###,##0}", row[0], ((double)totalAmount * 0.05) * levelFactor); } }
若是改用 NCalc 來寫的話,則是:
static void CalcSalaryWithNCalc(DataTable RawTable) { int totalAmount = 500000000; // 公式定義在字串中。 Expression e = new Expression("(0.05*[t])*[c]"); foreach (DataRow row in RawTable.Rows) { double levelFactor = 0.0; switch ((SalaryAwardLevel)row[1]) { case SalaryAwardLevel.Normal: levelFactor = 0.01; break; case SalaryAwardLevel.Silver: levelFactor = 0.02; break; case SalaryAwardLevel.WhiteSilver: levelFactor = 0.03; break; case SalaryAwardLevel.Gold: levelFactor = 0.04; break; case SalaryAwardLevel.WhiteGold: levelFactor = 0.05; break; } e.Parameters["t"] = totalAmount; e.Parameters["c"] = levelFactor; Console.WriteLine("{0}'s salary award: {1:$###,###,###,##0}", row[0], e.Evaluate()); } }
可能這樣讀者感覺不出來 NCalc 的優勢在哪裡,不過如果今天公司宣布獎金的計算公式改成這樣:
- y 為奬金金額。
- t 為公司總銷售額。
- c 為個人的獎金比例。
那麼使用 hard code(寫死在程式碼中)的程式,要回到原始碼中改變它的計算方式後重新編譯部署,但使用 NCalc 的程式,只需要改變那個字串即可,其他程式都不需要去動,開發人員可以將該字串移到組態檔中,這樣以後只要公式改變,只要修改組態檔即可,程式碼可以完全不用動到。
使用 NCalc
要在專案中使用 NCalc,只需要在專案中加入對 NCalc.dll 檔案(由 Codeplex 網站下載)的參考,並在程式碼中使用 NCalc 命名空間(using NCalc;)即可。NCalc 的運算式是以 Expression 類別為主,可以接受傳入邏輯運算式(Logic Expression)物件以及字串,並可以設定 NCalc 的運算式評估選項。選項決定 NCalc 的處理行為,像是不設定(None)、不分大小寫(IgnoreCases)、不快取運算式(NoCache)以及迭代參數處理(IternateParameters)等等。
NCalc 支援下列幾種算符(operator):
類型 | 算符 | 說明 |
Logical | or, || and, && | 邏輯運算算符。 |
Relational | =, ==, !=, <> <, <=, >, >= | 條件比對算符。 |
Additive | +, - | 加法算符。 |
Multiplicative | *, /, % | 乘法算符。 |
Bitwise | &, |, ^, >>, << | 位元運算與移動算符。 |
Unary | !, not, ~, - | 否定算符。 |
Primary | (, ), values | 括號與一般數值算符。 |
以及下列數值類型:
類型 | 說明 | |
integer | 整數型別。 | |
floating point number | 浮點數型別。 | |
scientific notation | 科學記號型數值型別(如 1e+100)。 | |
Dates and Times | 日期與時間型別,若要使用此型別,則要用 "#" 標記,例如 #2009/12/4# | |
Booleans | 布林值,true/false | |
Strings | 字串值。 | |
Functions | 函數,內建有 20 種數學函數(都是 Math 類別中提供的函數,但部份 Math 類別支援的,NCalc 未支援),以及兩種特別函數(in 和 if),可參考 http://ncalc.codeplex.com/wikipage?title=functions 取得支援的函數清單。 | |
Parameters | 參數,在運算式字串中要用 "[" "]" 包起來,然後以 Expression.Parameters 來設定其值,例如 2+5+[pi],[pi] 即為參數。 |
例如若在銀行存一筆 25 萬元的存款,銀行存款利率是 1.05%,那麼 20 年後這筆錢會是多少的問題,我們都知道銀行的存款使用的是複利公式,即:
- FV(Future Value)為該財富未來的價值。
- PV(Present Value)為該財富現在的價值。
- p 為年利率(或年報酬率)。
- n 為年數。
若要用 NCalc 來重現此公式,則可以撰寫成下列的程式碼:
static int GetFV(int PV, double Rate, int Years) { Expression e = new Expression("[PV] * ((1 + [p]) ^ [n])"); e.Parameters["PV"] = PV; e.Parameters["p"] = Rate; e.Parameters["n"] = Years; return (int)e.Evaluate(); }
其中,由於 PV、p 和 n 都是參數,因此要使用 [PV]、[p] 與 [n] 方式設定,然後使用 Expression.Parameters 來給定參數,最後呼叫 Expression.Evaluate() 即可得到計算結果。
又如平常在商場或是購物中心買東西,店員總是會說刷卡六期零利率,或是刷卡 12 期低利率的促銷手法,不過通常刷卡是要手續費的(各銀行標準不同,大約 3%-6% 不等),使用刷卡分期計算分期費用的公式是:
n
- V 是每期要繳的金額。
- PV 為商品的售價。
- p 是銀行的刷卡手續費率。
- n 為期數(月)。
若要用 NCalc 來重現此公式,則可以撰寫成下列的程式碼:
static int GetPerMonthlyPayment(int PV, double Rate, int Months) { Expression e = new Expression("([PV] * (1 + [p])) / [n]"); e.Parameters["PV"] = PV; e.Parameters["p"] = Rate; e.Parameters["n"] = Months; return Convert.ToInt32(e.Evaluate()); }
如果公式的參數值來自不同的公式的話,在 NCalc 中也是可以處理的,或者若算式中有包含自訂的函式,NCalc 會利用委派來讓開發人員設定它的內容。例如某銀行計算年息時,只要是雙月就加給 0.01% 的利息時,則前面本利和計算的公式可以改寫為:
static int GetSavingInterests(int PV, double Rate, int Month) { Expression e = new Expression("([PV] * [p]) / 12"); e.Parameters["PV"] = PV; e.EvaluateParameter += delegate(string name, ParameterArgs args) { if (name == "p") args.Result = (Month % 2 == 0) ? Rate + 0.0001 : Rate; }; return Convert.ToInt32(e.Evaluate()); }
在上列的程式中可以發現多使用了一個 Expression.EvaluateParameter 委派,這個委派會在評估運算式時被呼叫,可以由這個委派來指定特定參數的內容,每一個參數都會設定 name 代表參數名稱,開發人員只要比對這個名稱,再依名稱給值即可。
上面的程式也可以換個方式撰寫,即在運算式中加入一個自己的函數名稱,例如:
Expression e = new Expression("([PV] * GetRate()) / 12");
那麼 NCalc 要如何知道運算式中的函數實作?此時可以使用 Expression.EvaluateFunction 委派,這個委派的使用方式與 Expression.EvaluateParameter 委派差不多,同樣會傳入一個 name(字串型別)與 args(FunctionArgs 型別)參數,開發人員只要比對它的名稱出來,再設定它的執行結果給 FunctionArgs.Result 即可。
static int GetSavingInterests(int PV, double Rate, int Month) { Expression e = new Expression("([PV] * GetRate([i])) / 12"); e.Parameters["PV"] = PV; e.Parameters["i"] = Month; e.EvaluateFunction += delegate(string name, FunctionArgs args) { if (name == "GetRate") args.Result = (Convert.ToInt32(args.Parameters[0].Evaluate()) % 2 == 0) ? Rate + 0.0001 : Rate; }; return Convert.ToInt32(e.Evaluate()); }
結語
NCalc 很適合開發人員在需要有一個自動評估運算式的工具時的支援,它可以將運算式與程式碼直接隔離,同時可以在參數不變化的情況下去改變運算式的公式內容,特別適合在公式多變但輸入參數保持不變的環境中(例如折扣計算或是獎金計算等)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· 什么是nginx的强缓存和协商缓存
· 一文读懂知识蒸馏
· Manus爆火,是硬核还是营销?