安全的字符串转换为BigDecimal

130

我想从字符串中读取一些BigDecimal值。假设我有这个字符串:"1,000,000,000.999999999999999",我想从中获得一个BigDecimal值。有什么方法可以实现呢?

首先,我不喜欢使用字符串替换(替换逗号等)的解决方案。我认为应该有一种巧妙的格式化程序来完成这项工作。

我找到了DecimalFormatter类,但是由于它通过double操作 - 大量的精度会丢失。

那么,我该怎么做呢?


因为在给定某些自定义格式时,将其转换为BigDecimal兼容格式是一件麻烦的事情。 - bezmax
2
因为在给定某些自定义格式时,这是一件很麻烦的事情...我不知道,它有点分离问题域。首先,您需要从字符串中清除可读性内容,然后将其交给知道如何正确高效地将结果转换为BigDecimal的东西。 - T.J. Crowder
9个回答

92

请查看DecimalFormat中的setParseBigDecimal。有了这个设置器,parse将会为您返回一个BigDecimal。


1
BigDecimal类中的构造函数不支持自定义格式。我在问题中提供的示例使用了自定义格式(逗号)。您无法使用其构造函数将其解析为BigDecimal。 - bezmax
28
如果你要完全更改你的答案,我建议在答案中提到。否则,当人们指出你原来的回答没有任何意义时,看起来非常奇怪。 :-) - T.J. Crowder
1
是的,没有注意到那个方法。谢谢,这似乎是最好的方法。我现在要测试一下,如果它正常工作 - 就接受这个答案。 - bezmax
19
可以举个例子吗? - Woodchuck
贬低,没有军用口粮 - FARS

67
String value = "1,000,000,000.999999999999999";
BigDecimal money = new BigDecimal(value.replaceAll(",", ""));
System.out.println(money);

完整代码,证明没有抛出NumberFormatException异常:

import java.math.BigDecimal;

public class Tester {
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        String value = "1,000,000,000.999999999999999";
        BigDecimal money = new BigDecimal(value.replaceAll(",", ""));
        System.out.println(money);
    }
}

输出

1000000000.999999999999999


11
尝试使用德语本地化,以及1.000.000.000,999999999999。 - TofuBeer
16
所以,这并不安全。你并不了解所有的地方。 - ymajoros
4
如果你只是使用例如 DecimalFormatSymbols.getInstance().getGroupingSeparator() 而不是逗号,那就没问题了,不需要因为不同的语言环境而惊慌。 - Jason C
你用什么工具进行测试?自动生成的方法存根 - Kiquenet
“Auto-generated method stub”是Eclipse IDE在您选择创建主方法或覆盖方法时生成的内容。通常,我会编写一个JUnit测试,但对于这个练习来说,一个简单的应用程序就足够了。 - Buhake Sindi
显示剩余2条评论

26

以下示例代码有效(需要动态获取locale)

import java.math.BigDecimal;
import java.text.NumberFormat;
import java.text.DecimalFormat;
import java.text.ParsePosition;
import java.util.Locale;

class TestBigDecimal {
    public static void main(String[] args) {

        String str = "0,00";
        Locale in_ID = new Locale("in","ID");
        //Locale in_ID = new Locale("en","US");

        DecimalFormat nf = (DecimalFormat)NumberFormat.getInstance(in_ID);
        nf.setParseBigDecimal(true);

        BigDecimal bd = (BigDecimal)nf.parse(str, new ParsePosition(0));

        System.out.println("bd value : " + bd);
    }
}

6
代码可以更加简洁,但这似乎对于不同的语言环境有用。
import java.math.BigDecimal;
import java.text.DecimalFormatSymbols;
import java.util.Locale;


public class Main
{
    public static void main(String[] args)
    {
        final BigDecimal numberA;
        final BigDecimal numberB;

        numberA = stringToBigDecimal("1,000,000,000.999999999999999", Locale.CANADA);
        numberB = stringToBigDecimal("1.000.000.000,999999999999999", Locale.GERMANY);
        System.out.println(numberA);
        System.out.println(numberB);
    }

    private static BigDecimal stringToBigDecimal(final String formattedString,
                                                 final Locale locale)
    {
        final DecimalFormatSymbols symbols;
        final char                 groupSeparatorChar;
        final String               groupSeparator;
        final char                 decimalSeparatorChar;
        final String               decimalSeparator;
        String                     fixedString;
        final BigDecimal           number;

        symbols              = new DecimalFormatSymbols(locale);
        groupSeparatorChar   = symbols.getGroupingSeparator();
        decimalSeparatorChar = symbols.getDecimalSeparator();

        if(groupSeparatorChar == '.')
        {
            groupSeparator = "\\" + groupSeparatorChar;
        }
        else
        {
            groupSeparator = Character.toString(groupSeparatorChar);
        }

        if(decimalSeparatorChar == '.')
        {
            decimalSeparator = "\\" + decimalSeparatorChar;
        }
        else
        {
            decimalSeparator = Character.toString(decimalSeparatorChar);
        }

        fixedString = formattedString.replaceAll(groupSeparator , "");
        fixedString = fixedString.replaceAll(decimalSeparator , ".");
        number      = new BigDecimal(fixedString);

        return (number);
    }
}

4

我需要一种解决方案,将字符串转换为BigDecimal,而不知道区域设置并且与区域设置无关。我找不到任何标准解决此问题的方法,因此我编写了自己的帮助方法。也许它可以帮助其他人:

更新:警告!此辅助方法仅适用于小数,即始终具有小数点的数字!否则,对于1000至999999之间的数字(加/减),辅助方法可能会提供错误的结果。感谢bezmax的出色贡献!

static final String EMPTY = "";
static final String POINT = '.';
static final String COMMA = ',';
static final String POINT_AS_STRING = ".";
static final String COMMA_AS_STRING = ",";

/**
     * Converts a String to a BigDecimal.
     *     if there is more than 1 '.', the points are interpreted as thousand-separator and will be removed for conversion
     *     if there is more than 1 ',', the commas are interpreted as thousand-separator and will be removed for conversion
     *  the last '.' or ',' will be interpreted as the separator for the decimal places
     *  () or - in front or in the end will be interpreted as negative number
     *
     * @param value
     * @return The BigDecimal expression of the given string
     */
    public static BigDecimal toBigDecimal(final String value) {
        if (value != null){
            boolean negativeNumber = false;

            if (value.containts("(") && value.contains(")"))
               negativeNumber = true;
            if (value.endsWith("-") || value.startsWith("-"))
               negativeNumber = true;

            String parsedValue = value.replaceAll("[^0-9\\,\\.]", EMPTY);

            if (negativeNumber)
               parsedValue = "-" + parsedValue;

            int lastPointPosition = parsedValue.lastIndexOf(POINT);
            int lastCommaPosition = parsedValue.lastIndexOf(COMMA);

            //handle '1423' case, just a simple number
            if (lastPointPosition == -1 && lastCommaPosition == -1)
                return new BigDecimal(parsedValue);
            //handle '45.3' and '4.550.000' case, only points are in the given String
            if (lastPointPosition > -1 && lastCommaPosition == -1){
                int firstPointPosition = parsedValue.indexOf(POINT);
                if (firstPointPosition != lastPointPosition)
                    return new BigDecimal(parsedValue.replace(POINT_AS_STRING, EMPTY));
                else
                    return new BigDecimal(parsedValue);
            }
            //handle '45,3' and '4,550,000' case, only commas are in the given String
            if (lastPointPosition == -1 && lastCommaPosition > -1){
                int firstCommaPosition = parsedValue.indexOf(COMMA);
                if (firstCommaPosition != lastCommaPosition)
                    return new BigDecimal(parsedValue.replace(COMMA_AS_STRING, EMPTY));
                else
                    return new BigDecimal(parsedValue.replace(COMMA, POINT));
            }
            //handle '2.345,04' case, points are in front of commas
            if (lastPointPosition < lastCommaPosition){
                parsedValue = parsedValue.replace(POINT_AS_STRING, EMPTY);
                return new BigDecimal(parsedValue.replace(COMMA, POINT));
            }
            //handle '2,345.04' case, commas are in front of points
            if (lastCommaPosition < lastPointPosition){
                parsedValue = parsedValue.replace(COMMA_AS_STRING, EMPTY);
                return new BigDecimal(parsedValue);
            }
            throw new NumberFormatException("Unexpected number format. Cannot convert '" + value + "' to BigDecimal.");
        }
        return null;
    }

当然,我已经测试过这种方法:
@Test(dataProvider = "testBigDecimals")
    public void toBigDecimal_defaultLocaleTest(String stringValue, BigDecimal bigDecimalValue){
        BigDecimal convertedBigDecimal = DecimalHelper.toBigDecimal(stringValue);
        Assert.assertEquals(convertedBigDecimal, bigDecimalValue);
    }
    @DataProvider(name = "testBigDecimals")
    public static Object[][] bigDecimalConvertionTestValues() {
        return new Object[][] {
                {"5", new BigDecimal(5)},
                {"5,3", new BigDecimal("5.3")},
                {"5.3", new BigDecimal("5.3")},
                {"5.000,3", new BigDecimal("5000.3")},
                {"5.000.000,3", new BigDecimal("5000000.3")},
                {"5.000.000", new BigDecimal("5000000")},
                {"5,000.3", new BigDecimal("5000.3")},
                {"5,000,000.3", new BigDecimal("5000000.3")},
                {"5,000,000", new BigDecimal("5000000")},
                {"+5", new BigDecimal("5")},
                {"+5,3", new BigDecimal("5.3")},
                {"+5.3", new BigDecimal("5.3")},
                {"+5.000,3", new BigDecimal("5000.3")},
                {"+5.000.000,3", new BigDecimal("5000000.3")},
                {"+5.000.000", new BigDecimal("5000000")},
                {"+5,000.3", new BigDecimal("5000.3")},
                {"+5,000,000.3", new BigDecimal("5000000.3")},
                {"+5,000,000", new BigDecimal("5000000")},
                {"-5", new BigDecimal("-5")},
                {"-5,3", new BigDecimal("-5.3")},
                {"-5.3", new BigDecimal("-5.3")},
                {"-5.000,3", new BigDecimal("-5000.3")},
                {"-5.000.000,3", new BigDecimal("-5000000.3")},
                {"-5.000.000", new BigDecimal("-5000000")},
                {"-5,000.3", new BigDecimal("-5000.3")},
                {"-5,000,000.3", new BigDecimal("-5000000.3")},
                {"-5,000,000", new BigDecimal("-5000000")},
                {null, null}
        };
    }

1
抱歉,我不认为这个方法有用,因为它存在边缘情况。例如,你怎么知道7,333代表什么?是7333还是7又1/3(7.333)?在不同的语言环境中,这个数字会被解析成不同的值。这里有一个概念证明:http://ideone.com/Ue9rT8 - bezmax
好的输入,实践中从未遇到过这个问题。我会更新我的帖子,非常感谢!我将改进这些Locale特殊测试,以便知道哪里会出现问题。 - user3227576
1
我已经检查了不同语言环境和不同数字的解决方案...对我们来说,一切都正常,因为我们始终有带小数点的数字(我们正在从文本文件中读取货币值)。我已经在答案中添加了一个警告。 - user3227576

3
以下是我的做法:
public String cleanDecimalString(String input, boolean americanFormat) {
    if (americanFormat)
        return input.replaceAll(",", "");
    else
        return input.replaceAll(".", "");
}

显然,如果这将用于生产代码中,它不会那么简单。 我认为从字符串中简单地删除逗号是没有问题的。

1
这里的主要问题是数字可能来自不同的源,每个源都有自己的格式。因此,在这种情况下最好的方法是为每个来源定义一些“数字格式”。我不能简单地进行替换,因为一个源可以具有“123 456 789”格式,而另一个源可以具有“123.456.789”的格式。 - bezmax
@Steve:这是一个非常基本的区别,需要明确处理,而不是“正则表达式适用于所有”的方法。 - Michael Borgwardt
@Peter,是的。 这是我的疏忽。不过那句话已经被删除了。 - jjnguy
2
@Max: "这里的主要问题是数字可能来自不同的来源,每个来源都有自己的格式。" 这说明您应该在将输入交给您无法控制的代码之前清理它,而不是反对它。 - T.J. Crowder
@jay,没错。在没有任何人告诉你之前,你无法确定本地化设置。 - jjnguy
显示剩余3条评论

3
resultString = subjectString.replaceAll("[^.\\d]", "");

该函数会从字符串中删除除数字和小数点以外的所有字符。

如果需要进行本地化处理,您可能需要使用java.text.DecimalFormatSymbols中的getDecimalSeparator()方法。我不熟悉Java,但代码可能如下所示:

sep = getDecimalSeparator()
resultString = subjectString.replaceAll("[^"+sep+"\\d]", "");

这将为非美国数字提供意外的结果 - 请参阅Justin答案的评论。 - Péter Török

2

虽然这是一个老话题,但也许最简单的方法是使用Apache commons NumberUtils,它有一个名为createBigDecimal (String value)的方法...

我猜(希望)它会考虑到本地化,否则它将毫无用处。


createBigDecimal仅仅是new BigDecimal(string)的一个包装器,正如NumberUtils源代码所述,它不考虑任何Locale。 - Mar Bar

0
请尝试这个,它对我有效。
BigDecimal bd ;
String value = "2000.00";

bd = new BigDecimal(value);
BigDecimal currency = bd;

2
该方法不支持指定小数点和千位分隔符的自定义分隔符。 - bezmax
是的,但这是为了从类似HashMap的对象中获取字符串BigDecimal值,并将其存储到Bigdecimal值中以调用其他服务。 - Rajkumar Teckraft
3
好的,但那不是问题的关键。 - bezmax
也适用于我。 - Sathesh

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