Bezpieczna konwersja ciągu na BigDecimal

120

Próbuję odczytać niektóre wartości BigDecimal z ciągu. Powiedzmy, że mam ten ciąg: „1,000,000,000.999999999999999” i chcę uzyskać z niego BigDecimal. Jak to zrobić?

Przede wszystkim nie podobają mi się rozwiązania wykorzystujące zamiany ciągów znaków (zastępowanie przecinków itp.). Myślę, że powinien być jakiś zgrabny formater do wykonania tej pracy za mnie.

Znalazłem klasę DecimalFormatter, ale ponieważ działa podwójnie - tracone są ogromne ilości precyzji.

Więc jak mogę to zrobić?

bezmax
źródło
Ponieważ biorąc pod uwagę jakiś niestandardowy format, trudno jest przekonwertować go na format zgodny z BigDecimal.
bezmax
2
„Ponieważ biorąc pod uwagę jakiś niestandardowy format, jest to uciążliwe ...” Nie wiem, w pewnym sensie oddziela problematyczne domeny. Najpierw usuwasz z ciągu tekst czytelny dla człowieka, a następnie przekazujesz coś, co wie, jak poprawnie i skutecznie przekształcić wynik w plik BigDecimal.
TJ Crowder

Odpowiedzi:

90

Sprawdź setParseBigDecimalw DecimalFormat. Z tym ustawiaczem, parsezwróci za Ciebie BigDecimal.

Jeroen Rosenberg
źródło
1
Konstruktor w klasie BigDecimal nie obsługuje formatów niestandardowych. W przykładzie, który podałem w pytaniu, zastosowano format niestandardowy (przecinki). Nie możesz przeanalizować tego do BigDecimal przy użyciu jego konstruktora.
bezmax
24
Jeśli masz zamiar całkowicie zmienić swoją odpowiedź, proponuję wspomnieć o niej w odpowiedzi. W przeciwnym razie wygląda to bardzo dziwnie, gdy ludzie wskazują, dlaczego Twoja pierwotna odpowiedź nie miała żadnego sensu. :-)
TJ Crowder
1
Tak, nie zauważyłem tej metody. Dzięki, wydaje się, że to najlepszy sposób na zrobienie tego. Przetestuję to teraz i jeśli działa poprawnie - zaakceptuj odpowiedź.
bezmax
15
czy możesz podać przykład?
J Woodchuck
61
String value = "1,000,000,000.999999999999999";
BigDecimal money = new BigDecimal(value.replaceAll(",", ""));
System.out.println(money);

Pełny kod, aby udowodnić, że nie NumberFormatExceptionjest wyrzucane:

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

Wynik

1000000000.999999999999999

Buhake Sindi
źródło
@Steve McLeod .... nie, właśnie to przetestowałem .... moja wartość to1000000000.999999999999999
Buhake Sindi
10
Spróbuj z NIEMIECKIM ustawieniem regionalnym i 1.000.000.000,999999999999
TofuBeer
@ # TofuBeer .... przykład to 1000000000.999999999999999. Gdybym miał zrobić twoje ustawienia regionalne, musiałbym zamienić wszystkie kropki na spację ....
Buhake Sindi
14
Więc to nie jest bezpieczne. Nie znasz wszystkich lokalizacji.
ymajoros
3
W porządku, jeśli użyjesz po prostu np. DecimalFormatSymbols.getInstance().getGroupingSeparator()Zamiast przecinka, nie musisz przejmować się różnymi lokalizacjami.
Jason C
22

Poniższy przykładowy kod działa dobrze (ustawienia regionalne należy pobierać dynamicznie)

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
źródło
6

Kod może być czystszy, ale wydaje się, że działa to w różnych lokalizacjach.

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
źródło
3

Oto jak bym to zrobił:

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

Oczywiście, gdyby chodziło o kod produkcyjny, nie byłoby to takie proste.

Nie widzę problemu po prostu usuwając przecinki z ciągu.

jjnguy
źródło
A jeśli ciąg nie jest w formacie amerykańskim? W Niemczech będzie to 1.000.000.000 999999999999999
Steve McLeod
1
Głównym problemem jest to, że liczby mogą być pobierane z różnych źródeł, z których każde ma swój własny format. Zatem najlepszym sposobem na zrobienie tego w tej sytuacji byłoby zdefiniowanie jakiegoś „formatu liczbowego” dla każdego ze źródeł. Nie mogę tego zrobić z prostymi zamianami, ponieważ jedno źródło może mieć format „123 456 789”, a drugie „123.456.789”.
bezmax
Widzę, że po cichu redefiniujesz termin „metoda jednej linii” ... ;-)
Péter Török
@Steve: to dość fundamentalna różnica, która wymaga jawnej obsługi, a nie podejścia „regexp pasuje do wszystkich”.
Michael Borgwardt
2
@Max: „Główny problem polega na tym, że liczby można pobrać z różnych źródeł, z których każde ma swój własny format”. Co raczej przemawia za czyszczeniem danych wejściowych przed przekazaniem ich do kodu, nad którym nie masz kontroli, a nie przeciwko temu.
TJ Crowder
3

Potrzebowałem rozwiązania do przekonwertowania String na BigDecimal bez znajomości ustawień regionalnych i niezależności od ustawień regionalnych. Nie mogłem znaleźć żadnego standardowego rozwiązania tego problemu, więc napisałem własną metodę pomocniczą. Może pomoże to również komukolwiek innemu:

Aktualizacja: Ostrzeżenie! Ta pomocnicza metoda działa tylko w przypadku liczb dziesiętnych, a więc liczb, które zawsze mają kropkę dziesiętną! W przeciwnym razie metoda pomocnicza może dać błędny wynik dla liczb od 1000 do 999999 (plus / minus). Dzięki bezmax za jego wielki wkład!

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

Oczywiście przetestowałem metodę:

@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
źródło
1
Przepraszam, ale nie widzę, jak to może być przydatne ze względu na skrajne przypadki. Na przykład, skąd wiesz, co 7,333oznacza? Czy to 7333 czy 7 i 1/3 (7,333)? W różnych lokalizacjach numer ten byłby analizowany inaczej. Oto dowód słuszności
bezmax
Dobry wkład, nigdy nie miałem tego problemu w praktyce. Zaktualizuję mój post, bardzo dziękuję! I poprawię testowanie dla tych ofert regionalnych, żeby wiedzieć, gdzie pojawią się problemy.
user3227576
1
Sprawdziłem rozwiązanie dla różnych lokalizacji i różnych liczb ... i dla nas wszystkich działa, ponieważ zawsze mamy liczby z kropką dziesiętną (czytamy wartości Money z pliku tekstowego). Dodałem ostrzeżenie do odpowiedzi.
user3227576
2
resultString = subjectString.replaceAll("[^.\\d]", "");

usunie wszystkie znaki z wyjątkiem cyfr i kropki z twojego ciągu.

Aby był zgodny z lokalizacją, możesz użyć getDecimalSeparator()from java.text.DecimalFormatSymbols. Nie znam Javy, ale może to wyglądać tak:

sep = getDecimalSeparator()
resultString = subjectString.replaceAll("[^"+sep+"\\d]", "");
Tim Pietzcker
źródło
Co da nieoczekiwane rezultaty w przypadku liczb nieamerykańskich - zobacz komentarze do odpowiedzi Justina.
Péter Török
2

Stary temat, ale może najłatwiejszy jest użycie Apache commons NumberUtils, który ma metodę createBigDecimal (wartość ciągu) ....

Myślę (mam nadzieję), że bierze pod uwagę lokalizacje, inaczej byłoby to raczej bezużyteczne.

Lawrence
źródło
createBigDecimal jest tylko opakowaniem dla nowego BigDecimal (string), jak podano w kodzie źródłowym NumberUtils, który nie uwzględnia ustawień regionalnych AFAIK
Mar Bar
1

Spróbuj, to działa dla mnie

BigDecimal bd ;
String value = "2000.00";

bd = new BigDecimal(value);
BigDecimal currency = bd;
Rajkumar Teckraft
źródło
2
W ten sposób nie można określać niestandardowych ograniczników dla grup z przecinkiem dziesiętnym i thouthands.
bezmax
Tak, ale to służy do pobrania ciągu znaków BigDecimal z obiektu takiego jak HashMap i zapisania w wartości Bigdecimal w celu wywołania innych usług
Rajkumar Teckraft
1
Tak, ale nie o to chodziło.
bezmax
U mnie też
działa