Безопасное преобразование строки в BigDecimal

120

Я пытаюсь прочитать некоторые значения BigDecimal из строки. Скажем, у меня есть строка: «1,000,000,000.999999999999999», и я хочу получить из нее BigDecimal. Как это сделать?

Во-первых, мне не нравятся решения с заменой строк (заменой запятых и т. Д.). Я думаю, что должен быть какой-нибудь аккуратный форматтер, который сделает эту работу за меня.

Я нашел класс DecimalFormatter, однако, поскольку он работает через double, теряется огромная точность.

Итак, как я могу это сделать?

bezmax
источник
Потому что с учетом некоторого настраиваемого формата сложно преобразовать ваш формат в формат, совместимый с 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 .... в примере было 1000000000.999999999999999. Если бы мне пришлось сделать вашу локаль, мне бы пришлось заменить все точки на пробел ....
Бухак Синди
14
Так что это небезопасно. Вы не знаете всех регионов.
ymajoros
3
Ничего страшного, если вы просто используете, например, DecimalFormatSymbols.getInstance().getGroupingSeparator()вместо запятой, не нужно волноваться по поводу разных языков.
Jason 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);
    }
}
Ebith
источник
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);
    }
}
TofuBeer
источник
3

Вот как бы я это сделал:

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

Очевидно, если бы это происходило в производственном коде, это было бы не так просто.

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

оборота jjnguy
источник
А если строка не в американском формате? В Германии это будет 1.000.000.000,999999999999999
Стив МакЛауд,
1
Основная проблема здесь в том, что числа могут быть получены из разных источников, каждый из которых имеет свой формат. Таким образом, лучший способ сделать это в этой ситуации - определить некоторый «числовой формат» для каждого из источников. Я не могу сделать это простыми заменами, так как один источник может иметь формат «123 456 789», а другой - «123.456.789».
bezmax
Я вижу, вы молча переопределяете термин «однострочный метод» ... ;-)
Петер Торок
@Steve: это довольно фундаментальное различие, которое требует явной обработки, а не подхода "регулярное выражение для всех".
Майкл Боргвардт,
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}
        };
    }
user3227576
источник
1
Извините, но я не понимаю, как это может быть полезно из-за крайних случаев. Например, как узнать, что из себя 7,333представляет? Это 7333 или 7 и 1/3 (7,333)? В разных регионах этот номер будет анализироваться по-разному. Вот доказательство концепции: ideone.com/Ue9rT8
bezmax
Хороший вклад, никогда не сталкивался с этой проблемой на практике. Я обновлю свой пост, большое спасибо! И я улучшу тестирование этих специальных предложений Locale, чтобы знать, где возникнут проблемы.
user3227576
1
Я проверил решение для разных локалей и разных чисел ... и для нас все работает, потому что у нас всегда есть числа с десятичной точкой (мы читаем денежные значения из текстового файла). Я добавил к ответу предупреждение.
user3227576
2
resultString = subjectString.replaceAll("[^.\\d]", "");

удалит все символы, кроме цифр и точки из вашей строки.

Чтобы сделать его зависимым от локали, вы можете использовать getDecimalSeparator()from java.text.DecimalFormatSymbols. Я не знаю Java, но это может выглядеть так:

sep = getDecimalSeparator()
resultString = subjectString.replaceAll("[^"+sep+"\\d]", "");
Тим Пицкер
источник
Что даст неожиданные результаты для неамериканских чисел - см. Комментарии к ответу Джастина.
Péter Török
2

Старая тема, но, возможно, проще всего использовать Apache Commons NumberUtils, у которого есть метод createBigDecimal (String value) ....

Я предполагаю (надеюсь), что он принимает во внимание локали, иначе это было бы бесполезно.

Лоренс
источник
createBigDecimal - это только оболочка для нового BigDecimal (строка), как указано в исходном коде NumberUtils, который не учитывает языковой стандарт AFAIK
Mar Bar
1

Пожалуйста, попробуйте, это сработает для меня

BigDecimal bd ;
String value = "2000.00";

bd = new BigDecimal(value);
BigDecimal currency = bd;
Раджкумар Теккрафт
источник
2
Этот способ не поддерживает указание настраиваемых разделителей для десятичной точки и групп thouthands.
bezmax
Да, но это для получения строкового значения BigDecimal из объекта, такого как HashMap, и сохранения его в значении Bigdecimal для вызова других сервисов
Раджкумар Теккрафт,
1
Да, но вопрос был не в этом.
bezmax
У меня тоже работает
Sathesh