2009-12-08 85 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

如果你有一个点或逗号紧跟不超过两位数,这是小数点。否则,忽略它。