Безпечний рядок для перетворення BigDecimal


120

Я намагаюся прочитати деякі значення BigDecimal з рядка. Скажімо, у мене є ця рядка: "1,000,000,000,999999999999999", і я хочу отримати з неї BigDecimal. Який спосіб це зробити?

Перш за все, мені не подобаються рішення, що використовують заміну рядків (заміна коми тощо). Я думаю, що повинен бути якийсь акуратний форматер, який би міг зробити цю роботу для мене.

Я знайшов клас DecimalFormatter, однак, оскільки він працює через подвійну - величезна кількість точності втрачається.

Отже, як я можу це зробити?


Оскільки з урахуванням певного користувальницького формату, це змусити його перетворити ваш формат у формат, сумісний з BigDecimal.
bezmax

2
"Через те, що з урахуванням певного користувальницького формату це біль ..." Я не знаю, він на зразок розділяє проблемні домени. Спочатку ви очищаєте людські матеріали, що читаються з рядка, а потім передаєте те, що вміє правильно та ефективно перетворити результат на «а» BigDecimal.
TJ Crowder

Відповіді:


90

Перевірте setParseBigDecimalв DecimalFormat. З цим сетером parseповернеться BigDecimal для вас.


1
Конструктор у класі BigDecimal не підтримує власні формати. У прикладі, який я наводив у запитанні, використовується користувацький формат (коми). Ви не можете розібрати це в BigDecimal, використовуючи його конструктор.
bezmax

24
Якщо ви збираєтесь повністю змінити свою відповідь, пропоную згадати її у відповіді. Інакше це виглядає дуже дивно, коли люди вказали, чому ваша оригінальна відповідь не мала жодного сенсу. :-)
TJ Crowder

1
Так, не помітили цього методу. Дякую, це, здається, найкращий спосіб зробити це. Перевірте це зараз, і якщо він працює належним чином - прийміть відповідь.
bezmax

15
ви можете навести приклад?
J Woodchuck

61
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


@Steve McLeod .... ні, я просто перевірив це .... моє значення1000000000.999999999999999
Buhake Sindi

10
Спробуйте з німецькою мовою та 1.000.000.000,999999999999
TofuBeer

@ # TofuBeer .... прикладом було 1 000 000 000,999999999999999. Якби мені довелося виконувати твій локал, я повинен був би замінити всі крапки на пробіл ....
Buhake Sindi

14
Отже, це не безпечно. Ви не знаєте всіх локалів.
ymajoros

3
Це добре, якщо ви просто використовуєте, наприклад, DecimalFormatSymbols.getInstance().getGroupingSeparator()замість коми, не потрібно думати про різні локалі.
Джейсон C

22

Наступний зразок коду працює добре (локально потрібно отримувати динамічно)

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

3

Ось як я це зробив:

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

Очевидно, якби це відбувалося у виробничому коді, це було б не так просто.

Я не бачу проблем із простим вилученням коми з рядка.


А якщо рядок не в американському форматі? У Німеччині це було б 1.000.000.000.999999999999999
Стів МакЛеод

1
Основна проблема тут полягає в тому, що номери можна отримати з різних джерел, кожен з яких має свій власний формат. Тож найкращим способом зробити це в цій ситуації було б визначення певного "формату чисел" для кожного з джерел. Я не можу цього зробити з простими замінами, оскільки одне джерело може мати формат "123 456 789", а інше "123.456.789".
bezmax

Я бачу, ви мовчки переосмислюєте термін "метод однієї лінії" ... ;-)
Петер Тьорьк

@Steve: це досить принципова відмінність, яка вимагає явного поводження, а не підходу "regexp, який підходить усім".
Майкл Боргвардт

2
@Max: "Основна проблема тут полягає в тому, що номери можна отримати з різних джерел, кожен з яких має свій власний формат." Який стверджує , за , а не проти, ваші очищення введених даних перед тим як перейти до коду ви не контролюєте.
TJ Crowder

3

Мені знадобилося рішення для перетворення String в 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)? У різних локалях це число буде розбиратися по-різному. Ось доказ концепції: ideone.com/Ue9rT8
bezmax

Хороший внесок, жодної проблеми на практиці не було. Я оновлю свою посаду, дуже дякую! І я вдосконалю тестування цих спеціалістів з локальних послуг, щоб просто знати, де виникнуть неприємності.
користувач3227576

1
Я перевірив рішення для різних локалів та різних чисел ... і для всіх нас працює, тому що у нас завжди є цифри з десятковою точкою (ми читаємо гроші-значення з текстового файлу). Я додав попередження до відповіді.
користувач3227576

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

видалить усі символи, крім цифр і крапки, з вашої рядка.

Щоб зробити це відомим для локальних можливостей, ви можете скористатися getDecimalSeparator()з java.text.DecimalFormatSymbols. Я не знаю Java, але це може виглядати приблизно так:

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

Що дасть несподівані результати для неамериканських номерів - дивіться у коментарях до відповіді Джастіна.
Péter Török

2

Стара тема, але, можливо, найпростішою є використання Apache Commons NumberUtils, який має метод createBigDecimal (значення рядка) ....

Я думаю (сподіваюсь), що це враховує локалі, інакше це буде досить марно.


createBigDecimal - лише обгортка для нового BigDecimal (рядок), як зазначено у вихідному коді NumberUtils, який не вважає локальний AFAIK
Mar Bar

1

Будь ласка, спробуйте це працює для мене

BigDecimal bd ;
String value = "2000.00";

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

2
Цей спосіб не підтримує визначення спеціальних роздільників для груп з десятковою точкою та тисячами.
bezmax

Так, але це для отримання рядкового значення BigDecimal від об'єкта типу HashMap і зберігання в Bigdecimal значення для виклику інших служб
Rajkumar Teckraft

1
Так, але це не було питання.
bezmax

Працює і для мене
Sathesh
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.