Точный кошмар в Java и SQL Server - PullRequest
4 голосов
/ 17 ноября 2009

Я боролся с кошмаром точности в Java и SQL Server до того момента, когда я больше не знаю. Лично я понимаю проблему и основную причину ее возникновения, но объясняю, что для клиента на полпути по всему земному шару что-то неосуществимо (по крайней мере, для меня).

Ситуация такая. У меня есть два столбца в SQL Server - Qty INT и Price FLOAT. Значения для них - 1250 и 10,8601 - поэтому, чтобы получить общее значение, его Qty * Price и результат 13575.124999999998 (как на Java, так и на SQL Server). Правильно. Проблема в том, что клиент не хочет видеть это, он видит это число только как 13575.125 и все. В одном месте они видят его с точностью до 2 десятичных знаков, а в другом - с 4 десятичными. При отображении в 4 десятичных разрядах правильное число - 13575.125, но при отображении в 2 десятичных разрядах они считают, что это неправильно - 13575.12 - вместо этого должно быть 13575.13!

Помощь.

Ответы [ 11 ]

12 голосов
/ 17 ноября 2009

Ваша проблема в том, что вы используете поплавки. Что касается Java, вам нужно использовать BigDecimal, а не float или double, а на стороне SQL вам нужно использовать Decimal (19,4) (или Decimal (19,3), если это помогает перейти на уровень точности). Не используйте тип Money, потому что математика для типа Money в SQL вызывает усечение, а не округление. Тот факт, что данные хранятся в виде типа с плавающей запятой (который, как вы говорите, неизменяем), не влияет на это, вам просто нужно преобразовать их при первой возможности, прежде чем приступить к математике.

В конкретном примере, который вы даете, вам нужно сначала получить число с десятичной точностью и поместить его в BigDecimal или Decimal (19,4), в зависимости от случая, а затем еще больше округлить до 2 десятичной точности. Затем (если вы округлите число) вы получите желаемый результат.

5 голосов
/ 17 ноября 2009

Использование BigDecimal . Float не подходит для представления денег. Он будет правильно обрабатывать округление . Float всегда будет выдавать ошибки округления.

3 голосов
/ 17 ноября 2009

Для хранения денежных сумм значения с плавающей запятой не подходят. Из вашего описания я, вероятно, рассмотрю суммы в виде длинных целых чисел со значением, умноженным на 10 ^ 5, в качестве формата хранения базы данных.

Вы должны иметь возможность обрабатывать расчеты с величинами, которые не теряют точности, поэтому и здесь плавающая точка не является подходящим способом. Если общая сумма между дебетом и кредитом в бухгалтерской книге отклоняется на 1 цент, бухгалтерская книга не работает в глазах финансовых работников, поэтому убедитесь, что ваше программное обеспечение работает в их проблемной области, а не в вашей. Если вы не можете использовать существующие классы для денежных сумм, вам необходимо создать свой собственный класс, который работает с amount * 10^5 и форматирует в соответствии с требуемой точностью только для целей ввода и вывода.

1 голос
/ 17 ноября 2009

Мне кажется, я вижу проблему.

10.8601 нельзя изобразить идеально, и поэтому, пока округление до 13575.125 работает нормально, трудно довести его до округления до .13, потому что добавление 0,005 просто не совсем подходит. И что еще хуже, 0,005 также не имеет точного представления, так что вы в итоге немного не дотянете до 0,13.

Затем вы можете либо округлить дважды, от одного до трех цифр, а затем от одного до 2, либо сделать лучший расчет для начала. Используя длинный или высокоточный формат, масштабируйте до 1000, чтобы получить от *. 125 до * 125. Выполните округление, используя точные целые числа.

Кстати, не совсем правильно говорить, что одно из бесконечно повторяющихся изменений «с плавающей запятой неточно» или что оно всегда приводит к ошибкам. Проблема в том, что формат может представлять только дроби, которые вы можете суммировать отрицательные степени двух, чтобы создать. Таким образом, из последовательности от 0,01 до 0,99 только .25, .50 и .75 имеют точные представления. Следовательно, по иронии судьбы, лучше всего использовать FP, если его масштабировать так, чтобы использовались только целочисленные значения, тогда он такой же точный, как и целочисленная арифметика типов данных. Конечно, тогда вы могли бы просто использовать целые числа с фиксированной точкой для начала.

Будьте осторожны, масштабирование, скажем, от 0,37 до 37 все еще не является точным, если не округлить. С плавающей точкой можно использовать для денежной оценки, но это больше работы, чем стоит, и, как правило, необходимые знания отсутствуют.

1 голос
/ 17 ноября 2009

Не используйте тип данных float для цена. Вы должны использовать «Деньги» или "SmallMoney".

Вот ссылка на [MS SQL Datatypes] [1].

[1]: http://webcoder.info/reference/MSSQLDataTypes.html

Исправление: используйте десятичную (19,4)

Спасибо, Ишай.

0 голосов
/ 11 ноября 2010

Тип данных FLOAT не может точно представлять дроби, потому что это base2 вместо base10. (См. Удобную ссылку :) http://gregs -blog.com / 2007/12/10 / dot-net-decimal-type-vs-float-type / ).

Для финансовых расчетов или чего-либо, что требует точного представления дробей, должен использоваться тип данных DECIMAL.

0 голосов
/ 17 ноября 2009

Если вы не можете изменить базу данных на фиксированный десятичный тип данных, вы можете попробовать округлить с помощью усечения ((x + .0055) * 10000) / 10000. Тогда 1.124999 будет "округляться" до 1.13 и давать последовательные результаты. Математически это ненадежно, но я думаю, что это сработает в вашем случае.

0 голосов
/ 17 ноября 2009

Вы также можете сделать DecimalFormat и затем округлить его.

DecimalFormat df = new DecimalFormat("0.00"); //or "0.0000" for 4 digits.
df.setRoundingMode(RoundingMode.HALF_UP);
String displayAmt = df.format((new Float(<your value here>)).doubleValue());

И я согласен с другими, что вы не должны использовать Float в качестве типа поля БД для хранения валюты.

0 голосов
/ 17 ноября 2009

Итак, учитывая, что вы не можете изменить структуру базы данных (что, вероятно, будет лучшим вариантом, учитывая, что вы используете нефиксированную точность для представления чего-то, что должно быть фиксированным / точным, как уже сделали многие другие) обсудили), надеюсь, вы можете изменить код где-нибудь. Что касается Java, я думаю, что что-то вроде @andy_boot с ответом будет работать. Что касается SQL, вам, в основном, нужно приводить неточное значение с наивысшей точностью, которая вам нужна, и продолжать отбрасывать оттуда, в основном это примерно так в коде SQL:

declare @f  float,
        @n  numeric(20,4),
        @m  money;

select  @f = 13575.124999999998,
        @n = 13575.124999999998,
        @m = 13575.124999999998

select  @f, @n, @m
select  cast(@f as numeric(20,4)), cast(cast(@f as numeric(20,4)) as numeric(20,2))
select  cast(@f as money), cast(cast(@f as money) as numeric(20,2))
0 голосов
/ 17 ноября 2009

Хотя вы не должны хранить цену как float, вы можете преобразовать ее в decimal(38, 4), скажем, или money (обратите внимание, что money имеет некоторые проблемы, так как результаты выражения, включающие его, не имеют динамически настраиваемых масштабов), и это раскрывается в представлении о выходе из SQL Server:

SELECT Qty * CONVERT(decimal(38, 4), Price)
...