Комментарии 62
6,5 — это всегда 6,5. И десятичном виде, и в двоичном.
Как вот это вот всё что вы сказали, включая ad hominem к предыдущему комментатору, опровергает утверждение что 6.5 — это ровно 6.5 и в десятичном виде и в двоичном?
>>> type(x)
<type 'float'>
>>> print x
6.5
>>> print x==6.5
False
Вы всё ещё уверены, что 6.5 — это всегда 6.5?
В вашем примере не показано чему равен x.
Есть IEEE 754 вполне недвусмысленно описывающая работу чисел с плавающей точкой. Процессоры ей следуют. В рамках IEEE 754
13/2=6.5
5+1.5=6.5
Also, утверждение было про десятичную и двоичную системы счисления, а не про «как print в питоне неизвестно какой версии, скорее всего 2.х, печатает float'ы».
Исходное утверждение было «6,5 — это всегда 6,5». У него есть два возможных уточнения:
1) что там «под капотом» — ну да, IEEE754;
2) что можно увидеть в логе, наступив на эти грабли.
Я помнил эти грабли из питона, но, оказывается, их в третьем действительно убрали. Ну, есть же в вебе два других популярных языка, в которых старые грабли многочисленны и бережно сохраняемы — и один из них превзошёл мои ожидания:
php > echo PHP_VERSION;
7.3.8
php > $x = ((6 + 0.1 * 3 + 0.2) - 6.2 + 0.2) * 13;
php > echo '$x='.$x.', $x<6.5: '.($x<6.5 ? 'yes': 'no').', round($x)='.round($x);
$x=6.5, $x<6.5: yes, round($x)=7
В языках, создатели которых догадываются о подобных неоднозначностях, делают специальные флаги, как транслятор должен интерпретировать запись математических операций. В IEEE754 есть правила и по этому поводу тоже, а не только по представлению в памяти.
Я не понял, в чём именно вопрос.
а) Если дошло до такой задачи, где важны тонкости округления, то, может, стоит брать не C#, а Фортран?
или
б) С чего бы "высокоуровневым" языкам давать программисту возможность выбора, каким образом математические выражения в них транслируются в машинный код и какие оптимизации при этом могут применяться?
Моё мнение как раз в том, что в (б) — с того, чтобы вопрос (а) не возникал. Не особо убудет от разработчиков компилятора, если будет флаг, при включении которого x + 1e-10 - 3e-10
не будет автоматически оптимизироваться до x - 2e-10
, а float + int + float + int
не будет преобразовываться в (float + float) + (int + int)
для параллелизации сложений. А кому-то контроль на этом уровне может внезапно оказаться нужен.
А что делать, если x
больше 109, и добавление к нему 10-7 его не меняет, т.к. это за гранью двойной точности?
Стандарты, спецификации и флаги — они для того и существуют, чтобы, когда оказалось, что париться таки надо, — поведение было подконтрольно программисту и документировано, а лучше стандартизовано.
Какое "это" правило?
Кнокретно 6.5 может и всегда 6.5
Если это именно 6.5, а не почти оно:
>>> ((6 + 0.1 * 3 + 0.2) - 6.2 + 0.2) * 13
6.499999999999998
>>> ((6 + 0.1 * 3 + 0.2) - 6) * 13
6.5
Обобщим: есть числа, представимые с конечной точностью в некоей системе счисления, а есть непредставимые. Компьютеры обычно используют двоичную систему счисления. Память компьютеров штука не бесконечная. Поэтому не все числа компьютер может хранить без потери точности.
Дело в том, что «банковское» округление работает чуть хитрее — оно округляет число до ближайшего четного целого числа, а не до ближайшего целого по модулю
Это же только когда дробная часть в точности равна 1/2, и ближайших целых два равноудалённых.
Вполне логично, кстати, особенно когда цифры сначала округляются до десятых, а потом решаешь, что точности до целых уже достаточно. "Школьный" способ — при дробной части 1/2 округлять с повышением модуля — даёт, что какое-нибудь 3.48 округляется сначала до 3.5, а потом до 4. "Банковский", конечно, тоже, но также есть вероятность, что рядом 6.54 округлилось сначала до 6.5, а затем до 6. То есть "в среднем", действительно, можно ожидать более "несдвинутое" (unbiased) округление.
Математически, кстати, тоже логично: если двоичная дробная часть равна 0.12, то округляем не до 20, где есть неоднозначность, а до 21.
Не нормальное, а равномерное :)
На той же википедии пишут, что у случайного округления распределение лучше, но это выводит округление из категории чистых функций, к чему тоже есть претензии.
При том, что при использовании ГСЧ результат функции перестаёт быть воспроизводимым, нарушается условие ∀x f(x) == f(x)
Никаких, при условии, что функция round()
из стандартной библиотеки работает детерминированно.
Иначе, как уже заметили, будет много радости при отладке и объяснении пользователям, почему одни и те же входные данные им дают разные ответы.
i = floor(x);
f = x - i;
используется чаще в программировании.
Банки тут ни при чем, округление до чётного используется потому что оно более стабильно и не даёт "дрейфа" при сложении и умножении. Его ещё Кнут рекомендовал.
Обычное округление:
5 + 0.5 = 5.5 ≅ 6
6 - 0.5 = 5.5 ≅ 6
6 + 0.5 = 6.5 ≅ 7
Округление до чётного:
5 + 0.5 = 5.5 ≅ 6
6 - 0.5 = 5.5 ≅ 6
6 + 0.5 = 6.5 ≅ 6
Например, классика:
есть некоторые ежедневные значения.
каждый месяц их складываем и округляем (например отбрасываем копейки любым методом — банковским /к четному целому).
в конце года округляем сумму ежедневных значений за год.
А теперь сравниваем с суммой за 12 месяцев — с большой вероятностью они будут разные.
В описанном вами случае, очевидно, что округлять следует только конечный результат. Если бухгалтерам не нужна точность до тысячных долей, то их можно отбросить в каком-то отчёте, или документе. Но отчёт за более долгий период не должен быть суммой других отчётов, которые были округлены. Он должен считаться по исходным данным.
Так это же классика.
Так в стартовом сообщении я так и написал про классику.
Он должен считаться по исходным данным.
Да, так и делается. Но проблема в другом — у бухгалтеров есть уже распечатанные ежемесячные отчеты с некоторыми округленными суммами, так они хотят, чтобы они также сходились с годовым, как в программе (которая округляет исходные данные), так и при подсчете на калькуляторе округленных заранее ежемесячных сумм.
Наверняка тут не "бухгалтерия хочет", а очень даже "налоговая хочет". Поэтому тут надо не решать какие-то абстрактные проблемы округления, а в точности закодировать единственный правильный порядок вычислений, который должен сообщить бухгалтер. Так что это ни разу не интересная задача.
«бухгалтерия хочет», а очень даже «налоговая хочет»
Помимо налоговой, есть еще и контрагенты, поставщики и т.д.
а в точности закодировать единственный правильный порядок вычислений
И где такой взять бухгалтеру? Выше уже ответили, что с понижением точности, будет расти разность — это математика. Можно нагородить костылей, но на то они и костыли, что где-то можно не учесть/забыть и опять все развалится.
При проверке банков у нас, я слышал, что ЦБ дает поле для маневра в несколько тысяч на дельту. И самый простой способ — это договориться, что несколько копеек/рублей вполне допустимая разница.
Недавно нашел забавный баг в Math(F).Round :D
https://github.com/dotnet/coreclr/issues/25857
Например, 150.515 внезапно округляется до 150.51, даже если указать MidpointRounding.AwayFromZero.
Выход тут только преобразование в decimal, так как double точности не хватает и в памяти оно выглядит не просто как 150.515.
С float ещё хуже.
Зря в первый коммент минус кинули. Бывают сюрпризы.
Почему бы не использовать старый добрый способ с (int)(x + 0.5 + 1e-6)
? В реальных задачах почти не бывает кейсов, когда нужно округлять числа типа 6.4999999
И вроде как незнание этой мелочи сильно не сказывается на вашей работе, но как только вам приходится работать со статистикой и расчетами показателей, базирующихся на куче всяких записей и циферок эта штука вылазит боком
Я вот тут совсем не понял аргументацию автора. Как банковское округление может «вылезти боком» при работе со статистикой и расчетами показателей, базирующихся на куче всяких записей, если это как раз тот случай, где его необходимо использовать. Математическое округление при работе с крупными массивами данных приводит к некорректным итоговым суммам, и как раз для этого и было придумано банковское округление, которое дает статистически верный результат на массивах данных :)
decimal — не есть тип с плавающей точкой
Спецификация C# с Вами не согласна.
docs.microsoft.com/ru-ru/dotnet/api/system.decimal
Represents a decimal floating-point number.
Для примера: есть три операции и итог: 3 333.33 + 3 333.34 + 3 333.33 = 10 000.00
Делим на тысячу, округляем, получаем: 3.3 + 3.3 + 3.3 = 10.0. Как-то глупо.
Пробуем еще: 3.3 + 3.3 + 3.3 = 9.9. Тоже глупо, потому что отчет проверяется через сальдо счета 10 000.00, а отчет показывает 9.9.
Значит надо добиться 3.4 + 3.3 + 3.3 = 10.0.
Таких функций округления уже нет.
Такое огругление пока делаю только руками в каких-то простых случаях. Считаю разницу, и добавляю её в первую/последнюю строку.
Или даже в этом простом подходе есть свои дырки, например если очень много мелких сумм, то разница целого отчета может дорасти до рублей и добавлять её в случайную строку — тоже глупо, потому что сумма отдельной строки может измениться до не узнаваемости. Значит надо размазывать её равномерно среди всех строк.
А если случаи сложные, например иерархия по вертикали с промежуточными итогами + месяцы по горизонтали с итогом по году, плюс суперитог в правом нижнем углу, то я не берусь такое «округлять», отвечаю дайте методику или делайте отчет без тысяч.
Есть какие-то универсальные подходы?
И напоследок пару слов о приведении типов:
var number = 6.9;
var intNumber = (int)number;
В этом примере я привожу тип с плавающей запятой (double в данном случае) к целочисленному int. Так вот, при приведении типов к целочисленному вся не целая часть просто отсекается.
Вопрос только в том, что отсекается.
Потому что такой вот код, переводяющий рубли в копейки, выдаст нам не всегда верный результат:
public static int ToInt32(float inputValue)
{ return (int)(inputValue * 100); }
Public Function MyRound(val As Double) As Double
Dim temp As Double
' Get fractional part
temp = (val - Math.Truncate(val)) * 100
temp = Math.Truncate(temp - Math.Truncate(temp)) * 1000
' Check if number >= x.xx499
If temp >= 499 Then
' Do a little rounding hack
Return Math.Round(val + 0.0011, 2, MidpointRounding.AwayFromZero)
Else
' Use standard rounding routine
Return Math.Round(val, 2, MidpointRounding.AwayFromZero)
End If
End Function
Сейчас не нашёл на msdn той самой статьи, где прямым текстом указана .NET особенность (баг/фича), при которой что-то вроде 0.2354 окугляется не туда, куда показано в основной статье. А вот такой «школьный» костыль вполне справлялся и чётко обрабатывал числа вроде 6.499999999999998 рукописные и вычисленные.
Округление к целому в .NET