Округление одного
У меня есть это "научное приложение", в котором значение Single
должно быть округлено перед представлением его в пользовательском интерфейсе. Согласно этой статье MSDN, из-за "потери точности" метод Math.Round(Double, Int32)
иногда будет "неожиданно", например. округление от 2.135 до 2.13, а не 2.14.
Как я понимаю, эта проблема не связана с "округлением банкира" (см., например, этот вопрос).
В приложении кто-то, по-видимому, решил решить эту проблему, явно переведя Single
в Decimal
перед округлением (т.е. Math.Round((Decimal)mySingle, 2)
), чтобы вместо этого перегрузить Math.Round(Decimal, Int32)
. Помимо возникающих проблем с двоичным преобразованием, это "решение" также может вызвать выброс OverflowException
, если значение Single
слишком мало или велико для соответствия типу Decimal
.
Улавливая такие ошибки, чтобы вернуть результат из Math.Round(Double, Int32)
, если преобразование завершится неудачно, это не делает меня идеальным решением. Также не перезаписывает приложение для использования Decimal
полностью.
Есть ли более или менее "правильный" способ справиться с этой ситуацией, и если да, что это может быть?
Ответы
Ответ 1
Я бы сказал, что ваше существующее решение (с использованием версии Decimal
Math.Round) является правильным.
Основная проблема заключается в том, что вы ожидаете округления чисел в соответствии с их базовым представлением 10, но вы сохранили их в качестве чисел с плавающей запятой базы 2. Приведенный пример 2.135 является одним из тех крайних случаев, когда представление базы 2 точно не соответствует основанию 10.
Чтобы получить ожидаемое поведение округления, вы должны преобразовать числа в базу 10. Самый простой способ - это именно то, что вы уже делаете: временно конвертируйте число в Decimal
достаточно долго, чтобы вызвать Math.Round
.
Ответ 2
Поскольку точность с плавающей запятой для диапазона, десятичное значение 2.135 не может быть точно представлено в двоичном формате.
Двоичное представление [ближайшее] работает как 0.1348876953125
decimal, поэтому округление правильное (если не интуитивно очевидно).
Вы должны прочитать статью Голдберга, "Что каждый компьютерный ученый должен знать о арифметике с плавающей запятой" (ACM Computing Surveys, Volume 23 Issue 1, March 1991, pp. 5-48)
Аннотация. Арифметика с плавающей точкой рассматривается многими как эзотерическая тема. Это довольно удивительно, поскольку плавающая точка повсеместна в компьютерных системах: почти каждый язык имеет тип данных с плавающей запятой; компьютеры от ПК до суперкомпьютеров имеют ускорители с плавающей запятой; большинство компиляторов будут вынуждены время от времени компилировать алгоритмы с плавающей запятой; и практически каждая операционная система должна реагировать на исключения с плавающей запятой, такие как переполнение. В настоящем документе представлено учебное пособие по аспектам плавающей запятой, которые оказывают непосредственное влияние на разработчиков компьютерных систем. Он начинается с фона по представлению с плавающей запятой и ошибкой округления, продолжается с обсуждением стандарта с плавающей точкой IEEE и заканчивается примерами того, как сборщики компьютерных систем могут лучше поддерживать плавающие точки.
Ответ 3
Я просто посмотрел документацию и, похоже, перечислил, что вы можете перейти в Math.Round()
. Если вы перейдете на Math.Round(Double, Int32, MidpointRounding.AwayFromZero)
, вы получите желаемый результат.
https://msdn.microsoft.com/en-us/library/vstudio/ef48waz8(v=vs.100).aspx
Изменить: просто проверено с этими номерами. Изменены номера и
double abc = 2.335;
Console.WriteLine(Math.Round(abc, 2, System.MidpointRounding.AwayFromZero));
abc = 2.345;
Console.WriteLine(Math.Round(abc, 2, System.MidpointRounding.AwayFromZero));
abc = 2.335;
Console.WriteLine(Math.Round(abc, 2));
abc = 2.445;
Console.WriteLine(Math.Round(abc, 2));
и получили эти результаты.
2.34
2.35
2.34
2.44
Правка 2: Я использовал исходные номера, которые вы дали, и они ломаются. Я думал, что, используя AwayFromZero, он разрешит двойное округление (я полагал, что это применимо только к округлению банкиров), это не так. Если вам нужна точность, которую вы ищете от вашего округления, вам нужно будет создать свою собственную функцию, которая даст вам необходимую точность, конвертируя ее в двойной или другой метод, но я искал какое-то время и не имел нашел что-нибудь, я вернусь, чтобы посмотреть, придумали ли вы решение.
double abc = 2.135;
Console.WriteLine(Math.Round(abc, 2, System.MidpointRounding.AwayFromZero));
abc = 2.145;
Console.WriteLine(Math.Round(abc, 2, System.MidpointRounding.AwayFromZero));
abc = 2.135;
Console.WriteLine(Math.Round(abc, 2));
abc = 2.145;
Console.WriteLine(Math.Round(abc, 2));
2.13
2.15
2.13
2.14