将金额字符串解析为数字

Posted

技术标签:

【中文标题】将金额字符串解析为数字【英文标题】:Parsing amount strings into numbers 【发布时间】:2010-12-24 10:40:15 【问题描述】:

我正在开发一个使用 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

我需要一些方法来猜测数字中的小数分隔符和千位分隔符是什么,然后将值呈现给用户以判断这是否正确。

我想知道如何以优雅的方式解决这个问题。

【问题讨论】:

我假设您能够从纸质文档中将这些值读取为字符串格式? 我认为这是不可能的。在您的示例中,您有 "-33.017" => -33017 和 "-166.713" => -166.713 为什么第一种情况下的点被解释为千位分隔符,而第二种情况下的点被解释为小数点? 最后一个我相信你打错了 听起来你永远不知道“3,333”应该是什么。他们按照你所说的方式,可能是 3.333 或 3333 我猜唯一的方法是复制发票数学,并以这种方式验证可能的值。 【参考方案1】:

我不确定你能否找到一种优雅的方式来解决这个问题,因为如果你不能告诉它数据来自哪里,它总是会模棱两可。

例如,数字 1.234 和 1,234 都是有效数字,但如果不确定符号的含义,您将无法分辨哪个是哪个。

就个人而言,我会编写一个函数,尝试根据一些规则进行“最佳猜测”...

如果数字在 . 之前包含 ,,则 , 必须是千位,. 必须是小数 如果数字包含. BEFORE ,,则. 必须是千位,, 必须是小数 如果有>1个,符号,千位分隔符必须是, 如果有>1个.符号,千位分隔符必须是. 如果只有 1 个,,后面有多少个数字?如果不是 3,那么它必须是 小数点分隔符(. 的规则相同) 如果有 3 个数字分隔它(例如 1,234 和 1.234),也许你可以把这个数字放在一边,并在同一页面上解析其他数字,以尝试找出它们是否使用不同的分隔符,然后再回到它?

一旦您计算出小数分隔符,请删除任何千位分隔符(解析数字不需要)并确保小数分隔符为 .在您正在解析的字符串中。然后你可以把它传递给Double.TryParse

【讨论】:

你的第一条规则对于欧洲数字是错误的,其中看起来有一些例子,例如1.840.456,34 是欧洲格式的数字。 是的,我认为这可能会发生。我错过了。作为之前的千位分隔符。我现在已经复制了规则来解释它们。【参考方案2】:

我可能会设置一个按优先顺序指定的规则列表,这样您就可以按优先级插入规则。然后,您可以根据返回正确规则的正则表达式匹配来解析列表。

一个快速原型很容易设置,类似于:

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]+.\d4,$", CultureInfo.GetCultureInfo("en-US")),
    new FormatRule(@"^[1-9]+,\d4,$", CultureInfo.GetCultureInfo("fr-FR")),
    new FormatRule(@"^-?[1-9]1,3(,\d3,)*(\.\d*)?$", CultureInfo.GetCultureInfo("en-US")),
    new FormatRule(@"^-?[1-9]1,3(.\d3,)*(\,\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 变得更容易。此外,如果需要,您可以轻松替换我用于单、双或小数的浮点类型。

【讨论】:

@Daniel - 这是什么老东西?你太慷慨了;) 由于代码示例,我选择了这个作为答案。谢谢本【参考方案3】:

您必须创建自己的函数来猜测小数分隔符和千位分隔符是什么。然后您将能够使用相应的 CultureInfo 进行 double.Parse。

我建议做这样的事情(只是一个即这不是一个生产测试的功能):

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),其中可以是小数或千位分隔符。

您可以保存这个在文档中查找一个数字,该数字具有知道女巫是文档文化所需的数据量,保存该文化并始终使用相同的文化(如果您可以假设文档全部在相同的文化...)

希望这会有所帮助

【讨论】:

您还可以添加一些额外的检查:如果splitByDot[0]0-0,则返回dotDecimalSeparator,同样适用于splitByComma[0]/commaDecimalSeparator。跨度> 当您可以使用正则表达式非常简单地做同样的事情时,这似乎是一种非常冗长的做事方式......【参考方案4】:

Double.TryParse 应该可以做到这一点。在我看来,您最大的问题是您解释数字的方式不一致。

例如,如何

"-133.696" => -133696  

"-166.713" => -166.713

?

【讨论】:

文件中的金额是一致的,但如果我们在所有文件中查看它们,则不一致【参考方案5】:

如果转换数字的规则不一致,那么您将无法在代码中解决这个问题。正如克劳斯比斯科夫所指出的,为什么“-133.696”中的句点与“-166.713”中的句点含义不同?给定这两个示例,其中一个按预期使用但另一个将其用作千位分隔符,您如何知道如何处理包含小数点的数字?

【讨论】:

正确。在这种情况下,我的算法将失败,用户应该决定正确的格式 祝你好运!我认为对于我们这些处理外部或遗留数据的人来说,这种事情真的很痛苦(你应该在这里看到我们必须处理的各种不同的日期格式!)。看到 ammoQ 的评论了吗?关于逗号或小数点后的位数是否有任何模式可以提示您应该如何格式化数字?【参考方案6】:

您需要定义可能遇到的各种情况,创建一些逻辑以将每个传入字符串与您的情况之一匹配,然后指定适当的 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)) 执行实际的解析。

【讨论】:

【参考方案7】:

“然后将值呈现给用户,以决定这是否正确。”

如果有多种可能性,为什么不向用户展示它们呢?

您可以使用多个调用 TryParse 的方法来处理您希望能够处理的不同文化,并为列表中成功的方法收集解析结果(删除重复项)。

您甚至可以根据文档其他地方使用各种格式的频率来估计不同可能性正确的可能性,并在按正确可能性排序的列表中显示备选方案。例如,如果您已经看过很多像 3,456,231.4 这样的数字,那么当您稍后在同一文档中看到 4,675 时,您可以猜测逗号可能是千位分隔符,并在列表中首先显示“4675”,然后在列表中显示“4.675” .

【讨论】:

【参考方案8】:

如果您的点或逗号后跟不超过两位数,则为小数点。否则,忽略它。

【讨论】:

以上是关于将金额字符串解析为数字的主要内容,如果未能解决你的问题,请参考以下文章

在SQL语句里面如何将字符型转换成数字型?

sql怎么把字符串转换为数字?

VBA 如何将数字转换为中文大写

在sql中如何将字符串数字转换成数字?

PHP 数字金额转换成中文大写金额的函数 数字转中文

数字转中文,大写,金额