Комментарии 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
?
Откуда? Не нужно пытаться решить сферическую задачу в вакууме, нужно решать существующие задачи максимально хорошо.
Я не занимаюсь финансовой деятельностью, не могу сходу сказать. К тому же приписал "наверное", это значит неточно.
Вы, наверное, думаете, что удачно сострили, а тем не менее мы даем возможность нашим клиентам определять практически любые формулы для срабатывания триггеров на изменении курсов валют.
Я так не думаю — вам показалось.
И Rational тоже допускает потери, по определению, т.к. не имеет бесконечной точности, и к нему также применимо выражение «гарантирует отсустствие потерь, пока не превысится точность», как и к Decimal. В отличие от Double, который дает погрешность уже на примере операции 0.1 + 0.2
Rational тоже допускает потери, по определению.
У вас неверное определение. Рациональные числа Rational
обрабатывает без потерь. Хоть факториал миллиарда туда запихните, если памяти хватит. И делает он это именно что по определению.
Ни в руби, ни в эрланге, ограничений точности не существует. Если в вашем языке существуют — придется реализовывать самому, я ж говорил там уже.
Плюс операции на 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 так. Ну и как бы вот по быстрому нагуглилось:
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
.
Это не требуется. Расчет — что в платежном шлюзе, что в банке — это терминальная операция, там все равно придется округлять до копеек. Внутри же вашей системы далеко не все операции терминальные. Это просто.
Ах, да, их же поголовно писали зеленые юнцы.
Не умеете в сарказм (особенно если не особо понимаете предметную базу) — не нужно и пытаться.
Пример: есть рубль, его нужно разделить на троих вкладчиковЯ хочу получить свою 1/3 на счёт, а не так, что кому-то достанется больше, или остаток уйдёт банку.
Банки оперируют целыми землекопами :)
Будь эти бумажки и монетки сто раз фидуциарны, вы можете в любой момент потребовать закрыть счет. Вам по идее должны будут отсыпать фиата — и все, никто никому ничего не должен. Поэтому (наверное) банки не позволяют даже полкопейки туда-сюда.
Также с обменом валют: суммы считаются до четвертого знака после запятой (для валют с деноминатором 100 — это 0.01 цента), но получите на руки/счет вы округленную в пользу банка сумму.
Финансовые воротилы живут по своим законам, но это не повод в своем приложении накапливать ошибку без всякой на то причины.
Должен быть регламент как правильно считать, невозможно достичь абсолютной точности, вводятся округления, описываются конкретные формулы в соответствующих актах. Например вычисленный НДС от цены и от итоговой суммы в накладной всегда будет разный какой бы тип данных не использовать, поэтому в налоговом кодексе даны конкретные разъяснения по этому поводу, в банковской сфере думаю аналогично.
Как это работает в банке, я в курсе. В смысле, оно в разных банках по-разному работает. Терминальные операции всегда округляются к копейкам, да, в остальных правильно написанный код не должен ничего никуда округлять.
Для нетерминальных операций не нужны бизнес-правила, надо тащить rational без потерь — вплоть до терминальной операции. И вот тут уже применять бизнес-правила. Это, в принципе, было русским языком написано в том комментарии, на который вы отвечаете.
Перегрузка операторов — это фича, которая используется редко, но метко, для всяких векторных и матричных операций. В JavaScript же подобное поведение получается по-умолчанию.
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;
И проблема на мой взгляд не в том что в С#/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
IEEE 754 double-precision
aka binary64
) хватает точности без ошибок представлять любое целое число вплоть до Number.MAX_SAFE_INTEGER (9007199254740991 = 2^53 - 1
). В С# поведение double идентично.Console.WriteLine((9007199254740891+3.0).ToString("0")); // 9007199254740890
Вот так ему точности хватает.
ps. вот еще про точность long и double dotnetfiddle.net/eRwq64
Разница не в точности double, а в точности выполнения «внутренних»/«промежуточных» арифметических операций внутри процессора. Это поведение платформо-зависимо: на x86 используется x87 FPU, на x64 — SSE2.
Обратите внимание, для хранения значения точности double хватает. dotnetfiddle.net/rVcV1U
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.
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 тут совсем ни при чем.
Операторы в JavaScript иногда не отличаются коммутативностью.Дело не в коммутативности оператора, просто во втором примере фигурные скобки в начале строки будут расцениваться как объявление блока, а не объекта. Отсюда возникает синтаксическая ошибка. Чтобы исключить неоднозначность можно добавить пару круглых скобок:
Date() && {property: 1}; // {property: 1} {property: 1} && Date(); // Uncaught SyntaxError: Unexpected token '&&'
({property: 1}) && Date(); // Fri Jul 17 2020 ...
Как С#-разработчик у JavaScript плохому учился