如何在Java中将货币金额(美元或欧元)解析为浮点数值

16

在欧洲,小数使用,进行分隔,并且我们使用可选的.来分隔千位。我允许使用以下货币值:

  • 美式 123,456.78 标记法
  • 欧式 123.456,78 标记法

我使用下面这个正则表达式(来自 RegexBuddy 库)验证输入。我允许使用可选的两位小数和可选的千位分隔符。

^[+-]?[0-9]{1,3}(?:[0-9]*(?:[.,][0-9]{0,2})?|(?:,[0-9]{3})*(?:\.[0-9]{0,2})?|(?:\.[0-9]{3})*(?:,[0-9]{0,2})?)$
我想将货币字符串解析为浮点数。例如:
123,456.78 应存储为 123456.78 123.456,78 应存储为 123456.78 123.45 应存储为 123.45 1.234 应存储为 1234 12.34 应存储为 12.34
等等...
在Java中有简单的方法吗?
public float currencyToFloat(String currency) {
    // transform and return as float
}

使用BigDecimal而不是Float


感谢各位提供的出色答案。我已将代码更改为使用BigDecimal而不是float。我将保留先前使用float的部分,以防止其他人犯同样的错误。

解决方案


下面的代码显示了一个函数,它将美国和欧洲货币转换为BigDecimal(String)构造函数接受的字符串。也就是说,这个字符串没有千位分隔符,并且小数点用点表示。

   import java.util.regex.Matcher;
import java.util.regex.Pattern;


public class TestUSAndEUCurrency {

    public static void main(String[] args) throws Exception {       
        test("123,456.78","123456.78");
        test("123.456,78","123456.78");
        test("123.45","123.45");
        test("1.234","1234");
        test("12","12");
        test("12.1","12.1");
        test("1.13","1.13");
        test("1.1","1.1");
        test("1,2","1.2");
        test("1","1");              
    }

    public static void test(String value, String expected_output) throws Exception {
        String output = currencyToBigDecimalFormat(value);
        if(!output.equals(expected_output)) {
            System.out.println("ERROR expected: " + expected_output + " output " + output);
        }
    }

    public static String currencyToBigDecimalFormat(String currency) throws Exception {

        if(!doesMatch(currency,"^[+-]?[0-9]{1,3}(?:[0-9]*(?:[.,][0-9]{0,2})?|(?:,[0-9]{3})*(?:\\.[0-9]{0,2})?|(?:\\.[0-9]{3})*(?:,[0-9]{0,2})?)$"))
                throw new Exception("Currency in wrong format " + currency);

        // Replace all dots with commas
        currency = currency.replaceAll("\\.", ",");

        // If fractions exist, the separator must be a .
        if(currency.length()>=3) {
            char[] chars = currency.toCharArray();
            if(chars[chars.length-2] == ',') {
                chars[chars.length-2] = '.';
            } else if(chars[chars.length-3] == ',') {
                chars[chars.length-3] = '.';
            }
            currency = new String(chars);
        }

        // Remove all commas        
        return currency.replaceAll(",", "");                
    }

    public static boolean doesMatch(String s, String pattern) {
        try {
            Pattern patt = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE);
            Matcher matcher = patt.matcher(s);
            return matcher.matches();
        } catch (RuntimeException e) {
            return false;
        }           
    }  

}

1
那么1.23转换成什么?你的规则集是矛盾的。为什么不利用应用程序客户端端的本地化设施呢? - spender
好的,我的错...但是,如果不知道它来自哪个地区,试图解密它会有一种不好的味道。 - spender
基本上,我主要感兴趣的是解决欧盟符号风格的问题。但是有一个标准解决方案会更好。 - Sergio del Amo
@sergio:我更新了我的答案。尝试使用现有的与地区相关的NumberFormats,或者创建一个符合你期望格式的自定义格式化工具。 - Michael Petrotta
4个回答

38

回答一个稍微不同的问题:不要使用浮点数类型来表示货币值。 它会反咬你一口。相反,应该使用基于十进制的类型,比如BigDecimal,或者整数类型,比如intlong(代表您的价值量-例如,在美国货币中代表一分钱)。

您将无法存储精确的值-例如123.45作为浮点数,并且对该值进行的数学运算(例如乘以税收百分比)将产生舍入误差。

来自该页面的示例:

float a = 8250325.12f;
float b = 4321456.31f;
float c = a + b;
System.out.println(NumberFormat.getCurrencyInstance().format(c));
// prints $12,571,782.00 (wrong)

BigDecimal a1 = new BigDecimal("8250325.12");
BigDecimal b1 = new BigDecimal("4321456.31");
BigDecimal c1 = a1.add(b1);
System.out.println(NumberFormat.getCurrencyInstance().format(c1));
// prints $12,571,781.43 (right)

涉及到金钱时,你不希望出现错误。

关于原问题,我已经有一段时间没有接触Java了,但我知道我想避开使用正则表达式来完成这种工作。我看到这个建议;它可能会帮助你。未经测试,请注意开发者免责声明。

try {
    String string = NumberFormat.getCurrencyInstance(Locale.GERMANY)
                                            .format(123.45);
    Number number = NumberFormat.getCurrencyInstance(locale)
                                            .parse("$123.45");
    // 123.45
    if (number instanceof Long) {
       // Long value
    } else {
       // too large for long - may want to handle as error
    }
} catch (ParseException e) {
// handle
}

寻找符合您期望的规则的本地化设置。如果找不到,可以按顺序使用多个,或创建自己的自定义 NumberFormat

我还建议强制用户以单一和规范的格式输入值。123.45 和 123.456 看起来太相似了,根据您的规则,它们的结果可能相差1000倍。这就是为什么会损失数百万美元


你是的。我上面写的并没有涉及到这一点。我看到你建议使用浮点数来处理货币,就感到警觉了。 - Michael Petrotta
无论如何,我建议使用十进制类型而不是整数类型。许多数据库都有本地的十进制类型。 - Michael Petrotta
2
认真听这个人说,使用浮点数来处理货币在某些时候会对你造成伤害。 - Gareth Davis
7
是的,这是正确的。如需进一步讨论以及详细实例说明,可以参考《Effective Java》(第2版),第48条目“如果需要精确答案,请避免使用float和double”。引用一些关键点:“在货币计算中,float和double类型特别不适用[...]”,“解决此问题的正确方法是使用BigDecimal、int或long进行货币计算。”如果我没记错的话,《Java Puzzlers》一书中也在其中一个谜题中涉及了这个问题。 - Jonik
在SO上还有更多关于这个问题的内容,请参见“在Java中表示货币值”,https://dev59.com/dnVC5IYBdhLWcg3wdw65。 - Jonik
显示剩余4条评论

1
作为一种通用解决方案,您可以尝试:
char[] chars = currency.toCharArray();
chars[currency.lastIndexOf(',')] = '.';
currency = new String(chars);

替换为

而不是


if(currency.length()>=3) {
    char[] chars = currency.toCharArray();
    if(chars[chars.length-2] == ',') {
        chars[chars.length-2] = '.';
    } else if(chars[chars.length-3] == ',') {
        chars[chars.length-3] = '.';
    }
    currency = new String(chars);
}

使小数部分可以是任意长度。


0

试试这个.............

Locale slLocale = new Locale("de","DE");
        NumberFormat nf5 = NumberFormat.getInstance(slLocale);
        if(nf5 instanceof DecimalFormat) {
            DecimalFormat df5 = (DecimalFormat)nf5;
            try {
            DecimalFormatSymbols decimalFormatSymbols = DecimalFormatSymbols.getInstance(slLocale);
            decimalFormatSymbols.setGroupingSeparator('.');
            decimalFormatSymbols.setDecimalSeparator(',');
            df5.setDecimalFormatSymbols(decimalFormatSymbols);
            df5.setParseBigDecimal(true);
            ParsePosition pPosition = new ParsePosition(0);
            BigDecimal n = (BigDecimal)df5.parseObject("3.321.234,56", pPosition);
            System.out.println(n);
            }catch(Exception exp) {
                exp.printStackTrace();
            }
        }

-1
一个快速而粗糙的 hack 可能是:
String input = input.replaceAll("\.,",""); // remove *any* , or .
long amount = Long.parseLong(input);

BigDecimal bd = BigDecimal.valueOf(amount).movePointLeft(2);

//then you could use:
bd.floatValue();
//but I would seriously recommended that you don't use floats for monetary amounts.

请注意,此代码仅适用于输入以###.00的形式,即恰好有2个小数位。例如,input == "10,022"将破坏这个相当天真的代码。
另一种选择是使用BigDecimal(String)构造函数,但您需要将那些欧元样式的数字转换为使用“.”作为小数分隔符,并删除两者的千位分隔符。

另一种方法是使用BigDecimal(String)构造函数,但您需要将那些以欧元样式表示的数字转换为使用“。”作为小数分隔符,并删除千位分隔符。我可以使用正则表达式将逗号和后两位数字替换为点和最后两位数字。然后我可以将所有点和逗号替换为除了最后两个数字之前的“.”之外的任何内容。这应该可行,但看起来容易出错。 - Sergio del Amo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接