Округление одного

У меня есть это "научное приложение", в котором значение 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