随笔 - 290  文章 - 10  评论 - 85  2

来源: 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)並回傳結果的一種工具類別庫,它支援數種數學運算、邏輯運算以及自訂函數運算的功能,例如這樣的程式碼:

[C#]
Expression e = new Expression("2 + 3 * 5");
Debug.Assert(17 == e.Evaluate());

NCalc 能夠直接剖析運算式字串,並且求出其結果,這對經常要撰寫自訂運算式的開發人員來說,可以有效的減輕負擔(尤其是可以省去撰寫評估函式的工作負擔),像是要使用公司自己的薪水計算方式,或是獎金的計算方式的程式,都可以利用 NCalc 來協助開發,舉例來說,假設公司有這樣的一條規則:

「業務人員可以得到公司總銷售額的 0.5% 再乘以個人等級的比例的獎金」

如果具有 Database 開發能力的開發人員,可以很容易的將它以 SQL 來發展,不過如果是無法碰觸到資料庫的話,那麼上列的計算獎金公式即是:

y=(000.5*t)*c
  • y 為奬金金額。
  • t 為公司總銷售額。
  • c 為個人的獎金比例。

以上的算式若使用一般的作法的話,可能會是這樣寫:

[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 來寫的話,則是:

[C#]
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=(000.5*(t*c)+10000)
  • 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=PV(1+p)
n
  • FV(Future Value)為該財富未來的價值。
  • PV(Present Value)為該財富現在的價值。
  • p 為年利率(或年報酬率)。
  • n 為年數。

若要用 NCalc 來重現此公式,則可以撰寫成下列的程式碼:

[C#]
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% 不等),使用刷卡分期計算分期費用的公式是:

V=
PV(1+p)
n
  • V 是每期要繳的金額。
  • PV 為商品的售價。
  • p 是銀行的刷卡手續費率。
  • n 為期數(月)。

若要用 NCalc 來重現此公式,則可以撰寫成下列的程式碼:

[C#]
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% 的利息時,則前面本利和計算的公式可以改寫為:

[C#]
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 代表參數名稱,開發人員只要比對這個名稱,再依名稱給值即可。

上面的程式也可以換個方式撰寫,即在運算式中加入一個自己的函數名稱,例如:

[C#]
Expression e = new Expression("([PV] * GetRate()) / 12");

那麼 NCalc 要如何知道運算式中的函數實作?此時可以使用 Expression.EvaluateFunction 委派,這個委派的使用方式與 Expression.EvaluateParameter 委派差不多,同樣會傳入一個 name(字串型別)與 args(FunctionArgs 型別)參數,開發人員只要比對它的名稱出來,再設定它的執行結果給 FunctionArgs.Result 即可。

[C#]
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 很適合開發人員在需要有一個自動評估運算式的工具時的支援,它可以將運算式與程式碼直接隔離,同時可以在參數不變化的情況下去改變運算式的公式內容,特別適合在公式多變但輸入參數保持不變的環境中(例如折扣計算或是獎金計算等) 

posted on 2010-06-16 09:27  Gu  阅读(...)  评论(...编辑  收藏