Comments 101
Кстати, про эту проблему очень часто не задумываются — в том числе там, где идут серьёзные вопросы (деньги). В сентябре 2022 года я сделал такой скриншот интерфейса «СберМегаМаркета». Понятно, что это ни что это не влияет, это просто отображение на фронтенде.

Любопытно, я думал, что деньги всегда считают только целочисленной арифметикой.
Думаю, на бэке обрабатывают как целое число копеек, а при необходимости делят на сто. Очень надеюсь — иначе кто-нибудь опять сожжёт офисное здание из-за конфискованного красного степлера.
Насколько предполагаю, это они на фронте не получают с бэка изначальную цену без скидок, а просто сложили реальную заплаченную сумму со всеми скидками и вывели как изначальную цену. При этом складывали наивно — как числа с плавающей запятой.
А на что отсылка с красным степлером?
Ни разу не видел хранения в целых копейках, хотя работаю с этим достаточно давно. Почти всегда это DECIMAL(X,Y), причем Y>=4. Изредка, когда нужна не сумма как таковая, а расчет статистического показателя бывают float/double.
Аналогично, при этом всё время читаю статьи про то, как важно копейки хранить отдельным целым
Особого смысла в их хранении отдельно нет. Проще хранить всю сумму не в рублях, а в копейках (или там в десятых/сотых копейки -- зависит от конкретных требований).
Смысл появляется, когда вы конвертируете валюты. Там почти всегда на выходе получается число с множеством цифр после запятой. Округлять до 1/100 копейки? Тогда в масштабах страны на 100+ млн населения получится неплохой доход для того кто внедрит <правильный> алгоритм округления. Поэтому в банковской сфере используется плавучка.
Хранение в копейках как целым числом очень удобно. Начисление и тп можно хранить в тех же double, но после агрегации - уже опять в копейках.
Это всегда гарантирует вам safe сравнение и базовые операции с числами.
Ни разу не видел хранения в целых копейках
А я видел. Сумма хранится в базе в копейках в поле с типом int8. Довольно интересное решение.
Соответственно, чтобы получить сумму в рублях, её надо поделить на 100
Int8 — это максимум 1,27 рубля, что ли? Или имелось в виду, что копейки хранятся в отдельном поле от рублей?..
Или имелось в виду, что копейки хранятся в отдельном поле от рублей?..
Нет, вместе.
Int8 — это максимум 1,27 рубля, что ли?
Это int8 из постгресса, который псевдоним для bigint. Диапазон от -9223372036854775808 до +9223372036854775807, хватит с головой :)
Подобных пробем в вычислениях на самом деле много и только часть из них решается хранением финансовых данных в виде целочисленного количества копеек. Вообще этим всем занимается так называемая вычислительная математика, которую преподают в нормальных вузах.
у postgres даже есть специальный тип данных money где под капотом int8, у которого две правые цифры становятся копейками. вот почему этот тип не используют?) ну классный же для денег
Даже официальные курсы валют обычно имеют 4 знака после запятой. Так что обычно хранится 4 или больше.
Курс, это совсем не количество денег. Т.е. после умножения/деления числа рублей и копеек на курс результат округляется до центов. Понятия не имею, по какому финансовому правилу.
ну для этих случаев есть Decimal в кликхаус и numeric в постгрес. и то и другое под капотом в интах значение хранит, но цифры после запятой можно задать
Ещё от архитектуры процессора зависит. Мэйнфреймы IBM от самого своего рождения (Система 360, 1964 год) имеют поддержку двоично-десятичных чисел (до 31 десятичной цифры плюс знак), и на них "экономическая" информация естественным образом считается в десятичной системе. Вот на ПК (IA-32, AMD64 -- неважно) или, скажем, на ARM любой разновидности такие вещи приходится считать, используя целочисленную арифметику; плавающая запятая -- лишь для задач, где ошибки округления приемлемы (скажем, научные расчёты: там не обязательно, чтоб последняя циферка сходилась, достаточно лишь обеспечить необходимую точность).
Хуже, когда выводится округлённое значение, а при попытке перевода, пишет, что не хватает денег. Что об этом думают обычные люди - загадка
Первое требование — безопасность при обратном преобразовании:
Да ладно. Такое требование возникает относительно редко – только при сериализации, а не при выводе для пользователя.
Не только в сериализации. А вообще с первых фраз я упомянул, что пользователь может указать точность, которая ему нужна. И предположил, что по умолчанию нужна полная точность.
Ну вот это предположение, как правило, неверно. Но задача да, остаётся сложной, и много ответвлений. Например, считаем процентные доли чего-то и выводим – получаем 80% +13% + 6% (ну или там с десятыми-сотыми – сколько знаков пользователю нужно). Всегда найдётся кто-то, кто их сложит и будет возмущаться.
Может для float нужно хранить не только значение, но и погрешность (в том числе накопленную при вычислениях)? Это позволило бы сразу понимать, где значащие разряды, а где мусор.
А в какой форме хранить погрешность? Либо в длинной арифметике, но тогда уж сразу нужно в ней считать, либо во флоатах, и тогда нам нужна будет погрешность на погрешность :)
Ну можно хранить не точное значение, а оценку сверху - порядок погрешности. Тогда хоть в int'е.
Вообще люди уже об этом думали, конечно же. Но это сильно небесплатно, и часто можно обойтись без таких ухищрений. Если не принимать нужных мер, то зачастую интервалы расходятся, таким образом не давая никакой полезной информации. А если принимать нужные меры, то нередко интервалы не нужны.
А в какой форме хранить погрешность? Либо в длинной арифметике, но тогда уж сразу нужно в ней считать, либо во флоатах, и тогда нам нужна будет погрешность на погрешность :)
Физики у погрешности пишут только первую цифру и порядок. Обычно цель - оценить порядок бедствия и понять, в каком месте теряется точность.
Для этой цели вполне хорошо подходит пара float, где первый float это значение и второй это погрешность.
Формулы для погрешности сложения/умножения/синусов/косинусов и сходны с теми, что получаются при вычислении производных, там ничего сложного нет.
Из минусов только то, что арифметических операций будет больше.
Ну всё зависит от того, что и как вы считаете. У меня вычисления представляют собой длиннейшие цепочки, и чисто одной парой флоатов не обойтись.
Дело в том, что хорошие вычислительные методы, дают предсказуемую погрешность, зачастую значительно меньшую, чем погрешность отдельных операций в составе их алгоритмов. "остаточный член на n-ной итерации", такие вот штуки, и, конечно, кому это надо, они это считают. Смутно конечно помню это с курсов в универе. Но суть представляю: зачем на каждую операцию погрешность считать, тратя на это время, если про многие популярные методы вычислений эти погрешности уже аналитически рассчитаны.
Физики у погрешности пишут только первую цифру и порядок.
А в двоичной системе первая цифра всегда будет 1.
?
Мы говорим о погрешности, которая по определению ненулевая.
Эм. Покажите, пожалуйста, определение?
Физики у погрешности пишут только первую цифру и порядок
Отнюдь. Гравитационная постоянная
G = (6,67430 ±15) *10E−11 м3/с^2/кг рекомендованное, или
G = (6,674184±78) *10E−11 м3/с^2/кг уточнённое в эксперименте
Или оценка постоянной Хаббла
74,03 ± 1,42 (км/с)/Мпк
Я, наверное, расскажу про Кэхэна и Бабушку в одной из статей моего ликбеза.
Кто об этой проблеме не подозревал, зумеры-вайбкодеры? Статья интересная, заголовок ужасен.
Предложите новый заголовок? Лично я не зумер, но сам никогда не задумывался, пока не понадобилось свою библиотеку написать, что вывод хотя бы просто целой части флоата уже требует длиной арифметики.
А что научные статьи на эту тему продолжают выходить сегодня, тем более.
Как же без кликбейта. Хорошо что не "ТОП1 проблем, о которых не подозревают 99% программистов!"
Вася, зырь, миллениалы открыли для себя экспоненциальную форму хранения чисел (и прилагающиеся к ней детские грабли)!
Я правильно понял, что что вы предлагаете перестать писать учебники?
вы предлагаете перестать писать учебники?
Я предлагаю начать их читать.
(Прикиньте, я об этой проблеме знал ещё до того, как у меня появился компьютер. Из книжек.)
А писать-то надо, или нет? Переведёте свой первый комментарий с саркастического на понятный?
Писать‑то надо, но не на уровне «если получается Н.Е.Х., надо камлать заклинание import Decimal
», а на уровне «На Самом Деле™ в битовом поле хранится N бит мантиссы и M бит порядка, [через 10 страниц] и поэтому надо подключить правильную библиотеку».
Но, конечно, миллениалы — не читатели...
Писать‑то надо, но не на уровне «если получается Н.Е.Х., надо камлать заклинание
import Decimal
», а на уровне «На Самом Деле™ в битовом поле хранится N бит мантиссы и M бит порядка, [через 10 страниц] и поэтому надо подключить правильную библиотеку».Но, конечно, миллениалы — не читатели.
Если честно, я так и не понял, что именно вы имете против миллениалов, и кого вы имеете в виду.
Мне кажется, что я вложил больше усилий в попытку вас понять, нежели вы в попытку мне донести свою мысль. Возможно, конечно, что я ошибаюсь, но у нас разговор как-то не складывается.
Вы статьи по приведённым в моём самом первом комментариии ссылочкам прочитали? Нет? Ч.Т.Д.
Я внимательно читаю комментарии, и посмотрел статьи по вашим ссылкам сразу же (оказалось, что я их пристально изучал до того). Что же означает ваш комментарий, для меня осталось загадкой.
Кажется, я начал догадываться. А вы же самую первую ссылку в моей статье пропустили, да?
И я пропустил. И ссылку, и ту саму статью. Меня еще в 9-м классе учили переводить числа из десятичного представления в двоичное и обратно. Угу, карандишиком на бумажке в клеточку. Ага, 1972 год, тяжелое детство, деревянные игрушки, двухтомник Лорана Шварца (чуть позже, правда). И поэтому заголовок данной статьи меня несколько изумил. Потому что, то, о чем вы пишете. оно в подкорке. Все, как отношение котов/кошек с ежами. Ну нет такого зверя, в упор не вижу. Ну как я могу, хозяин, уколоться, нет же никого тут, кроме нас с тобой.
Миллениалы открыли её минимум 15 лет назад. Теперь, наверно, уже очередь зуммеров.
В отдельных ЯП встречается тип данных Rational Numerics, где числа хранятся в виде числителя и знаменателя, поэтому в них 0.1 + 0.2 == 0.3 без сюрпризов.
Да, но это цикл статей "санпросвет о плавающей точке".
Mille pardons за мое грубое вмешательство
санпросвет
Уже не первый раз встречаю это слово на Хабре за последнюю неделю в совершенно неподходящем контексте. О каком санитарно-эпидемиологическом просвещении речь? Похоже, стоит вам провести ликбез.
Для желающих чиста картинки посмареть.
По этой дороге можно зайти очень далеко: https://habr.com/ru/articles/883366/
Есть же крейт rust-decimal
какую бы десятичную строку мы ни вывели
Вопрос - что такое "вывели"? И для чего это "вывели" требуется. Вывели на экран? Вывели сумму в документе? Этот "вывод" требуется для представления человеку. И если человек складывал 0.1+0.2 то выводиться должно 0.3, как бы оно там в двоичном преобразовании туда-сюда не выглядело. Сколько знаков после запятой в слагаемых, столько же должно "выводиться" в сумме. 0.11+0.22=0.33 3.14159+0.00001=3.14160 0.1+0.001=0.1001
Сколько знаков после запятой в слагаемых, столько же должно "выводиться" в сумме.
А сколько их в слагаемых? Что если мы сложили 0.1+0.2 на сервере а потом результат передали на клиент?
А что должен человек увидеть на клиенте? Я говорю о том, что есть внутреннее машинное представление, а есть "вывод на экран" для человека.
поидее можно как-то понять сколько разрядов в a b, и вывести в таком же стиле, но именно в этом случае получается сервер отправил такой ответ, тогда можно пойти дальше, что должно показаться если 0.1 / 0.1234
тоесть 4 или 8 знаков после запятой или все или поидее, можно наверно что-то сделать типо обвести наверное, пока сам не знаю)
Секунду, а как должен выглядеть вывод на экран, например ? Не все действия сводятся к простым суммам. Да и с суммами быстро проблемы начнутся, не зря в финансовой математике нужны специальные ухищрения.
Ну, а чему равен синус 0? .
Хорошо, давайте я с другого края зайду. Как выводить на экран значение ?
Да, сколько знаков после запятой выводить должен определять человек.
Синусы как раз прекрасно сводятся к простым суммам (ряды Тейлора), но не понятно, как это вообще должно быть связано с отображением результата. В sin(1) ни конечной в десятичной записи рациональной дроби, ни даже периодической вы не увидите, поэтому, по сути вопрос в том, на каком знаке после запятой его округлить. А это примерно "как пользователю нужно". Меня бы 0.01745 бы устроило, кроме тех случаев, когда нужно дальше с этим что-то считать.
Кому должно? преобразование в двоичное представление необратимо вносит ошибку для десятичных дробей.
И что? Сложите в калькуляторе Windows 0.1+0.001 - будет он на экране результат отображать с ошибкой? А если команда print выводит на экран черти что - то кто в этом виноват?
зы.
Сколько знаков после запятой в слагаемых, столько же должно "выводиться" в сумме. 0.11+0.22=0.33
В общем случае "программа не знает" конечно сколько знаков после запятой было в слагаемых.
a=0.1
b=0.2
c=a+b
print c
В этом случае print не знает, сколько знаков выводить.Программист должен это явно как-то указать.
по стандарту получается знает сколько бит у float задействовано в счете
числа в памяти храняться определенным образом там стандарт есть, функция выводящая на С имеет интерфейс под задание количества после запятой, и функция перегоняющая в строку тоже такой же интерфейс имеет
по стандарту получается знает сколько бит у float задействовано в счете
числа в памяти храняться определенным образом там стандарт есть
Вот поэтому я и пишу свои статьи: чтобы их читали. При счёте у флоата задействованы все биты.
по вашему примеру я вижу что память выделится под все, а биты будут определенные, в стандарте тоже на сколько помню мы видим сколько в целой части и мантиссе разве нет?
есть формат float вот например как у вас
https://godbolt.org/z/35j944xzM
может можно разобрать float на метаданные
например те данные, которые будут удобны для понимания
целое число
мантисса
количество какоето чтобы вернуть обратно
https://godbolt.org/z/qPKc39K5h вот типо такого
За статью спасибо, пишете хорошо, о существовании прямо нескольких и замороченных алгоритмов для такого лично я не подозревал. О существовании типов decimal и rational действительно знают достаточно малое людей только потому, что они не на слуху. Ещё меньше людей знают, что формат с плавающей точкой был придуман не для дробных чисел, а для инженеров и физиков, у которых есть как очень большие, так и очень маленькие фундаментальные константы.
Спасибо на добром слове. Знаете, что меня удивляет*? Эти статьи о замороченных алгоритмах имеют крохи цитирований. Например, один из основоположников процитирован 34 раза..
И это при том, что во всех рантаймах всех толковых языков есть один из этих алгоритмов...
Скрытый текст
На самом деле, не удивляет, поскольку эту кухню я знаю слишком хорошо.
С помощью decimal.getcontext().prec = (число знаков после запятой) можно получить точность намного больше 64 знаков после запятой
я тоже заметил кое-что щас меня закидают тапками, возможно, кароче можно легко задаться вопросом от всего этого почему в ПК и как появились числа, если их надо так и так 1 функцией прогонять туда-сюда, я столкнулся с этим при написании, пускай и на аски текстового редактора, и где-то день просто философски размышлял зачем в пк нужны числа, если по итогу функция для всего этого 1
самая простая функция по взятию числа в строку или самописный делитель или sprintf/snprintf
и вот, да я видел стандарт тот, что-то тоже читал, и всё равно задам вопрос почему в пк есть типы чисел если есть строки
получается ситуация, мы видим строки, но считаем алгоритмы числами используя строки
Потому что скорость расчёта в "десятичном текстовом" виде будет многократно меньше, чем при естественном для машины двоичном числовом, а схемы для всего этого будут существенно сложней.
Я бы Вас закидал тапками, за то что в начале предложения "Shift" ленитесь нажать. Палец, блин, отвалится.
Проблема, о которой вы наверняка не задумывались: print(.1+.2)