Осторожно, ROUND !!!

Если читать в Справке, то

function Round(x: real): integer; Возвращает x, округленное до ближайшего целого.

Из описания мы видим, что функция принимает аргумент вещественного типа и возвращает целочисленный результат. Попробуем угадать, какое значение вернет эта функция в случае Round(3/2) и какое в случае Round(5/2).

Если рассуждать чисто математически, то 3/2=1.5, а 5/2=2.5 и следует ожидать по правилам округления результаты 2 и 3 соответственно. Проверим, что по этому поводу думает наш Паскаль?

Забавно он думает, не правда ли? Ошибка? Отнюдь нет, потому что так же думает и Free Pascal. Смотрим:

А как же “старый добрый” Турбо/Борланд Паскаль? А вот и неожиданность:

Такой вот прогресс произошел в компьютерной арифметике. Round для чисел, оканчивающихся на 0.5, прежде делал математическое округление, приводя результат к целому в сторону большего по модулю значения. А теперь он делает “банковское округление”, приводя результат к ближайшему четному значению.

Как итог: остерегаемся применять Round, если аргументом может оказаться нецелое значение, кратное 0.5. Помним, что нас подстерегает банковское округление!

Напоследок предлагаю запустить вот такую программку и полюбоваться на результат… это же генератор последовательности из пар четных чисел!

// PascalABC.NET 3.1, сборка 1192 от 07.03.2016
begin
  Range(3,30,2).Select(i->Round(i/2)).Println
end.

Это не так: см. «банковское округление». Именно этот метод реализован в .NET: Math.Random:

If the fractional component of a is halfway between two integers, one of which is even and the other odd, then the even number is returned.

Говорят, что в американских школах именно так учат поступать для округления чисел X.5. Конечно, давно следовало отразить этот факт в документации, а то вопросы всплывают регулярно.

В американских - возможно, но в наших - нет. И, кроме того, я написал эту заметочку для того, чтобы показать проблему, которая может возникнуть при переводе старых программ с ТурбоПаскаль на современные платформы.

Вы не поверите, но я в курсе. Я это к тому написал, что глобализация… Скоро придётся всех учить одинаково, а то вот проблемы.

Надеюсь, я до этого не доживу. Ибо американский стиль учебы - на самом деле не учёба, а натаскивание. Загорелась лампочка №2 - потяни за рычаг №18. И не дай бог, рычаг тот заклинит - натасканный вслед за ним в ступор придет. Это только в голливудских фильмах они мир спасают, проявляя чудеса смекалки.

Это всё мифы. Американская учёба так же, как и любая другая — разная. А насчёт фильмов и реальности: если не спасают, то тянут за собой мир основательно именно они: беспилотные и электромобили, марсоходы и гражданская космонавтика, многое другое… В конце концов, и интернет, в котором мы с вами общаемся, «возник как проект ЦРУ, так и развивается…». Что уж говорить про Microsoft .NET.

uses System;
function RoundMath(value: real): integer:= Convert.ToInt32(Math.Round(value, MidpointRounding.AwayFromZero));
begin
  write(RoundMath(3 / 2), RoundMath(5 / 2), RoundMath(7 / 2), RoundMath(9 / 2));
end.
2 лайка

Я “побеждаю” это нескольно проще:

// PascalABC.NET 3.1, сборка 1192 от 07.03.2016
begin
  Range(3,30,2).Select(i->Round(i/2+1e-15)).Println
end.

2 3 4 5 6 7 8 9 10 11 12 13 14 15

Оно конечно да, но тут есть опасность округлить 1.499999999999999 до 2.

Как и в Math.Round. Только истинные 1.4(9) еще надо суметь получить; чаще именно х.4(9) и выходит из-за особенностей представления и машинной арифметики, когда должно быть х.5. Тут вопрос скорее не в использовании для точного перевода каких-то жутких дробей в десятичные числа с округлением, а в генерации индексов или целочисленных элементов в последовательностях.

Вспоминается К.Айверсон, автор языка APL, который решил, что для практических нужд результаты “плавающей арифметики” всегда надо округлять с точностью до “некоторой пушинки”. Оно действительно удобно, когда надо сравнивать на точное равенство два вещественных значения.

Нет. В приведённом примере (1.499999999999999) написанная @Pavel_Devv функция RoundMath на основе Math.Round округлит к 1.

В приведенном-то округлит. Да и у меня все нормально получается с “довеском”. Речь немного о другом: никакая Math.Round не спасет от ошибок машинного представления данных с недостатком и может наступить момент, когда вместо точного 0.5, округляемого к единице, мы получим 0.4(9), округляемое к нулю.

С каким ещё «недостатком»? Числа с плавающей точкой хранятся по степеням двойки. Поскольку 0.5 это точная степень двойки, то с её представлением никаких проблем не возникает. Для 1.5 ситуация тоже довольно хорошая. А вот с примером, приведённым выше (1.49…) ваше решение не справляется.

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

А я не стесняюсь!. Хорошо стоять на арене во фраке и белых перчатках. Наверно Вы детишкам тоже объясняете, что надо “стандартно писать вот такую фиговину” и (как “вилька,бутилька”), “Дэти, это нэлзя понять, это надо запомнить!” (с)

В справка не совсем правильно написано. А так какие проблемы? Сейчас только этим округлением и пользуются ибо оно позволяет снизить погрешность при суммировании чисел. А то что в школах… Так там много чего оторвано от реальности…

Продолжая Вашу мысль, в вузах тоже много чего оторвано от реальности. Так что, не нужно учить вообще? Есть, увы (а может наоборот, хорошо?) учебные программы - им и приходится следовать. И при изучении арифметики детям сначала говорят, что от меньшего числа нельзя отнять большее, потом - что три (и уж тем более, единица) не делится на два, потом оказывается что отнимать можно, потом и делить можно, потом квадратные корни не извлекаются из отрицательных чисел, потом извлекаются… нельзя с нуля сразу получать некое “высшее знание” - этого ни один мозг не выдержит.

Я поправил подсказку на Round чтобы не возникало вопросов

1 лайк

Проверил на нормальность ваш довесок. Не работает.

begin
  var i:=1.499999999999999;
  writeln(i);
  writeln(System.Convert.ToInt32(System.Math.Round(i, System.MidpointRounding.AwayFromZero)));
  writeln(Round(i));
  writeln(Round(i+1e-15));
end.

результаты такие:

1.5
1
1
2

Поэтому такой довесок будет при любом нечетном с дробной частью .499999999999999 ошибаться на единицу вверх. такие случаи наверно нечасто бывают, действительных чисел достаточно много, но из-за этого и мест где можно ошибиться тоже много. в сторону положительной бесконечности - (2*n+1)+.499999999999999, в сторону отрицательной тоже с шагом в два, но с дробной частью .500000000000001. Как заметил @Ulysses - решение далеко от идеала.

Давайте уже не будем это обсуждать - я поправил подсказку, и считаю, что этого достаточно. В подсказке сказано, как работает Round.