Sichere Konvertierung von String in BigDecimal

120

Ich versuche, einige BigDecimal-Werte aus der Zeichenfolge zu lesen. Nehmen wir an, ich habe diesen String: "1,000,000,000.999999999999999" und möchte ein BigDecimal daraus machen. Wie geht das?

Erstens mag ich die Lösungen mit String-Ersetzungen (Ersetzen von Kommas usw.) nicht. Ich denke, es sollte einen ordentlichen Formatierer geben, der diesen Job für mich erledigt.

Ich habe eine DecimalFormatter-Klasse gefunden, die jedoch doppelt funktioniert - große Mengen an Präzision gehen verloren.

Wie kann ich das machen?

bezmax
quelle
Da es bei einem benutzerdefinierten Format schwierig ist, Ihr Format in ein BigDecimal-kompatibles Format zu konvertieren.
Bezmax
2
"Weil es bei einem benutzerdefinierten Format schmerzhaft ist ..." Ich weiß nicht, es trennt Problembereiche. Zuerst entfernen Sie das für Menschen lesbare Material aus der Zeichenfolge und übergeben es dann an etwas, das weiß, wie das Ergebnis korrekt und effizient in ein Ergebnis umgewandelt werden kann BigDecimal.
TJ Crowder

Antworten:

90

Überprüfen Sie setParseBigDecimalin DecimalFormat. Mit diesem Setter parsewird ein BigDecimal für Sie zurückgegeben.

Jeroen Rosenberg
quelle
1
Der Konstruktor in der BigDecimal-Klasse unterstützt keine benutzerdefinierten Formate. Das Beispiel, das ich in der Frage gegeben habe, verwendet das benutzerdefinierte Format (Kommas). Sie können das nicht mit dem Konstruktor in BigDecimal analysieren.
Bezmax
24
Wenn Sie Ihre Antwort vollständig ändern möchten, würde ich vorschlagen, sie in der Antwort zu erwähnen. Sonst sieht es sehr seltsam aus, wenn Leute darauf hingewiesen haben, warum Ihre ursprüngliche Antwort keinen Sinn ergab. :-)
TJ Crowder
1
Ja, habe diese Methode nicht bemerkt. Danke, das scheint der beste Weg zu sein. Ich werde es jetzt testen und wenn es richtig funktioniert - akzeptiere die Antwort.
bezmax
15
Können Sie ein Beispiel geben?
J Woodchuck
61
String value = "1,000,000,000.999999999999999";
BigDecimal money = new BigDecimal(value.replaceAll(",", ""));
System.out.println(money);

Vollständiger Code, um zu beweisen, dass nein NumberFormatExceptiongeworfen wird:

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

Ausgabe

1000000000.999999999999999

Buhake Sindi
quelle
@ Steve McLeod .... nein, ich habe es gerade getestet .... mein Wert ist1000000000.999999999999999
Buhake Sindi
10
Versuchen Sie es mit dem deutschen Gebietsschema und 1.000.000.000.999999999999
TofuBeer
@ # TofuBeer .... das Beispiel war 1.000.000.000.999999999999999. Wenn ich Ihr Gebietsschema machen müsste, müsste ich alle Punkte im Weltraum ersetzen ...
Buhake Sindi
14
Es ist also nicht sicher. Sie kennen nicht alle Gebietsschemas.
Ymajoros
3
Es ist in Ordnung, wenn Sie nur zB DecimalFormatSymbols.getInstance().getGroupingSeparator()anstelle eines Kommas verwenden, ohne sich über unterschiedliche Gebietsschemata Gedanken machen zu müssen.
Jason C
22

Der folgende Beispielcode funktioniert gut (Gebietsschema muss dynamisch abgerufen werden)

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
quelle
6

Der Code könnte sauberer sein, aber dies scheint den Trick für verschiedene Gebietsschemas zu tun.

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
quelle
3

So würde ich es machen:

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

Wenn dies im Produktionscode geschehen würde, wäre es natürlich nicht so einfach.

Ich sehe kein Problem darin, einfach die Kommas aus dem String zu entfernen.

jjnguy
quelle
Und wenn der String nicht im amerikanischen Format ist? In Deutschland wäre es 1.000.000.000.999999999999999
Steve McLeod
1
Das Hauptproblem hierbei ist, dass die Zahlen aus verschiedenen Quellen abgerufen werden können, die jeweils ein eigenes Format haben. Der beste Weg, dies in dieser Situation zu tun, wäre die Definition eines "Zahlenformats" für jede der Quellen. Ich kann das nicht mit einfachen Ersetzungen machen, da eine Quelle das Format "123 456 789" und die andere das Format "123.456.789" haben kann.
Bezmax
Ich sehe, dass Sie den Begriff "Einzeilenmethode" stillschweigend neu definieren ... ;-)
Péter Török
@Steve: Das ist ein ziemlich grundlegender Unterschied, der eine explizite Behandlung erfordert, nicht einen "Regexp Fits All" -Ansatz.
Michael Borgwardt
2
@Max: "Das Hauptproblem hierbei ist, dass die Zahlen aus verschiedenen Quellen abgerufen werden können, die jeweils ein eigenes Format haben." Die argumentieren , für , nicht gegen, Ihre die Eingabe vor Gabe Abreinigung zu Code , den Sie nicht kontrollieren.
TJ Crowder
3

Ich brauchte eine Lösung, um einen String in ein BigDecimal zu konvertieren, ohne das Gebietsschema zu kennen und unabhängig vom Gebietsschema zu sein. Ich konnte keine Standardlösung für dieses Problem finden und habe meine eigene Hilfsmethode geschrieben. Vielleicht hilft es auch anderen:

Update: Warnung! Diese Hilfsmethode funktioniert nur für Dezimalzahlen, also Zahlen, die immer einen Dezimalpunkt haben! Andernfalls könnte die Hilfsmethode für Zahlen zwischen 1000 und 999999 (Plus / Minus) ein falsches Ergebnis liefern. Vielen Dank an bezmax für seinen tollen Input!

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

Natürlich habe ich die Methode getestet:

@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
quelle
1
Entschuldigung, aber ich sehe nicht, wie dies aufgrund von Randfällen nützlich sein kann. Woher wissen Sie zum Beispiel, was 7,333repräsentiert? Ist es 7333 oder 7 und 1/3 (7,333)? In verschiedenen Gebietsschemas würde diese Nummer unterschiedlich analysiert. Hier ist ein Proof of Concept: ideone.com/Ue9rT8
bezmax
Guter Input, hatte dieses Problem in der Praxis nie. Ich werde meinen Beitrag aktualisieren, vielen Dank! Und ich werde die Tests für diese Locale-Specials verbessern, nur um zu wissen, wo Probleme auftreten werden.
user3227576
1
Ich habe die Lösung für verschiedene Gebietsschemas und verschiedene Zahlen überprüft ... und für uns alle funktioniert, weil wir immer Zahlen mit einem Dezimalpunkt haben (wir lesen Geldwerte aus einer Textdatei). Ich habe der Antwort eine Warnung hinzugefügt.
user3227576
2
resultString = subjectString.replaceAll("[^.\\d]", "");

entfernt alle Zeichen außer den Ziffern und dem Punkt aus Ihrer Zeichenfolge.

Um es locale-aware, könnten Sie verwenden möchten getDecimalSeparator()aus java.text.DecimalFormatSymbols. Ich kenne Java nicht, aber es könnte so aussehen:

sep = getDecimalSeparator()
resultString = subjectString.replaceAll("[^"+sep+"\\d]", "");
Tim Pietzcker
quelle
Dies führt zu unerwarteten Ergebnissen für nichtamerikanische Zahlen - siehe die Kommentare zu Justins Antwort.
Péter Török
2

Altes Thema, aber vielleicht am einfachsten ist es, Apache Commons NumberUtils zu verwenden, das eine Methode createBigDecimal (String value) hat ....

Ich denke (hoffe), dass es Gebietsschemas berücksichtigt, sonst wäre es eher nutzlos.

Lawrence
quelle
createBigDecimal ist nur ein Wrapper für neues BigDecimal (Zeichenfolge), wie im NumberUtils-Quellcode angegeben, der kein Gebietsschema berücksichtigt. AFAIK
Mar Bar
1

Bitte versuchen Sie dies, es funktioniert für mich

BigDecimal bd ;
String value = "2000.00";

bd = new BigDecimal(value);
BigDecimal currency = bd;
Rajkumar Teckraft
quelle
2
Auf diese Weise wird die Angabe benutzerdefinierter Trennzeichen für Dezimalpunkt- und Tausendgruppen nicht unterstützt.
Bezmax
Ja, aber dies dient dazu, den BigDecimal-Wert der Zeichenfolge von einem Objekt wie HashMap abzurufen und im Bigdecimal-Wert zu speichern, um die anderen Dienste
aufzurufen
1
Ja, aber das war nicht die Frage.
Bezmax
Funktioniert auch für mich
Sathesh