Conversão Safe String para BigDecimal

120

Estou tentando ler alguns valores BigDecimal da seqüência de caracteres. Digamos que eu tenho essa String: "1.000.000.000.999999999999999" e quero obter um BigDecimal. Qual é a maneira de fazer isso?

Primeiro de tudo, eu não gosto das soluções usando substituições de string (substituindo vírgulas etc.). Eu acho que deveria haver algum formatador limpo para fazer esse trabalho para mim.

Eu encontrei uma classe DecimalFormatter, no entanto, uma vez que opera através do dobro - enormes quantidades de precisão são perdidas.

Então, como posso fazer isso?

bezmax
fonte
Como, devido a algum formato personalizado, é muito difícil convertê-lo para o formato compatível com BigDecimal.
Bezmax
2
"Porque, dado um formato personalizado, é uma dor ..." Não sei, isso meio que separa domínios problemáticos. Primeiro, limpe o material legível por humanos e depois entregue a algo que saiba como transformar o resultado de maneira correta e eficiente em um BigDecimal.
TJ Crowder

Respostas:

90

Confira setParseBigDecimalem DecimalFormat. Com este levantador, parsevocê retornará um BigDecimal para você.

Jeroen Rosenberg
fonte
1
O construtor na classe BigDecimal não suporta formatos personalizados. O exemplo que dei na pergunta usa o formato personalizado (vírgulas). Você não pode analisar isso no BigDecimal usando seu construtor.
Bezmax
24
Se você vai mudar completamente sua resposta, sugiro mencioná-la na resposta. Senão, parece muito estranho quando as pessoas apontam por que sua resposta original não fazia sentido. :-)
TJ Crowder
1
Sim, não percebeu esse método. Obrigado, essa parece ser a melhor maneira de fazer isso. Vou testá-lo agora e se funcionar corretamente - aceite a resposta.
Bezmax
15
você pode dar um exemplo?
J Woodchuck
61
String value = "1,000,000,000.999999999999999";
BigDecimal money = new BigDecimal(value.replaceAll(",", ""));
System.out.println(money);

Código completo para provar que não NumberFormatExceptioné lançado:

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);
    }
}

Resultado

1000000000.999999999999999

Buhake Sindi
fonte
@ Steve McLeod .... Não, eu só testei .... meu valor é1000000000.999999999999999
Buhake Sindi
10
Tente com o
código de
@ # TofuBeer .... o exemplo foi 1.000.000.000.999999999999999. Se eu tivesse de fazer sua localidade, eu teria que substituir todos os pontos para o espaço ....
Buhake Sindi
14
Então, não é seguro. Você não conhece todos os locais.
ymajoros 23/01
3
Tudo bem se você usar, por exemplo, em DecimalFormatSymbols.getInstance().getGroupingSeparator()vez de vírgula, não precisa se assustar com a variedade de localidades.
Jason C #
22

O código de exemplo a seguir funciona bem (o código de idioma precisa ser obtido dinamicamente)

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);
    }
}
Ebith
fonte
6

O código pode ser mais limpo, mas isso parece fazer o truque para diferentes localidades.

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);
    }
}
TofuBeer
fonte
3

Aqui está como eu faria isso:

public String cleanDecimalString(String input, boolean americanFormat) {
    if (americanFormat)
        return input.replaceAll(",", "");
    else
        return input.replaceAll(".", "");
}

Obviamente, se isso estivesse ocorrendo no código de produção, não seria assim tão simples.

Não vejo problema em simplesmente remover as vírgulas da String.

jjnguy
fonte
E se a string não estiver no formato americano? Na Alemanha, isso seria 1.000.000.000.999999999999999 #
Steve McLeod
1
O principal problema aqui é que os números podem ser buscados em diferentes fontes, cada uma com seu próprio formato. Portanto, a melhor maneira de fazer isso nessa situação seria definir um "formato numérico" para cada uma das fontes. Não posso fazer isso com substituições simples, pois uma fonte pode ter o formato "123 456 789" e a outra "123.456.789".
Bezmax
Vejo que você está silenciosamente redefinindo o termo "um método de linha" ... ;-)
Péter Török
@ Steve: essa é uma diferença fundamental que exige um tratamento explícito, e não uma abordagem "regexp fits all".
Michael Borgwardt
2
@ Max: "O principal problema aqui é que os números podem ser buscados em diferentes fontes, cada uma com seu próprio formato". O que argumenta a favor , e não contra, a limpeza da entrada antes de passar para o código que você não controla.
TJ Crowder
3

Eu precisava de uma solução para converter uma String em um BigDecimal sem conhecer o código de idioma e ser independente do código de idioma. Não encontrei nenhuma solução padrão para esse problema, então escrevi meu próprio método auxiliar. Pode ser que também ajude outras pessoas:

Atualização: Aviso! Este método auxiliar funciona apenas para números decimais, portanto, números que sempre têm um ponto decimal! Caso contrário, o método auxiliar poderá fornecer um resultado incorreto para números entre 1000 e 999999 (mais / menos). Obrigado ao bezmax por sua excelente contribuição!

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;
    }

Claro que testei o método:

@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}
        };
    }
user3227576
fonte
1
Desculpe, mas não vejo como isso pode ser útil devido a casos extremos. Por exemplo, como você sabe o que 7,333representa? São 7333 ou 7 e 1/3 (7,333)? Em locais diferentes, esse número seria analisado de maneira diferente. Aqui está a prova de conceito: ideone.com/Ue9rT8
bezmax 17/01/17
Boa entrada, nunca tive esse problema na prática. Vou atualizar meu post, muito obrigado! E melhorarei os testes para essas especialidades da Locale apenas para saber onde receberá os problemas.
user3227576
1
Eu verifiquei a solução para diferentes localidades e números diferentes ... e para todos nós funciona, porque sempre temos números com um ponto decimal (estamos lendo valores monetários em um arquivo de texto). Eu adicionei um aviso à resposta.
precisa saber é o seguinte
2
resultString = subjectString.replaceAll("[^.\\d]", "");

irá remover todos os caracteres, exceto dígitos e o ponto da sua string.

Para torná-lo compatível com a localidade, você pode usar a getDecimalSeparator()partir de java.text.DecimalFormatSymbols. Eu não sei Java, mas pode ser assim:

sep = getDecimalSeparator()
resultString = subjectString.replaceAll("[^"+sep+"\\d]", "");
Tim Pietzcker
fonte
O que dará resultados inesperados para números não americanos - veja os comentários à resposta de Justin.
Péter Török
2

Tópico antigo, mas talvez o mais fácil seja usar o NumberUtils do Apache commons, que possui o método createBigDecimal (String value) ....

Eu acho que (espero) leve em consideração os locais, ou seria um pouco inútil.

Lawrence
fonte
createBigDecimal é apenas um invólucro para o novo BigDecimal (string), conforme declarado no código-fonte NumberUtils, que não considera localidade AFAIK
Mar Bar
1

Por favor, tente isso está funcionando para mim

BigDecimal bd ;
String value = "2000.00";

bd = new BigDecimal(value);
BigDecimal currency = bd;
Rajkumar Teckraft
fonte
2
Dessa maneira, não é possível especificar delimitadores personalizados para grupos de ponto decimal e de ponteiros.
Bezmax # 26/15
Sim, mas isso é para obter o valor BigDecimal seqüência de caracteres de objeto como HashMap e armazenar em valor BigDecimal para chamar os outros serviços
Rajkumar Teckraft
1
Sim, mas essa não era a questão.
Bezmax # 27/15
Funciona para mim também
Sathesh 4/16