The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

利用 .NET 支援的數值格式來解析各種複雜的數字表示式

前陣子處理了一個特殊的案子,該客戶提供一批與「會計」有關的數據,在客戶手中的資料來源裡,我拿到一個這樣的數值字串:(95,403.8075)e-02,在會計的領域中刮號代表「負數」的意思,但這樣的數字在 .NET 應該怎樣 Parse 比較好呢?其實在 .NET 裡已經內建了許多好物等著你去挖掘了!

這個需求我之前是沒碰過,不過我倒是直覺的認為這種需求應該在會計業界蠻常見的,不太可能 .NET 會不顧這部分的數字解析需求,所以就開始進行各種不同的數字格式測試,我們先慢慢的從簡單的格式去解析數值,累積一些成功經驗,寫程式才不會挫折感這麼重:
註:我們使用 Visual Studio Unit Test Framework 進行單元測試的方式來撰寫程式碼。

[TestMethod]
public void double_Parse_01()
{
// Arrange
string num = "95,403.8075";
double expectedNum = 95403.8075;
double actualNum = 0;

// Act
actualNum = double.Parse(num);

// Assert
Assert.AreEqual(expectedNum, actualNum);
}
[TestMethod]
public void double_Parse_02()
{
// Arrange
string num = "-95,403.8075";
double expectedNum = -95403.8075;
double actualNum = 0;

// Act
actualNum = double.Parse(num);

// Assert
Assert.AreEqual(expectedNum, actualNum);
}

以上兩個都沒啥問題,不過以下格式 (95,403.8075) 就會引發 System.FormatException 例外:

[TestMethod]
[ExpectedException(typeof(System.FormatException))]
public void double_Parse_03()
{
// Arrange
string num = "(95,403.8075)";
double expectedNum = 95403.8075;
double actualNum = 0;

// Act
actualNum = double.Parse(num);

// Assert
Assert.AreEqual(expectedNum, actualNum);
}

這時就必須在 Parse 方法的第二個參數加上 NumberStyles 列舉型別,並指定 NumberStyles.Currency 指定「貨幣」相關的數字樣式,而此項目是設定一個複合的數字樣式,同時包括 AllowCurrencySymbol , AllowExponent , AllowThousands , AllowDecimalPoint , AllowParentheses , AllowTrailingSign , AllowLeadingSign , AllowTrailingWhite , AllowLeadingWhite 等格式的複合旗標,幾乎可以應付所有的貨幣格式。

[TestMethod]
public void double_Parse_04()
{
// Arrange
string num = "(95,403.8075)";
double expectedNum = -95403.8075;
double actualNum = 0;

// Act
actualNum = double.Parse(num, NumberStyles.Currency);

// Assert
Assert.AreEqual(expectedNum, actualNum);
}

再來我們進一步挑戰客戶的奇異數字格式,由於客戶的數字包含指數表示式,所以我們特別再加上一個數字樣式旗標 NumberStyles.AllowExponent,不過結果卻還是發生 System.FormatException 例外:

[TestMethod]
[ExpectedException(typeof(System.FormatException))]
public void double_Parse_05()
{
// Arrange
string num = "(95,403.8075)e-02";
double expectedNum = -954.038075;
double actualNum = 0;

// Act
actualNum = double.Parse(num, NumberStyles.Currency | NumberStyles.AllowExponent);

// Assert
Assert.AreEqual(expectedNum, actualNum);
}

接著我試著將指數符號移至刮號內竟然就神奇的執行成功了:

[TestMethod]
public void double_Parse_06()
{
// Arrange
string num = "(95,403.8075e-02)";
double expectedNum = -954.038075;
double actualNum = 0;

// Act
actualNum = double.Parse(num, NumberStyles.Currency | NumberStyles.AllowExponent);

// Assert
Assert.AreEqual(expectedNum, actualNum);
}

這時我只好利用 Regular Expression 去修正客戶的數字格式讓 .NET 可以解析才行:

[TestMethod]
public void double_Parse_07()
{
// Arrange
string num = "(95,403.8075)e-02";
double expectedNum = -954.038075;
double actualNum = 0;

// Act
num = Regex.Replace(num, @"\(([0-9,\.]+)\)([eE][\+\-]?\d+), "($1$2)");
actualNum = double.Parse(num, NumberStyles.Currency | NumberStyles.AllowExponent);

// Assert
Assert.AreEqual(expectedNum, actualNum);
}

不過,由於客戶的數字格式比較多元,有正、有負、有指數表示法、…,所以我最後的作法是將刮號表示法移除而已改用直接用負號表示負數,不過我想上面這個寫法與如下的範例程式寫法都能正常運作才是:

[TestMethod]
public void double_Parse_08()
{
// Arrange
string num = "(95,403.8075)e-02";
double expectedNum = -954.038075;
double actualNum = 0;

// Act
num = Regex.Replace(num, @"\(([0-9,\.]+)\)", "-$1");
actualNum = double.Parse(num, NumberStyles.Currency | NumberStyles.AllowExponent);

// Assert
Assert.AreEqual(expectedNum, actualNum);
}

後記

其實當時測試的過程不是很順利,不過探索的過程中也研究出 .NET 對數字格式的解析能力,對日後應付各種不同的數字格式字串有著極佳的助益,所以我個人其實很珍惜這些 "失敗之路",所以即便是多個月前發生的事,但心裡還是覺得一定要寫成文章分享。

此外,我也經常鼓勵公司同事在寫程式的時候不要太快從 Google 找到範例程式,或找到範例程式之後不要太快套用在專案上,凡事多想一下、多找些資料,甚至於抽空寫成文章,養成掌握細節的習慣,慢慢的你的開發功力就會在無形中進步,養成一個好的開發習慣對你這輩子的開發生涯影響甚鉅,千萬別小看這些小東西喔。

相關連結