Conversione da stringa sicura a BigDecimal


120

Sto cercando di leggere alcuni valori BigDecimal dalla stringa. Diciamo che ho questa stringa: "1,000,000,000.999999999999999" e voglio ottenere un BigDecimal da esso. Qual è il modo per farlo?

Prima di tutto, non mi piacciono le soluzioni che utilizzano sostituzioni di stringhe (sostituzione di virgole ecc.). Penso che dovrebbe esserci un buon formattatore per fare quel lavoro per me.

Ho trovato una classe DecimalFormatter, tuttavia poiché funziona con il doppio, si perdono enormi quantità di precisione.

Allora, come posso farlo?


Perché dato un formato personalizzato è un problema convertirlo in un formato compatibile con BigDecimal.
bezmax

2
"Perché dato un formato personalizzato è un problema ..." Non lo so, separa i domini problematici. Per prima cosa pulisci le cose leggibili dall'uomo dalla stringa, quindi passa a qualcosa che sa come trasformare correttamente ed efficacemente il risultato in un file BigDecimal.
TJ Crowder,

Risposte:


90

Controlla setParseBigDecimalin DecimalFormat. Con questo setter, parserestituirà un BigDecimal per te.


1
Il costruttore nella classe BigDecimal non supporta i formati personalizzati. L'esempio che ho fornito nella domanda utilizza il formato personalizzato (virgole). Non puoi analizzarlo in BigDecimal usando il suo costruttore.
bezmax

24
Se hai intenzione di cambiare completamente la tua risposta, ti suggerirei di menzionarla nella risposta. Altrimenti sembra molto strano quando le persone hanno sottolineato perché la tua risposta originale non aveva alcun senso. :-)
TJ Crowder,

1
Sì, non ho notato quel metodo. Grazie, sembra essere il modo migliore per farlo. Lo testerò ora e se funziona correttamente, accetta la risposta.
bezmax

15
puoi fornire un esempio?
J marmotta

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

Codice completo per dimostrare che no NumberFormatExceptionviene lanciato:

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

Produzione

1000000000,999999999999999


@Steve McLeod .... no, l'ho appena provato .... il mio valore è1000000000.999999999999999
Buhake Sindi,

10
Prova con la lingua TEDESCO e 1.000.000.000,999999999999
TofuBeer

@ # TofuBeer .... l'esempio era 1.000.000.000,99999999999999999. Se dovessi fare la tua localizzazione, dovrei sostituire tutti i punti nello spazio ...
Buhake Sindi

14
Quindi, non è sicuro. Non conosci tutte le località.
ymajoros

3
Va bene se usi ad es. DecimalFormatSymbols.getInstance().getGroupingSeparator()Invece di una virgola, non c'è bisogno di impazzire per la variazione delle impostazioni locali.
Jason C

22

Il seguente codice di esempio funziona bene (le impostazioni locali devono essere ottenute 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);
    }
}

6

Il codice potrebbe essere più pulito, ma questo sembra fare il trucco per diverse localizzazioni.

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

3

Ecco come lo farei:

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

Ovviamente, se questo andasse nel codice di produzione, non sarebbe così semplice.

Non vedo alcun problema con la semplice rimozione delle virgole dalla stringa.


E se la stringa non è in formato americano? In Germania, sarebbe 1.000.000.000,999999999999999
Steve McLeod,

1
Il problema principale qui è che i numeri potrebbero essere recuperati da diverse fonti, ognuna con il proprio formato. Quindi il modo migliore per farlo in questa situazione sarebbe definire un "formato numerico" per ciascuna delle fonti. Non posso farlo con semplici sostituzioni poiché una fonte può avere il formato "123 456 789" e l'altra "123.456.789".
bezmax

Vedo che stai ridefinendo silenziosamente il termine "metodo di una riga" ... ;-)
Péter Török

@ Steve: questa è una differenza fondamentale che richiede una gestione esplicita, non un approccio "regexp adatto a tutti".
Michael Borgwardt

2
@ Max: "Il problema principale qui è che i numeri potrebbero essere recuperati da diverse fonti, ognuna con il proprio formato." Il che sostiene a favore , piuttosto che contro, la tua pulizia dell'input prima di passare a codice che non controlli.
TJ Crowder,

3

Avevo bisogno di una soluzione per convertire una stringa in un BigDecimal senza conoscere la locale ed essere indipendente dalla locale. Non sono riuscito a trovare alcuna soluzione standard per questo problema, quindi ho scritto il mio metodo di supporto. Forse aiuta anche chiunque altro:

Aggiornamento: attenzione! Questo metodo di supporto funziona solo per i numeri decimali, quindi i numeri che hanno sempre un punto decimale! In caso contrario, il metodo di supporto potrebbe fornire un risultato errato per i numeri compresi tra 1000 e 999999 (più / meno). Grazie a bezmax per il suo fantastico contributo!

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

Ovviamente ho testato il metodo:

@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
Mi dispiace, ma non vedo come questo possa essere utile a causa dei casi limite. Ad esempio, come fai a sapere cosa 7,333rappresenta? È 7333 o 7 e 1/3 (7.333)? In diverse località quel numero verrebbe analizzato in modo diverso. Ecco la prova del concetto: ideone.com/Ue9rT8
bezmax

Buon input, non ho mai avuto questo problema in pratica. Aggiornerò il mio post, grazie mille! E migliorerò i test per queste offerte speciali locali solo per sapere dove riceveranno problemi.
user3227576

1
Ho controllato la soluzione per diverse localizzazioni e diversi numeri ... e per tutti noi funziona, perché abbiamo sempre numeri con un punto decimale (stiamo leggendo i valori di denaro da un file di testo). Ho aggiunto un avviso alla risposta.
user3227576

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

rimuoverà tutti i caratteri tranne le cifre e il punto dalla stringa.

Per renderlo compatibile con le impostazioni locali, potresti voler usare getDecimalSeparator()from java.text.DecimalFormatSymbols. Non conosco Java, ma potrebbe assomigliare a questo:

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

Il che darà risultati inaspettati per i numeri non americani - vedi i commenti alla risposta di Justin.
Péter Török

2

Vecchio argomento ma forse il più semplice è usare Apache commons NumberUtils che ha un metodo createBigDecimal (String value) ....

Immagino (spero) che prenda in considerazione le impostazioni locali altrimenti sarebbe piuttosto inutile.


createBigDecimal è solo un wrapper per il nuovo BigDecimal (stringa), come indicato nel codice sorgente NumberUtils che non considera le impostazioni locali AFAIK
Mar Bar

1

Per favore prova questo funziona per me

BigDecimal bd ;
String value = "2000.00";

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

2
In questo modo non è possibile specificare delimitatori personalizzati per il punto decimale e i gruppi di mani.
bezmax

Sì, ma questo serve per ottenere la stringa BigDecimal da un oggetto come HashMap e memorizzarla in Bigdecimal per chiamare gli altri servizi
Rajkumar Teckraft

1
Sì, ma non era questo il problema.
bezmax

Funziona anche per me
Sathesh
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.