2009-12-08 82 views
2

我正在研究使用OCR引擎識別紙質文檔的系統。這些文件是包含總額,增值稅和淨額的發票。我需要將這些數量的字符串解析爲數字,但它們以各種格式和口味使用不同的符號來表示每個發票中的小數和千位分隔符號。如果我想使用.NET中的正常double.tryparse和double.parse方法然後他們通常無法對一些數額將數量字符串解析爲數字

這些都是一些我收到的金額

"3.533,65" => 3533.65 
"-133.696" => -133696 
"-33.017" => -33017 
"-166.713" => -166713 
"-5088,8" => -5088.8 
"0.423" => 0.423 
"9,215,200" => 9215200 
"1,443,840.00" => 1443840 

我的例子需要一些方法來猜測數字中的小數分隔符和千位分隔符,然後將該值呈現給用戶以確定這是否正確。

我想知道如何以優雅的方式解決這個問題。

+1

我假設您可以從紙質文檔中將這些值讀取爲字符串格式? – BenAlabaster 2009-12-08 14:16:14

+2

我不認爲這是可能的。在你的例子中,你有「-33.017」=> -33017和「-166.713」=> -166.713爲什麼第一種情況下的點解釋爲千位分隔符,第二種情況下解釋爲小數點? – Henrik 2009-12-08 14:19:43

+0

也是最後一個我相信你犯了一個錯字 – RichardOD 2009-12-08 14:22:18

回答

7

我可能會設置一個按照優先順序指定的規則列表,這樣您可以按優先順序插入規則。然後,您可以根據返回正確規則的正則表達式來解析列表。

快速原型將是非常容易建立類似:

public class FormatRule 
{ 
    public string Pattern { get; set; } 
    public CultureInfo Culture { get; set; } 

    public FormatRule(string pattern, CultureInfo culture) 
    { 
     Pattern = pattern; 
     Culture = culture; 
    } 
} 

現在的FormatRule用你的規則存儲在優先順序列表:

List<FormatRule> Rules = new List<FormatRule>() 
{ 
    /* Add rules in order of precedence specifying a culture 
    * that can handle the pattern, I've chosen en-US and fr-FR 
    * for this example, but equally any culture could be swapped 
    * in for various formats you may need to use */ 
    new FormatRule(@"^0.\d+$", CultureInfo.GetCultureInfo("en-US")), 
    new FormatRule(@"^0,\d+$", CultureInfo.GetCultureInfo("fr-FR")), 
    new FormatRule(@"^[1-9]+.\d{4,}$", CultureInfo.GetCultureInfo("en-US")), 
    new FormatRule(@"^[1-9]+,\d{4,}$", CultureInfo.GetCultureInfo("fr-FR")), 
    new FormatRule(@"^-?[1-9]{1,3}(,\d{3,})*(\.\d*)?$", CultureInfo.GetCultureInfo("en-US")), 
    new FormatRule(@"^-?[1-9]{1,3}(.\d{3,})*(\,\d*)?$", CultureInfo.GetCultureInfo("fr-FR")), 

    /* The default rule */ 
    new FormatRule(string.Empty, CultureInfo.CurrentCulture) 
} 

,那麼你應該能夠迭代你的列表尋找適用的正確規則:

public CultureInfo FindProvider(string numberString) 
{ 
    foreach(FormatRule rule in Rules) 
    { 
     if (Regex.IsMatch(numberString, rule.Pattern)) 
      return rule.Culture; 
    } 
    return Rules[Rules.Count - 1].Culture; 
} 

這組使您可以輕鬆管理規則,並設置何時應以某種方式處理某些事物時的優先順序。它還使您能夠指定不同的文化來處理一種格式和另一種格式。

public float ParseValue(string valueString) 
{ 
    float value = 0; 
    NumberStyles style = NumberStyles.Any; 
    IFormatProvider provider = FindCulture(valueString).NumberFormat; 
    if (float.TryParse(numberString, style, provider, out value)) 
     return value; 
    else 
     throw new InvalidCastException(string.Format("Value '{0}' cannot be parsed with any of the providers in the rule set.", valueString)); 
} 

最後,請致電您ParseValue()方法,你有一個浮動的字符串值轉換:

string numberString = "-123,456.78"; //Or "23.457.234,87" 
float value = ParseValue(numberString); 

您可以決定使用字典,以節省額外的FormatRule類;這個概念是相同的...我在示例中使用了一個列表,因爲它使查詢使用LINQ變得更加容易。此外,如果需要,您可以輕鬆地替換我用於單倍,雙倍或十進制的浮點類型。

+0

+1這是我的方式'去做吧。好的代碼! – 2009-12-08 16:46:36

+0

@丹尼爾 - 這是什麼舊事?你太慷慨了;) – BenAlabaster 2009-12-08 17:07:18

+0

我選擇這個作爲答案,因爲codesample。謝謝本 – gyurisc 2009-12-08 21:42:20

2

您應該可以通過Double.TryParse。我認爲你最大的問題在於你解釋數字的方式不一致。

例如,如何能

"-133.696" => -133696 

"-166.713" => -166.713 

+0

金額在文件內部是一致的,但如果我們在所有文件中查看,金額是不一致的 – gyurisc 2009-12-08 14:57:31

9

我不確定你能否找到一個很好的方法來解決這個問題,因爲如果你不能告訴它數據來自哪裏,它總是會是模糊的。

例如,數字1.234和1,234都是有效的數字,但沒有確定符號的含義,您將無法確定哪個是哪個。

就個人而言,如果該號碼包含, BEFORE .,則,必須爲成千上萬的.我必須會寫它試圖這樣做基於某些規則的「最佳猜測」的功能...

  • 對於小數
  • 如果號碼包含. BEFORE ,,然後.必須爲成千上萬的,必須是小數
  • 如果有> 1 ,個符號,千位分隔符必須是,
  • 如果有> 1個.符號,千位分隔符必須是.
  • 如果只有1 ,多少個號碼跟隨呢?如果不是3,那麼它必須是 的小數點分隔符(對於.的規則相同)
  • 如果有3個數字分開(例如1,234和1.234),也許你可以把這個數字放在一邊,並解析其他數字頁面來嘗試弄清楚他們是否使用不同的分隔符,然後回到它呢?

一旦你找到了十進制分隔符,刪除所有千位分隔符(不需要解析數字),並確保小數點分隔符是。在你正在解析的字符串中。然後你可以通過這Double.TryParse

+0

您的第一條規則對於歐洲數字是錯誤的,其中看起來有一些例子,例如, 1.840.456,34是歐洲格式的數字。 – cjk 2009-12-08 17:09:45

+0

是的,我認爲這可能會發生。我錯過了。作爲千分離器之前。我現在重複了規則來解釋它們。 – Richard 2009-12-08 18:14:52

2

如果轉換數字的規則不一致,那麼你將無法在代碼中解決這個問題。正如克勞斯比斯科夫指出的那樣,爲什麼「-133.696」的時期與「-166.713」的時期有不同的含義?你如何知道如何處理一個包含小數點的數字,給出這兩個例子,其中一個按照預期使用它,另一個使用它作爲千位分隔符?

+0

正確。在這種情況下,我的算法將失敗,用戶應該決定正確的格式 – gyurisc 2009-12-08 15:36:20

+0

祝你好運!我認爲,對於我們這些使用外部或遺留數據的人來說,這種事情真的很痛苦(你應該看到我們在這裏處理的各種不同的日期格式!)。你看到ammoQ的評論嗎?有沒有關於逗號或小數點後的數字位數的任何模式,可能會提示您如何格式化數字? – TabbyCool 2009-12-08 16:16:36

2

您需要定義您可能遇到的各種情況,創建一些邏輯以將每個傳入的字符串與您的某個情況進行匹配,然後解析它以指定適當的FormatProvider。例如 - 如果你的字符串在逗號前包含一個小數點,那麼你可以假定對於這個特定的字符串,他們使用小數點作爲千位分隔符,逗號作爲小數點分隔符,所以你可以構造一個格式提供者以應付這種情況。

嘗試一些沿着這些路線:

public IFormatProvider GetParseFormatProvider(string s) { 
    var nfi = new CultureInfo("en-US", false).NumberFormat; 
    if (/* s contains period before comma */) { 
    nfi.NumberDecimalSeparator = ","; 
    nfi.NumberGroupSeparator = "."; 
    } else if (/* some other condition */) { 
    /* construct some other format provider */ 
    } 
    return(nfi); 
} 

然後用Double.Parse(MyString的,GetParseFormatProvider(MyString的))來執行實際的解析。

1

「然後向用戶顯示該值,以確定這是否正確。」

如果存在多種可能性,爲什麼不向用戶顯示他們兩個呢?

您可以使用多個方法調用您希望能夠處理的不同文化的TryParse,併爲在列表中成功的方法收集解析結果(刪除重複項)。

您甚至可以根據文檔中其他位置使用的各種格式的頻率估計不同可能性的可能性,並根據正確可能性排序列表中的備選方案。例如,如果您已經看到很多數字(如3,456,231.4),那麼您可以猜測,逗號可能是數千個分隔符,當您在​​同一文檔中看到4,675個分隔符時,並且在列表中首先顯示「4675」,並且顯示「4.675」秒。

3

您將不得不創建自己的函數來猜測小數分隔符和千位分隔符是什麼。然後你將能夠double.Parse,但與相應的CultureInfo。

我建議做這樣的事情(只是一個即這不是一個生產測試功能):

private CultureInfo GetNumbreCultureInfo(string number) 
    { 
     CultureInfo dotDecimalSeparator = new CultureInfo("En-Us"); 
     CultureInfo commaDecimalSeparator = new CultureInfo("Es-Ar"); 

     string[] splitByDot = number.Split('.'); 
     if (splitByDot.Count() > 2) //has more than 1 . so the . is the thousand separator 
      return commaDecimalSeparator; //return a cultureInfo where the thousand separator is the . 

     //the same for the , 
     string[] splitByComma = number.Split(','); 
     if (splitByComma.Count() > 2) 
      return dotDecimalSeparator; 

     //if there is no , or . return an invariant culture 
     if (splitByComma.Count() == 1 && splitByDot.Count() == 1) 
      return CultureInfo.InvariantCulture; 

     //if there is only 1 . or 1 , lets check witch is the last one 
     if (splitByComma.Count() == 2) 
      if (splitByDot.Count() == 1) 
       if (splitByComma.Last().Length != 3) // , its a decimal separator 
        return commaDecimalSeparator; 
       else// here you dont really know if its the dot decimal separator i.e 100.001 this can be thousand or decimal separator 
        return dotDecimalSeparator; 
      else //here you have something like 100.010,00 ir 100.010,111 or 100,000.111 
      { 
       if (splitByDot.Last().Length > splitByComma.Last().Length) //, is the decimal separator 
        return commaDecimalSeparator; 
       else 
        return dotDecimalSeparator; 
      } 
     else 
      if (splitByDot.Last().Length != 3) // . its a decimal separator 
       return dotDecimalSeparator; 
      else 
       return commaDecimalSeparator; //again you really dont know here... i.e. 100,101 
    } 

你可以做一個簡單的測試是這樣的:

string[] numbers = { "100.101", "1.000.000,00", "100.100,10", "100,100.10", "100,100.100", "1,00" }; 

     decimal n; 
     foreach (string number in numbers) 
     { 
      if (decimal.TryParse(number, NumberStyles.Any, GetNumbreCultureInfo(number), out n)) 
       MessageBox.Show(n.ToString());//the decimal was parsed 
      else 
       MessageBox.Show("there was problems parsing"); 
     } 

還望如果你真的不知道女巫是分隔符(如100,010或100.001),那麼可以是小數點或千位分隔符。

你可以在文檔中保存一個數字,其中包含了知道女巫是文化文化所必需的數據量,保存該文化並始終使用相同的文化(如果您可以假定文檔是全部在相同的文化......)

希望這將有助於

+0

你也可以添加一些額外的檢查:如果'splitByDot [0]'是'0'或'-0',則返回'dotDecimalSeparator',同樣對'splitByComma [0]'''commaDecimalSeparator'也是如此。 – LukeH 2009-12-08 15:43:48

+0

似乎很長時間的做事方式,當你可以做很簡單的使用正則表達式相同... – BobTheBuilder 2009-12-08 17:00:01

+0

你是對的,我沒有想到,當我回應... – 2009-12-08 17:07:23

0

如果你有一個點或逗號緊跟不超過兩位數,這是小數點。否則,忽略它。