Как стать автором
Обновить

Комментарии 43

Хотя и не гарантирует их отсутствие.

Гарантирует, пока не превысится точность. Точность не бесконечна естественно, хоть и больше, чем у double.

В JS (как и в шарповом double я думаю) даже без каких либо алгебраических действий можно получить такую кривизну. Просто читаешь ораклом из базы дробное число, получаешь не то что в базе записано. Решали тем, что в селекте значение кастили к строке, а в коде делали new Decimal(val). Библиотечку Decimal использовали, ибо работали с «деньгами» и такие double-фокусы были недопустимы.
Библиотечку Decimal использовали, ибо работали с «деньгами» и такие double-фокусы были недопустимы.

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


При работе с деньгами, например, естественным требованием является коммутативность деления и умножения, что никакими численными классами (кроме Rational, есть таковой представлен в языке) не обеспечить. Пример: есть рубль, его нужно разделить на троих вкладчиков, а через год собрать обратно для расчета выплаты дивидентов. Оп-па, копеечка потерялась. Первый же аудит завернет такую математику далеко и надолго.


Числа для денег используют только совсем зеленые юнцы, будь эти числа хоть трижды decimal.

Кругом столько минусов, что хочется, чтобы кто то написал статью(или дал ссылку) как правильно работать с деньгами.
Как сохранять в БД бесконечные дроби, да хоть бы и конечные, но с большей точностью, чем стандартные типы. Даже не арифметические операции с такими числами, а просто сохранить в базе и считать без потери.

ЗЫ У нас не было операций деления. Сложение, вычитание и умножение на целое количество процентов(не дает бесконечных дробей, очевидно). А порой просто ввод, хранение и вывод. Double валится уже тут. Decimal на этом всём работает. Это кстати были не банковские деньги, а рассчеты, что то типа выставления сметы.
как правильно работать с деньгами

Fowler’s Money pattern. А так — ну загляните в код любой библиотеки для любого языка же. RubyMoney, ExMoney, ElixirMoney, наверняка для вашего стека тоже есть что-то подобное.


Как сохранять в БД бесконечные дроби

Как Rational, очевидно, если число рациональное. Иррациональным числам там взяться неоткуда (если вы не выплачиваете дивиденты пропорционально квадратным корням из π, конечно :). Нужно хранить два поля, номинатор и деноминатор. Если ваш язык не умеет Rational из коробки, придется его этому научить. Вот пример на руби:


r1, r2, r3 = Rational(1, 3), 2/9r, 4/9r # 1, 3 — из базы
#⇒ [(1/3), (2/9), (4/9)]
(r1+r2+r3).to_d(3)
#⇒ 0.1e1
если вы не выплачиваете дивиденты пропорционально квадратным корням из π, конечно

Для иррациональности достаточно просто корней. В принципе и экспонента и логарифмы тоже наверное могут появиться. Хотя можно пойти дальше и записывать последовательность операций в аналитическом виде.

Для иррациональности достаточно просто корней.

Спасибо, буду знать.


и экспонента и логарифмы тоже наверное могут появиться

Откуда? Не нужно пытаться решить сферическую задачу в вакууме, нужно решать существующие задачи максимально хорошо.


можно пойти дальше и записывать последовательность операций в аналитическом виде

Иногда можно, да. Вы, наверное, думаете, что удачно сострили, а тем не менее мы даем возможность нашим клиентам определять практически любые формулы для срабатывания триггеров на изменении курсов валют. Я даже вынужден был специальную библиотеку написать. Как вы думаете, сколько клиентов воспользовались формулой, отличной от неравенства типа курс > 1.2?

Откуда? Не нужно пытаться решить сферическую задачу в вакууме, нужно решать существующие задачи максимально хорошо.

Я не занимаюсь финансовой деятельностью, не могу сходу сказать. К тому же приписал "наверное", это значит неточно.


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

Я так не думаю — вам показалось.

Так и я решах существующую задачу, а не сферическую задачу в вакууме, и эта задача прекрасно решается через Decimal.
И Rational тоже допускает потери, по определению, т.к. не имеет бесконечной точности, и к нему также применимо выражение «гарантирует отсустствие потерь, пока не превысится точность», как и к Decimal. В отличие от Double, который дает погрешность уже на примере операции 0.1 + 0.2
Rational тоже допускает потери, по определению.

У вас неверное определение. Рациональные числа Rational обрабатывает без потерь. Хоть факториал миллиарда туда запихните, если памяти хватит. И делает он это именно что по определению.


Ни в руби, ни в эрланге, ограничений точности не существует. Если в вашем языке существуют — придется реализовывать самому, я ж говорил там уже.

Да, в шарпе нашел в том числе реализацию с BigInteger, бесконечным целым. (первое что нашел было с uint). Стало интересно, не случается ли риск при каких нибудь безобидных действиях получить удар по производительности/памяти?
Риск есть. BigInt иммутабельный, то есть у вас постоянно создаются новые объекты. И если у вас достаточно большие числа и/или много операций, то…

Плюс операции на BigInt заметно медленнее.
если у вас достаточно большие числа и/или много операций, то…

Как же, интересно, тогда хаскели с эрлангами выживают, да еще и уделывают шарп по производительности в хвост и в гриву?


операции на BigInt заметно медленнее

В диапазоне uint это не так, а вне этого диапазона сравнивать не с чем.

Как же, интересно, тогда хаскели с эрлангами выживают, да еще и уделывают шарп по производительности в хвост и в гриву?

Без понятия, я им под капот не заглядывал. Но у шарпа эта проблема есть. И даже сам майкрософт пишет
The other numeric types in the .NET Framework are also immutable. However, because the BigInteger type has no upper or lower bounds, its values can grow extremely large and have a measurable impact on performance.


В диапазоне uint это не так, а вне этого диапазона сравнивать не с чем.

По моему опыту это и в диапазоне uint так. Ну и как бы вот по быстрому нагуглилось:
BigInteger vs Int64
Now that we can easily compute values in BigInteger, the next question is performance. What is the performance of BigInteger compared to Int64? To determine this, an integer random value is obtained, then added to itself 500 million times, first as an int64 value and then as a BigInteger value, with the calculation times compared.
The BigInteger calculation loop is over 52 times slower. Both the Int64 and BigInteger values are immutable; a new copy of the value is created in memory each time it is modified. In fact all integral types, including the venerable Integer data type, are immutable. The extra complexity of creating an arbitrarily large number, even when the number is no larger than a standard numeric type, causes the difference in performance. To minimize this performance penalty, especially when modifying BigInteger values in loops, perform as many calculations in standard integral types as possible and assign to the BigInteger structure only when necessary.
visualstudiomagazine.com/articles/2011/01/25/biginteger.aspx?m=1
Ну то что оверхед есть это понятно, мне интересно часто ли случается такое, что при какой то относительно простой операции результатом будет число с огромными нумератором и деноминатором, что одно число сожрет пару гигов памяти и соответствующую же производительность.
Я бы сказал что одной относительно простой операцией вы себе вряд-ли сможете создать большие проблемы.
А вот если много операций, да ещё если их скажем распараллелить…

В общем я OutOfMemoryException из-за использования BigInteger видел. Причём я так и не понял зачем там был использован этот самый BigInteger.
то что оверхед есть это понятно

Нет, это тоже непонятно :) Судя по всему (и по цитате, приведенной Kanut), в шарпе очень наивная реализация. Ни в руби, ни в эрланге (говорю про то, о чем знаю) — значимого оверхеда нет, пока есть такая возможность, используется встроенный тип, ложащийся на архитертуру железа. То есть, пока вы не покинули диапазон интов, под капотом используется этот самый инт. Ну да, есть проверка на выход из диапазона, и поэтому все-таки небольшой оверхед есть, но это даже не проценты.


число с огромными нумератором и деноминатором [...] сожрет пару гигов памяти

Мы годы используем Rational и я никогда не видел ничего даже отдаленно похожего. Наверное, сто́ит написать заметку о том, как оно реализовано, скажем, в эрланге: код открыт, и он не особо прям сложный. В руби все еще проще: Матц просто использовал алгоритм Bruno Haible стянутый из Common LISP.

значимого оверхеда нет, пока есть такая возможность, используется встроенный тип, ложащийся на архитертуру железа
В native int64 — это просто
add rax, rbx
В любом bigint/rational типе — это возня с объектами. В регистры кладутся ссылки, вызывается метод add, внутри проверяются флажки, что каждый из аргументов сейчас хранится как native int64, выполняется операция, проверяется, что результат опять не вышел за пределы int64, кладём результат обратно в поле объекта по ссылке. Достаточно, чтобы получить 50x замедление?
Если бы ещё платёжные шлюзы и банковские API принимали честный Rational.
Ах, да, их же поголовно писали зеленые юнцы
Если бы ещё платёжные шлюзы и банковские API принимали честный Rational.

Это не требуется. Расчет — что в платежном шлюзе, что в банке — это терминальная операция, там все равно придется округлять до копеек. Внутри же вашей системы далеко не все операции терминальные. Это просто.


Ах, да, их же поголовно писали зеленые юнцы.

Не умеете в сарказм (особенно если не особо понимаете предметную базу) — не нужно и пытаться.

Пример: есть рубль, его нужно разделить на троих вкладчиков
Я хочу получить свою 1/3 на счёт, а не так, что кому-то достанется больше, или остаток уйдёт банку.

Банки оперируют целыми землекопами :)


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


Также с обменом валют: суммы считаются до четвертого знака после запятой (для валют с деноминатором 100 — это 0.01 цента), но получите на руки/счет вы округленную в пользу банка сумму.


Финансовые воротилы живут по своим законам, но это не повод в своем приложении накапливать ошибку без всякой на то причины.

Должен быть регламент как правильно считать, невозможно достичь абсолютной точности, вводятся округления, описываются конкретные формулы в соответствующих актах. Например вычисленный НДС от цены и от итоговой суммы в накладной всегда будет разный какой бы тип данных не использовать, поэтому в налоговом кодексе даны конкретные разъяснения по этому поводу, в банковской сфере думаю аналогично.

Для терминальных расчетов — это так, все без исключения внутренние расчеты можно и нужно делать с абсолютной точностью. Как именно — я подробно расписал выше.

Нет, это не так работает. Вы не можете отсыпать каждому вкладчику 0,(3) копейки. В банке и на счёте всегда только целое число минимальных единиц (в данном случае копеек). И что делать с оставшейся 1 копейкой после разделения на троих определяют бизнес-правила конкретного случая.

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


Для нетерминальных операций не нужны бизнес-правила, надо тащить rational без потерь — вплоть до терминальной операции. И вот тут уже применять бизнес-правила. Это, в принципе, было русским языком написано в том комментарии, на который вы отвечаете.

Т.е. вы реализуете поведение JS, а потом говорите — смотрите, оно ведет себя как JS. Однако.

Перегрузка операторов — это фича, которая используется редко, но метко, для всяких векторных и матричных операций. В JavaScript же подобное поведение получается по-умолчанию.

Нога, конечно, прострелена, но достаточно ли это изящно сделано?
Нет, конечно. Ибо подражание. Для изящных результатов надо пользоваться собственными особенностями языка. Например, механическим преобразованием linq в вызовы методов с последующей попыткой всё это дело скомпилировать. Тогда в C# «Hello, World!» можно написать так:
class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(from whatever in new Trickster()
                          orderby null
                          select null);
    }
}

class Trickster
{
    public string Select(Func<object, object> obj) => "Hello, World!";
    public Trickster OrderBy(Func<Trickster, object> obj) => new Trickster();
}

Или даже так:

using System;

class Program
{
    static void Main()
    {
        Console.WriteLine(
            from @default in (object)default
            join _ in (object)default
            on default equals default
            where default
            group default
            by default
            into @default
            orderby default
            select default);
    }
}

static class Extensions
{
    public static object Join(
        this object outer,
        object inner,
        Func<object, object> outerKeySelector,
        Func<object, object> innerKeySelector,
        Func<object, object, object> resultSelector)
        => default;

    public static object Where(
        this object source,
        Func<object, bool> predicate)
        => default;

    public static object GroupBy(
        this object source,
        Func<object, object> keySelector,
        Func<object, object> elementSelector)
        => default;

    public static object OrderBy(
        this object source,
        Func<object, object> keySelector)
        => default;

    public static string Select(
        this object source,
        Func<object, object> selector)
        => "Hello, World!";
}

В JavaScript можно выполнять математические операции совместно над строками и числами.
В C# так можно только со сложением.

Это не совсем корректное заявление. В С# в данном случае это не математическая операция, а конкатенация этой самой строки и «выполнения ToString() на числе». Просто если я не ошибаюсь, то с какой-то там версии языка в такой ситуации не нужно вызывать этот самый ToString() самому и это делается за вас. И результатом у вас всегда будет объект типа string.

И самое главное я бы хотел посмотреть как вы в С# выполните вот такое:
var str = "42";
var i = 1;
i = str + i;
var в шарпе просто сахар, думаю вы все же имели виду dynamic

image
Нет, я имел ввиду именно var. Потому что dynamic мало кто использует в С# просто так. То есть если кто-то использует dynamic, то он обычно проверяет что он там получает в результате. И даже если нет, то значение dynamic переменной просто так не присвоить переменной имеющей тип int.

И проблема на мой взгляд не в том что в С#/JS можно «выполнять математические операции на строке и числе», а в том что в JS вы можете случайно/незаметно выполнить конкатенацию вместо сложения и потом записать string в переменную, которая до этого была числом или от которой ожидается что она является числом.

И единственный способ быть уверенным что этого не произойдёт это проверять являются ли все ваши переменные/параметры числами и не «заползла» ли где-то случайно строка. А это по хорошему куча работы. Особенно если много работаешь с UI и/или сторонними библиотеками.
При этом числа имеют тип Number и хранятся в 64 битах в соответствии со стандартом IEEE 754. В .NET у этого типа есть брат-близнец System.Double или просто double.

Нет, Number не брат близнец System.Double, он может вести себя и как целое число int или long и как double. Пока число целое и помещается в 32 бит, оно будет вести себя как int, но любая операция с числом с плавающей точкой превратит его в double. Я не помню со скольки бит (можно почитать в спецификации) но int превращается в long и остается long даже при операциях с double
9007199254740891 + 0.4 // 9007199254740891
9007199254740891 + 0.5 // 9007199254740892
Ничто ни во что не превращается! Просто у Number (aka IEEE 754 double-precision aka binary64) хватает точности без ошибок представлять любое целое число вплоть до Number.MAX_SAFE_INTEGER (9007199254740991 = 2^53 - 1). В С# поведение double идентично.
Что с пруфами? Как ведет себя js я написал выше, как c# dotnetfiddle.net/CguG53
Console.WriteLine((9007199254740891+3.0).ToString("0")); // 9007199254740890
Вот так ему точности хватает.
ps. вот еще про точность long и double dotnetfiddle.net/eRwq64
Попробуйте собрать этот-же код под x86. ;)
Разница не в точности double, а в точности выполнения «внутренних»/«промежуточных» арифметических операций внутри процессора. Это поведение платформо-зависимо: на x86 используется x87 FPU, на x64 — SSE2.

Обратите внимание, для хранения значения точности double хватает. dotnetfiddle.net/rVcV1U

Занятный пример, непонятно почему если в double есть точное значение, почему оно округляется при печати.
Попробуйте doubleValue.ToString("G17") ;)
In some cases, Double values formatted with the «R» standard numeric format string do not successfully round-trip if compiled using the /platform:x64 or /platform:anycpu switches and run on 64-bit systems. To work around this problem, you can format Double values by using the «G17» standard numeric format string. The following example uses the «R» format string with a Double value that does not round-trip successfully, and also uses the «G17» format string to successfully round-trip the original value.
www.ecma-international.org/ecma-262/5.1/#sec-4.3.19
4.3.19 Number value
primitive value corresponding to a double-precision 64-bit binary format IEEE 754 value

NOTE: A Number value is a member of the Number type and is a direct representation of a number.

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

Ваш пример показывает только то, что у типа double есть ограничение на количество значащих символов. Точно такое же поведение будет и в питоне, и в плюсах. Значения типа long тут совсем ни при чем.
Оно «крякает» как long, в данной статье речь о c# и js, я относительно недавно переносил логику из c# в js и столкнулся с разным поведением на больших числах. Тот же самый код, может вернуть разные числа. При передаче по сети, если они сериализуются в стринг, то в js это будет long, в c# это будет double. Это надо учитывать.
Операторы в JavaScript иногда не отличаются коммутативностью.

Date() && {property: 1}; // {property: 1}
{property: 1} && Date(); // Uncaught SyntaxError: Unexpected token '&&'
Дело не в коммутативности оператора, просто во втором примере фигурные скобки в начале строки будут расцениваться как объявление блока, а не объекта. Отсюда возникает синтаксическая ошибка. Чтобы исключить неоднозначность можно добавить пару круглых скобок:
({property: 1}) && Date(); // Fri Jul 17 2020 ...
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации