Pull to refresh

Comments 29

больше никогда не буду писать от лени int(x+0.5)

Почему бы и нет, если заведомо известно, что аргумент неотрицателен и влезет в int, а нам нужно работать именно с int? В этом случае `int(x+0.5)` будет значительно эффективнее по производительности, чем `int(round(x))` (операция roundss имеет больший latency, чем простое сложение).

Как вариант, потому что вот это "заведомо известно" потом превращается в изменения исходных требований, и опа — вылезли за границы допустимых входных данных :( С другой стороны, производительность это тоже серьезный аргумент.

Округление (round) в Go нелегко сделать правильно


А деление вычитанием в этом фантастическом языке делать не надо?
Я правильно понял, что итоговая Round() в 1.10 округляет случаи ровно между двумя целыми — по направлению от нуля?
Откуда взялась десятичная точка в двоичном мире?
Для тех, кто не понял: называть двоичную точку десятичной и странно и некорректно

{-0.49999999999999994, negZero}, // -0.5+epsilon
{-0.5, -1},
{-0.5000000000000001, -1}, // -0.5-epsilon
Первый и третий кейс понятны. Но почему округление -0.5 должно приводить к -1? Математически верно приводить к 0 (ну или к -0, если есть это спецзначение).
Или в фразе
Я буду рассматривать округление к ближайшему целому, причём 0,5 будет округляться в большую сторону.
имелось в виду в «большую по модулю»?
Но почему округление -0.5 должно приводить к -1?

По крайней мере у round() в C такое же правило — цитирую по линуксовому ману:
These functions round x to the nearest integer, but round halfway cases away from zero (regardless of the current rounding direction, see fenv(3)), instead of to the nearest even integer like rint(3). For example, round(0.5) is 1.0, and round(-0.5) is -1.0.
В доступных текстах стандартов то же самое по сути.
Так что если это решение не идеальное, то по крайней мере традиционное.


(В идеале, я бы предпочёл видеть или дополнительный параметр направления округления, или раздельные функции для каждого режима округления. Но сейчас и введение только одного варианта — уже успех.)

Вообще говоря, округление — это чуть более сложная и разнообразная операция с числами, чем это преподаётся в школе. И самый спорный фактор тут, как правило, куда сдвигать середину (0.5). Вот лишь возможные очевидные ответы: большему/меньшему целому, большему/меньшему целому по модулю, а также к ближайшему чётному/нечётному. У каждого из этих подходов есть плюсы и минусы.
Округление к меньшему по модулю имеет вот такое замечательное свойство: round(x) + round(-x) == 0
В большинстве случаев, встречается округление к меньшему как поведение по умолчанию.
Статья на вики
Округление к меньшему по модулю имеет вот такое замечательное свойство: round(x) + round(-x) == 0

Округление к чётному, к нечётному, к нулю (к меньшему по модулю) и от нуля — все одновременно имеют это свойство.


В большинстве случаев, встречается округление к меньшему как поведение по умолчанию.

Хм… вообще-то в большинстве случаев или к ближайшему, а середину — от нуля (как в обсуждаемой реализации), или к ближайшему, а посредине — к чётному (банкирское, оно же гауссово округление). В школе нас учили единственному варианту, что округление — к ближайшему, а середину — от нуля.
В терминах IEEE754-2008 это соответственно roundToNearestTiesAway и roundToNearestTiesToEven.


Статья на вики

Статья хорошая как вводной обзор, но сильно неполная.
Нет, например, фоннеймановского округления, про которое см. документацию IBM:


Round to prepare for shorter precision: For a BFP or HFP permissible set (двоичная арифметика), the candidate selected is the one whose voting digit has an odd value. For a DFP permissible set (десятичная арифметика), the candidate that is smaller in magnitude is selected, unless its voting digit has a value of either 0 or 5; in that case, the candidate that is greater in magnitude is selected.

Часто или редко встречается округление к меньшему, это, конечно, открытый вопрос. решил проинспектировать свой инструментарий. Получилось примерно поровну

PHP round(-0.5) = -1
Exasol select round(-0.5) = -1
JS Math.round(-0.5) = 0
Java Math.round(-0.5) = 0
MySQL select round(-0.5) = -1
C round(-0.5) is -1.0
R round(-0.5) = 0
Erlang round(-0.5) = -1
Чтобы понять, какой именно вариант работает в каждом конкретном случае, надо сравнивать не один случай -0.5, а несколько: например: -1.5, -0.5, +0.5, +1.5.
Тогда получается соответственно для этих значений (в том же порядке, что у Вас; Exasol не знаю, пропускаю):

PHP round(): -2, -1, 1, 2 (середина — от нуля)
Javascript Math.round(): -1, 0, 1, 2 (середина — к +INF)
Java Math.round(): -1, 0, 1, 2 (середина — к +INF)
(но рядом есть rint(), который округляет середину к чётному: -2, 0, 0, 2)
MySQL select round(): -2, -1, 1, 2 (середина — от нуля)
C round(): -2, -1, 1, 2 (середина — от нуля)
R round(): -2, 0, 0, 2 (середина — к чётному)
Erlang round(): -2, -1, 1, 2 (середина — от нуля)

Кроме того:

Python3 round(): -2, 0, 0, 2 (середина — к чётному)
Perl POSIX::round: как в C (середина — от нуля)

как видите, до сих пор ни одного случая ни «середина к нулю», ни «середина к меньшему». Особый случай — Java и склонировавший её Javascript, где, наоборот, середина — к +INF (интересно, кто и зачем такое выбрал?)

Спасибо, вы правы, поправил текст.

Пользуясь случаем хочу спросить, почему в этом коде все ок, на выходе будет 7000
$x = 0.00007;
$y = $x * 100000000;
echo($y); //outputs 7000


… а вот здесь на выходе будет 6999?
$x = 0.00007;
$y = $x * 100000000;
$y = (int)$y;
echo($y); //outputs 6999, why??

UFO just landed and posted this here
Могу предположить следующее: $y равен 6999.999999… после умножения.
Функция которая печатает, округляет флоаты до определенного знака.

Так и есть (по крайней мере в double == float64).

UFO just landed and posted this here

Всё верно, а такое поведение связано с тем, что echo кастует все параметры к string следующим образом:


case IS_DOUBLE: {
    zend_string *str;
    double dval = Z_DVAL_P(op);

    str = zend_strpprintf(0, "%.*G", (int) EG(precision), dval);
    /* %G already handles removing trailing zeros from the fractional part, yay */
    ZVAL_NEW_STR(op, str);
    break;
}
Меня несколько удивляют подобные реализации. Я так понимаю, весь сыр-бор из-за платформ, где нет соответствующих инструкций? Но ведь представление чисел с плавающей точкой стандартизировано, правильнее действительно будет работать напрямую с представлением.
«Но я надеюсь, что теперь вам стало понятно, как устроено округление в Go и как нужно тестировать реализации округления»
да использовать их надо, а не тестировать
программист он для заказчика программист, а для языка программирования пользователь

«корректно работающее округление появилось лишь в шестой мажорной версии Java (через 15 лет, прошедших с релиза Java 1.0 до выхода Java 7)»
15 лет… пендосиннопром жжет…
мне просто интересно, если бы такие базовые штуки, как округление, я выдавал бы своим пользователям через 15 лет,… да нет, я бы не дождался результатов теста, столько не проработал бы.
и ведь на этом языке до сих пор большая часть вакансий в нашем замкадье-захолустье
А чего, такая штука не проходит
if math.IsNaN(x) {
return x
}
return math.Copysign(1.0, x) * math.Floor(math.Copysign(1.0, x) * x + 0.5)
странно… ваша функция (переписанная из postgres) фейлится на ваших же тестах:
=== RUN   TestRound7
--- FAIL: TestRound7 (0.00s)
	main_test.go:37: round( -0.5 ) = -0 	!= -1
	main_test.go:37: round( 0.5 ) = 0 	!= 1
	main_test.go:37: round( 2.2517998136852485e+15 ) = 2.251799813685248e+15 	!= 2.251799813685249e+15

Тесты я брал из статьи. В builtins_test.go теста для round не нашел.
Там округление к ближайшему чётному, если дробная часть равна 0.5, тесты в статье же на округление к большему по модулю.
команда Go согласилась добавить math.Round в Go 1.10! И даже появилась работающая реализация

Подскажите, пожалуйста, как правильно на базе этой реализации округлять до двух знаков после запятой. Например, по коду получаю 63 и -63, а хочу получить 62,84 и -62,84:
func main() {

	x := 62.83444444444446
	y := -62.83444444444446
	fmt.Println(Round(x))   //  63 --> want  62.84
	fmt.Println(Round(y))   // -63 --> want -62.84
}

В других случаях, — умножал и делил на сто, пример здесь: play.golang.org/p/jE9Q8BSsHQt

Точное представление десятичных дробей в виде конечных двоичных дробей невозможно, поэтому ответ — никак. Можно умножить на 100, взять round, поделить на 100 и вывести число на экран со спецификатором "%.2f".

Спасибо за помощь. Делал по варианту из статьи
github.com/montanaflynn/stats

вот так: play.golang.org/p/d_OUvLXSWaJ
Вариант на базе math.Round, когда берём битовое представление числа, сдвигаем его и применяем маску…, — правильно подправить не получается.

Я всё-таки советую добавлять спецификатор точности при выводе, чтобы в какой-то момент не увидеть 62.840000152587890625 вместо 62.84.

Sign up to leave a comment.