Не секрет, что финансовая информация (счета, проводки и прочая бухгалтерия) не очень дружит с числами с плавающей точкой, и множество статей рекомендует использовать фиксированную точку (fixed point arithmetic). В Java этот формат представлен, по сути, только классом BigDecimal, который не всегда можно использовать по соображениям производительности. Приходится искать альтернативы. Эта статья описывает самописную Java библиотеку для выполнения арифметических операций над числами с фиксированной точностью. Библиотека была создана для работы в высокопроизводительных финансовых приложениях и позволяет работать с точностью до 9 знаков после запятой при сохранении приемлемой производительности. Ссылка на исходники и бенчмарки приведены в конце статьи.


Арифметика с плавающей точкой


Cовременные компьютеры могут выполнять арифметические операции только с ограниченной точностью. Это дискретные устройства, которые могут работать не со всеми возможными числами, а только с некоторым их счетным подмножеством. Самым распространённым форматом работы с вещественными числами в памяти компьютера является плавающая (двоичная) точка — floating (binary) point, когда числа хранятся в виде M*2^E, где M и E — целые мантисса и порядок числа. Но некоторые числа, например 0.1, невозможно точно представить в этом формате. Поэтому в ходе сложных вычислений неизбежно накапливается некоторая ошибка. То есть результат машинного вычисления, скажем 0.1 + 0.1 + 0.1, не совпадает с математически правильным 0.3. Учитывая вышесказанное, при программировании сложной арифметики можно придерживаться нескольких стратегий:


Стратегия 1 — игнорировать. Не обращать внимания на погрешность, считать все операции идеально-математическими и надеятся, что имеющейся точности хватит для приемлемых результатов. Самый распространённый вариант.


Стратегия 2 — скрупулёзно подсчитать. Формулы для подсчета машинных погрешностей известны не одно десятилетие. Они позволяют оценить сверху относительную погрешность любой арифметической операции. Наверное, так и приходится делать для серьёзного численного моделирования. Проблема в том, что это очень трудоемко. По сути, каждый символ + — * / в коде должен сопровождаться вычислением погрешности. Нужно учесть все зависимости между вычислениями и повторять процедуру каждый раз при изменении кода.


Стратегия 3 — использовать десятичную точку (floating decimal point) вместо двоичной. То есть хранить числа в виде M*10^E. Это не решает проблем с погрешностью (мантисса по-прежнему округляется до конечного числа значащих цифр), но по крайней мере все «простые» для человека числа (вроде 1.1) теперь представлены в памяти точно. Расплатой будет производительность. Любая нормализация чисел (то есть эквивалентное уменьшение мантиссы и увеличение порядка) требует деления на степень 10, что очень не быстро, в отличие от деления на степень 2. А нормализовывать приходится много — при каждом сложении или вычитании с разными порядками.


Стратегия 4 — использовать фиксированную точку (fixed decimal point). Упрощение стратегии 3, когда мы фиксируем порядок E. В этом случае для сложения/вычитания не нужна нормализация. Кроме того, все вычисления будут иметь одинаковую абсолютную погрешность. Именно этой стратегии посвящена статья.


Арифметика с фиксированной точкой


В отличие от физики, где важна относительная погрешность, в финансах нужна как раз абсолютная. Если после проведения сложной финансовой транзакции клиенту выставить счёт в $1000000.23 в то время как он ожидает $1000000.18, то могут возникнуть некоторые трудности. Объяснения типа «да зачем вам точность в 8 значащих цифр??» могут не прокатить. И дело тут не в 5 центах убытка (ошибиться наоборот, «в пользу» клиента, не сильно лучше), а в нестыковках бухгалтерского учёта. Поэтому правила вычислений и округлений четко оговариваются между сторонами, и артефакты от использования double и float переменных порой усложняют жизнь.


В Java есть стандартный класс для fixed point арифметики — BigDecimal. Проблемы с ним две: он медленный (из-за своей универсальности) и он немутабельный. Немутабельность означает что любая операция выделяет объект в куче. Выделение и освобождение в пересчете на объект занимает немного времени, но интенсивные вычисления в «горячем» коде создают приличную нагрузку на GC, неприемлемую в некоторых случаях. Можно понадеяться на escape-analysis и скаляризацию, но они очень нестабильны в том смысле, что даже незначительное изменение в коде или в JIT (типа ленивой загрузки новой реализации интерфейса) может перевернуть вверх ногами всю структуру инлайна, и метод, минуту назад нормально работавший, вдруг начнёт бешено выделять память.
UPD из-за вопросов в комментариях: Основная причина отказа от BigDecimal и BigInteger — вовсе не низкая производительность вычислений, а немутабельность и выделение объектов.


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


Сразу покажу пример использования, прежде чем переходить к деталям реализации:


public class Sample {
    private final Decimal margin;
    private final Quantity cumQuantity = new Quantity();
    private final Quantity contraQuantity = new Quantity();
    private final Quantity cumContraQuantity = new Quantity();
    private final Price priceWithMargin = new Price();
    private final Price avgPrice = new Price();

    public Sample(int marginBp) {
        // 1 + margin / 10000
        this.margin = Decimal.create(marginBp).divRD(10000L).add(1);
    }

    public Price calculateAvgPrice(Quantity[] quantities, Price[] prices) {
        cumQuantity.set(0);
        contraQuantity.set(0);

        // avg = sum(q * p * margin) / sum(q)
        for (int i = 0; i < quantities.length; i++) {
            cumQuantity.add(quantities[i]);
            priceWithMargin.set(prices[i]).mulRD(margin);
            contraQuantity.set(quantities[i]).mulRD(priceWithMargin);
            cumContraQuantity.add(contraQuantity);
        }

        return avgPrice.quotientRD(cumContraQuantity, cumQuantity);
    }

    public static void main(String[] args) throws ParseException {
        Price p1 = Price.create("1.5");
        Price p2 = Price.create(1.6);

        Quantity q1 = Quantity.create("100");
        Quantity q2 = Quantity.create(200);

        // apply 0.05% margin to the prices
        Sample sample = new Sample(5); 
        System.out.println(sample.calculateAvgPrice(new Quantity[]{q1, q2}, new Price[]{p1, p2}));
    }
}

Идея реализации


Итак, нам нужна мутабельная обертка целочисленного примитива, если точнее — long’а, который даст нам почти 19 значащих цифр (хватит и на целую и на дробную часть). В long'е мы подразумеваем N десятичных знаков после запятой. Например, при N=2, число 2.56 хранится как 256 (двоичное 100000000). Отрицательные числа хранятся стандартно, в дополнительном коде:


-2.56
-256


(Здесь и далее курсивом обозначены «математические» числа и вычисления, а жирным – их внутреннее представление)


Также мне показалось полезным ввести NaN отдельным значением, которое возвращается в случае арифметических ошибок (вместо исключения или мусора). NaN представлен внутри как Long.MIN_VALUE, «распространяется» (propagated) через все операции и позволяет определить инвертирование знака для всех оставшихся чисел.


Попробуем прикинуть алгоритмы арифметических операций для случая, когда N=2.


Сложение и вычитание не требуют никаких лишних телодвижений, просто используем значения как есть:


1.20 + 2.30 = 3.50
120 + 230 = 350


Умножение и деление требуют дополнительной нормализации, то есть умножения/деления на 10^N (на 100 в нашем примере)


1.20 * 2.00 = 2.40
120 * 200 / 100 = 240


1.20 / 2.00 = 0.60
100 * 120 / 200 = 60


Дополнительное деление — не самая быстрая операция. Но в данном случае это деление на константу, ведь мы заранее зафиксировали N=2 и 10^N=100. Деление на константу, особенно на «красивую» (типа 10), интенсивно оптимизируется в CPU и сильно быстрее деления на случайное число. Мы делаем кучу делений на 10 каждый раз, когда преобразовываем любое число в строку (например в логах), и производители CPU об этом знают (подробнее про оптимизации см "Division by a constant").


Для закрепления понимания того, что мы делаем, приведу ещё одну операцию: унарное обращение числа, то есть 1/х. Это частный случай деления, нужно просто представить 1.00 в нашем формате и не забыть нормализовать:


1.00 / 2.00 = 0.50
100 * 100 / 200 = 50


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


Округление


Попробуем обратить другое число:


1.00 / 3.00 = 0.33
100 * 100 / 300 = 33


Честный математический результат лежит между 0.33 и 0.34, но мы не можем его точно представить. В какую сторону округлять? Обычно округляют к 0, и это самый быстрый способ (поддерживается аппаратно). Но, возвращаясь к реальным финансовым задачам, это не всегда так. Обычно при обработке транзакций с клиентом округление идёт «в пользу клиента». То есть цена округляется вверх, если клиент продаёт, и вниз, если клиент покупает. Но могут потребоваться и другие варианты, например арифметическое округление к ближайшему числу с подтипами (half-up, half-down, half-even) для минимизации бухгалтерских нестыковок. Или округление к ±бесконечности для отрицательных цен (у некоторых финансовых инструментов). Java BigDecimal уже содержит список стандартных режимов округления, и описываемая библиотека их все поддерживает. Режим UNNECESSARY возвращает NaN, если операция неожиданно потребует округления.


В режиме округления вверх наше вычисление должно давать:


1.00 / 3.00 = 0.34
100 * 100 / 300 + 1 = 34


Как узнать, что нужно добавить единицу? Нужен остаток от деления 10000 % 300 = 100. Который такой же медленный, как и само деление. К счастью, если написать подряд в коде "a/b; a%b", то JIT сообразит что 2 деления не нужно, достаточно одной ассебмлерной команды div возвращающей 2 числа (частное и остаток).


Другие варианты округления чуть сложнее, но тоже могут быть вычислены на основе остатка и делителя.


В API я намеренно сделал упоминание округления везде, где оно происходит, либо в виде параметра, либо в виде суффикса RoundDown в методах, где оно по умолчанию происходит к нулю.


Переполнение


Мы подходим к самой сложной части. Вспомним ещё раз наше умножение:


1.20 * 2.00 = 2.40
120 * 200 / 100 = 240


Теперь представим, что мы в 1980-х и процессоры у нас 16-битные. То есть нам доступен только short с максимальным значением 65535. Первое умножение переполнится и будет равно 240000 & 0xFFFF = 44392 (это если без знака, со знаком оно будет ещё и отрицательным), что поломает нам результат.


Так не пойдёт. У нас 2 нормальных (влезающих в наш диапазон значений) аргумента, и такой же нормальный ожидаемый результат, но мы переполняемся на полдороге. Точно такая же ситуация возможна и с 64-битным long’ом, просто числа нужны побольше.


В 1980-х нам потребовалось бы умножение, дающее 32-битный результат. Сегодня нам требуется умножение с 128-битным результатом. Самое обидное то, что оба умножения доступны в ассемблерах 8086 и x86-64 соответственно, но мы не можем использовать их из Java! JNI, даже в случае хака с быстрым JavaCritical, даёт оверхед в десятки наносекунд, привносит сложности с деплоем и совместимостью, замораживает GC на время вызова. К тому же нам каким-то образом пришлось бы возвращать 128-битный результат из native метода, а запись по ссылке в массив (в память) — это дополнительная задержка.


В общем пришлось мне писать ручное умножение и деление. Столбиком. Мне требовались 2 вспомогательные операции:


  1. A(64) * B(64) = T(128); T(128) / N(32)= Q(64),R(32) — как часть fixed point умножения A*B
  2. N(32) * A(64) = T(96); T(96) / B(64) = Q(64),R(64) — как часть fixed point деления A/B
    (в скобках указана размерность данных в битах, T — временная переменная, которая не должна переполняться)

Обе операции возвращают частное и остаток (одно – как результат метода, второе — в поле объекта). Они тоже могут переполняться, но только на последнем шаге, когда это неизбежно. Вот пример (из 1980-х):


500.00 / 0.50 = 1000.00
100 * 50000 / 50 = 100000 — переполнение!


Деление столбиком а-ля Кнут — не самый простой алгоритм. Плюс это все должно быть ещё и относительно быстрым. Поэтому код обоих операций — сотни строк достаточно суровой битовой магии, у меня самого уйдет много времени, чтобы снова вспомнить что конкретно там происходит. Я вытащил их в отдельный класс и откомментировал подробно как мог.


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


Обычно (за исключением особых случаев), обе операции содержат 4 умножения и 2 деления. Операция 1 существенно быстрее 2, так как в ней эти деления — на константу.


Кстати, если кто заметил, N(32) — это наша 10^N для нормализации. Она 32-битная, из чего следует, что N может быть максимум 9. В реальных виденных мной приложениях использовалось 2, 4 или 8 знаков после запятой. Больше 9 я не встречал, так что должно хватить. Если делать 10^N 64-битным, код усложняется (и замедляется) ещё сильнее.


Несколько разных точностей


Иногда необходимо выполнить операцию над аргументами с разным количеством знаков после запятой. Как минимум – ввести операции с участием обычного long.


К примеру:


2.0000(N=4) + 3.00(N=2) = 5.0000(N=4)
20000 + 300 * 100 = 50000


3.00 (N=2) + 2.0000(N=4) = 5.00(N=2)
300 + 20000 / 100 = 500


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


Количество знаков после запятой НЕ хранится в объекте. Вместо этого, предполагается наличие отдельного подкласса для каждой точности. Имена классов могут быть бизнес-ориентированными, например Price (N=8), Quantity (N=2). А могут быть обобщенными: Decimal1, Decimal2, Decimal3,… Чем больше точность, тем меньше диапазон хранимых значений, минимальный диапазон имеет Decimal9: ±9223372036. Предполагается, что одного-двух классов будет достаточно, чтобы покрыть необходимую функциональность, и в этом случае абстрактный метод getScale скорее всего будет девиртуализирован и заинлайнен. Подклассы (вместо дополнительного поля) позволяют строго типизировать точность аргументов и результата, а также сигнализировать о возможном округлении на этапе компиляции.


Библиотека позволяет производить операции в которых участвуют максимум 2 (но не 3) разных точности. То есть должны совпадать либо точности двух аргументов, либо точность одного из аргументов и результата. Опять-таки, поддержка 3-х разных точностей сильно замедлила бы код и усложнила бы API. В качестве аргументов можно передавать обычный long, для которого предполагается точность N=0.


2.0000 / 3.0 = 0.6667 — ok (2 разных точности)
2 / 3= 0.6667 — ok (long аргументы, decimal результат)
2 / 3.0 = 0.6667 — невозможно! (3 разных точности)


Достоинства и недостатки


Очевидно, вычисления повышенной разрядности, проводимые библиотекой, медленнее аппаратно поддерживаемых. Тем не менее, оверхед не настолько велик (см бенчмарки ниже).


Кроме того, из-за отсутствия перегрузки операторов в Java, использование методов вместо арифметических операторов усложняет восприятие кода.


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


Сложные математические алгоритмы (моделирование, статистика, прогнозирование) обычно проще проводить стандартно в double, так как их результат в любом случае не абсолютно точен.


Код и бенчмарки


Код


Benchmark Mode Cnt Score Error Units
DecimalBenchmark.control avgt 200 10.072 ± 0.074 ns/op
DecimalBenchmark.multiplyNative avgt 200 10.625 ± 0.142 ns/op
DecimalBenchmark.multiplyMyDecimal avgt 200 35.840 ± 0.121 ns/op
DecimalBenchmark.multiplyBigDecimal avgt 200 126.098 ± 0.408 ns/op
DecimalBenchmark.quotientNative avgt 200 70.728 ± 0.230 ns/op
DecimalBenchmark.quotientMyDecimal avgt 200 138.581 ± 7.102 ns/op
DecimalBenchmark.quotientBigDecimal avgt 200 179.650 ± 0.849 ns/op

В целом, умножение получается в 4 раза быстрее BigDecimal, деление — в 1.5. Скорость деления сильно зависит от аргументов, отсюда такой разброс значений.

Комментарии (42)


  1. Sultansoy
    06.10.2018 20:21

    Отличная работа. Я тоже думал реализовать это именно таким же способом, но в итоге никак не дошли руки. Добавил в избранное, буду смотреть код и пул реквестить. Спасибо за реально полезную тулу. А то в энтерпрайз языке отсутствие нормальной реализации чисел с фикс точкой это ад. Хотя я помню, когда занимался бух задачами, находил какую-то библиотеку.


    1. tmaxx Автор
      07.10.2018 00:18

      Спасибо. Улучшения, а особенно багфиксы всячески приветствуются


    1. mspain
      07.10.2018 06:26

      в смысле «нет нормальной реализации»? а упомянутый Biginteger куда делся? а вот ад (точнее трэш) это велосипед, который работает с финансами, при этом ради 1,5 кратного выигрыша


      1. tmaxx Автор
        07.10.2018 11:56
        -1

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


    1. TerraV
      07.10.2018 13:05

      Это реально вредная тулза. Аргументы против BigDecimal в высшей степени спорны. А подобная кастомщина в проекта начинает резко задирать Total cost of ownership. Потому что поведение библиотеки очевидно только самому автору и то пока не забыл. Приходит новый человек на проект (автор разумеется уже уволился) и видит этот треш. Вы реально думаете что словив багу он будет исправлять либу? Причем по уровню исполнения, эта библиотека написана в лучшем случае мидлом.


      1. APXEOLOG
        07.10.2018 23:06
        +1

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

        А какие косяки в исполнении вы видите?


        1. TerraV
          08.10.2018 11:27
          -1

          public static void main в Decimal? Locale dependent исполнение? Отсутствие форматтера/парсера? NaN, причем только отрицательный, про то что NaN может быть как положительный так и отрицательный не слышали. Константный scale? Open-Close principle violation? Подобный трешачок return a == NaN || b == NaN || (result < 0) != (a < 0) && (result < 0) != (b < 0)? NaN: result;

          И самое главное — плохое API (можете сказать что делают product и quotient не глядя в исходники?) и мутабельность. От мутабельности уходят не просто так, а потому что это error-prone подход.


          1. mayorovp
            08.10.2018 11:30
            -1

            Кажется, автор уже несколько раз сказал что именно ради мутабельности все и делалось…


            1. TerraV
              08.10.2018 11:31

              От того что глупость повторять много раз она не становится мудростью.

              P.S. Искренне желаю вам использовать эту библиотеку в своих проектах.


          1. mpa4b
            08.10.2018 14:14

            Вы NaN с бесконечностями не попутали? Те бывают положительные и отрицательные, а NaN — это not a number.


            1. TerraV
              08.10.2018 14:37
              +1

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


      1. tmaxx Автор
        08.10.2018 21:35

        Мне очень жаль, что вас огорчило нарушение Open-closed principle в библиотеке из 2 классов без планов расширения.


        Ваши замечания насчёт getScale и ArithmeticException противоречат требованиям, из-за которых проект был создан. У меня сложилось впечатление, что вы эти требования не понимаете.


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


        Вашу оценку моей квалификации оставлю без комментариев.


  1. UnrealQW
    06.10.2018 21:52

    https://ru.wikipedia.org/wiki/Десятичный_разделитель:

    В англоязычных странах в качестве десятичного разделителя используется точка (.), в большинстве остальных — запятая (,).

    Статью делали вы или это перевод?


    1. tmaxx Автор
      07.10.2018 00:24
      +5

      Это не перевод. Я понимаю, что «запятая» по-русски правильнее. Но во большинстве языков программирования мы используем точку. И я решил, что если буду использовать в одной статье «floating point», «плавающая запятая», «1.23» (в коде) и «1,23» (не в коде), то я запутаюсь сам и запутаю остальных. Я думаю, что ИТ-сообщество вполне привыкло к англицизмам в русских технических текстах, в том числе и к «точке».


  1. resetme
    06.10.2018 22:24
    +1

    А почему бы не считать все в копейках? Или есть проблемы у такого решения?


    1. tmaxx Автор
      07.10.2018 00:27

      По-сути так и делается для N=2. Но иногда требуется делить копейки на какие-нибудь центы (чтобы получить курс обмена) и результат должен содержать больше 2 знаков после запятой. Класс просто позволяет работать с разными точностями.


  1. nikitasius
    06.10.2018 22:25
    +1

    Интересная вещь. Для себя решил остаться на BigDecimal в виду неудачного гугляжа альтернатив в свое время. Использую там, где точность до 12 знаков (крипта), радует в нем то, что можно и более высокую точность использовать.


    Конечно,


    В целом, умножение получается в 4 раза быстрее BigDecimal, деление — в 1.5

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


    1. tmaxx Автор
      07.10.2018 00:30
      +1

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


  1. Sap_ru
    07.10.2018 01:36
    +4

    Есть мнение, что такие библиотеки вовсе так просты, как кажется и нужно понимать, что подобные велосипеды имеют очень ограниченное применение со множеством побочных эффектов. Не дай Бог расслабиться и забыть об этом (раз проект сделал, два сделал — работе же!). А потом через год окажется, что программа тупо неправильно считает деньги во многих случаях.
    Вы уверены, что ваша библиотека корректно будет работать во всех случаях умножения (и в каким именно случаях)?
    У вас же можно умножать только числа с порядком не больше половины максимального, да ещё и с учётом множителя дробной части.
    Т.е. 1'000'000'000.0000*1'000'000'000.0000 приведёт к катастрофе. И, что хуже, при таком подходе к перемножению чисел разной точности будут неочевидные побочные эффекты. Например, 10'000'000'000.0000*10.000000 (второе число с большим числом знаков после запятой) тоже приведёт к катастрофе. Вы можете тупо единицу на единицу при большом количество занком после запятой умножить и получить неконтролируемое переполнение. По-моему, так делать нельзя. Нужно, как минимум, ловить потенциальные перполнения по точности и значению аргументов и обрабатывать пограничные случаи отдельно.
    А ещё есть, как минимум, операцииквадратного корня и возведения в степень, которые даже в бухгалтерии используются — они у вас при таком подходе вообще неправильно вычисляться будут.
    И про опитимизацию деления и взятия остатка — вы точно в этом уверены (что будет сгенерирована одна операция деления)?


    1. tmaxx Автор
      07.10.2018 11:47
      +2

      умножать только числа с порядком не больше половины максимального

      Нет, умножать/делить/etc можно любые числа при условии что результат вместе с дробной частью влезает в long. Если не влезает, возвращается NaN, который легко проверить. Напомню, Java в случае переполнения типа Long.MAX_VALUE * 10 вообще возвращает мусор.


      Ваш первый пример — переполнение (18 нулей до запятой и 4 после):


      System.out.println(new Decimal4().parse("1000000000.0000")
          .mulRD(new Decimal4().parse("1000000000.0000"))); // "NaN"

      Ваш второй пример работает нормально:


      System.out.println(new Decimal4().parse("10000000000.0000")
          .mulRD(new Decimal6().parse("10.000000"))); // 100000000000.0000

      Насчет "вы уверены?". 100% гарантию отсутствия багов я дать не могу, как и большиство разработчиков. Но класс достаточно хорошо оттестирован, в том числе и на граничных случаях. Если вы нашли конкретную проблему, нет ничего проще написать падающий юнит-тест и прислать мне, я буду очень признателен. У библиотеки нет зависимостей, можно просто скопировать 2 класса из Гитхаба в любой пакет.


      Я категорически не приветствую написание велосипедов, но моя исходная задача не решалась стандартными средствами, поэтому пришлось писать по-своему.
      Утверждение "любые велосипеды содержат больше багов чем любые не-велосипеды" некорректно.


      1. Sap_ru
        07.10.2018 17:56

        NaN возвращается, это уже хорошо, но пользователю от этого не легче. Он множит два «нормальных» с его точки зрения числа, результат влазит в представление с большим запасом, а на выходе получает NaN. Если не знать о внутренней реализации, то можно гарантировать баги при использовании.
        Под уверенностью я имел в виду умножение отрицательных чисел с переполнением — там NaN правильно обрабатывается?


  1. Anton23
    07.10.2018 07:35

    0.1 * 0.1 * 0.1 = 0.30000000000000004 в Java


    1. barker
      07.10.2018 08:03
      +1

      0.1 * 0.1 * 0.1 = 0.30000000000000004
      У вас очень странная Java.


      1. Anton23
        07.10.2018 08:50

        У вас очень странная Java.

        11 OpenJDK


        1. nfw
          07.10.2018 09:17
          +1

          Скрытый текст


          1. Anton23
            07.10.2018 09:21
            +1

            Мдауж. Sorry, так я еще не фейлился…


  1. maxzh83
    07.10.2018 09:26

    При работе с BigDecimal очень напрягает, что нельзя написать формулу с помощью стандартных операторов (+, * и т.д.). И это очень сильно сказывается на читаемости, с BigDecimal даже самые простые вычисления выглядят монструозно. К сожалению, это особенности Java (не хватает перегрузки операторов) и тут сложно что-то придумать.


    1. TerraV
      07.10.2018 12:55

      Вам в Kotlin. Там сделать нормальные унарные и бинарные операторы для BigDecimal дело пяти минут


      1. fshp
        07.10.2018 14:28

        Или сразу использовать scala.


  1. ssh24
    07.10.2018 10:07
    -2

    Никогда не понимал, в чем тут проблема. Ведь можно хранить все стоимости в копейках. Отилично подходит тип long. И не надо никакого шаманства с округлениями и специальными типами.

    Если надо, например, показать количество рублей, то колиечство копеек делим на сто: value / 100;
    Если нужна копеечная часть стоимости, то просто берем модуль: value % 100;

    Если уж нужна более высокая точность вычислений. Можно взять не 100, а другие порядки, например, 10000.


    1. balexa
      07.10.2018 11:47
      -1

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

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

      А во вторых, за 10+ лет работы над финансовым софтом, я ни разу не видел чтобы производительность арифметики в них была критична. У всех конечно разные кейзы, но тем не менее

      Если уж нужна более высокая точность вычислений. Можно взять не 100, а другие порядки, например, 10000.

      Так по сути целочисленная арифметика это под капотом и делает.


      1. tmaxx Автор
        07.10.2018 11:58

        Можно все-таки пруф насчет «считает неправильно»?
        Один маленький падающий юнит-тест, вместо долгих рассуждений на тему «это не заработает»


      1. ssh24
        07.10.2018 12:55

        Если вашим задачам нужна такая точность. То тогда ИМХО надо брать готовые АПИ, например, JavaMoney, а не пилить велосипед.


        1. tmaxx Автор
          07.10.2018 13:08

          Реализация JavaMoney на Гитхабе (jsr354-ri) использует BigDecimal внутри. Навскидку — в методе divide. Почему мне не подходит BigDecimal я уже писал. Если вы знаете другую реализацию — поделитесь.


  1. balexa
    07.10.2018 11:42

    del


  1. HSerg
    07.10.2018 14:23
    +1

    Далеко не сразу нашёл лицензию проекта — она указана только в паре файлов. Стоит её добавить в readme и корень проекта (чтобы github её в заголовке указывал), pom.xml и во все исходники.


    1. tmaxx Автор
      07.10.2018 16:05

      Ок, добавлю лиценцию (MIT). В главные классы проекта (их ровно 2) я добавил сразу, а вот про тесты, примеры и прочее забыл


  1. DukeKan
    07.10.2018 14:32
    -1

    Обратил внимание, что до примерно 2014 года на хабре можно было получить спасибо и плюсы за такую статью и за многие другие. После — обольют помоями. Здесь просто многие комментирующие самоутверждаются


  1. munrocket
    07.10.2018 15:42

    Чувствуется вдохновление моей статьей, плюсанул :)

    Вам стоило сделать стресс-тест со случайными числами и сверять результат с классом BigDecimal. Или же взять юнит-тесты со свободных либ на других языках, коли уж вы связаны с финансами. Тогда всем «критикам» можно было бы легко парировать.


    1. tmaxx Автор
      07.10.2018 16:06
      +1

      Случайные тесты vs BigDecimal у меня бежали где-то неделю на 2 ядрах (домашняя машина). С юнит-тестами хорошая идея, попробую найти в OpenJDK и позаимствовать.



  1. kefirfromperm
    08.10.2018 17:02

    Есть такая штука docs.oracle.com/en/java/javase/11/docs/api/java.base/java/math/MathContext.html
    и есть процессоры, например IBM Power, которые поддерживают арифметику с десятичной запятой. Процессоры intel тоже поддерживают, но не уверен что она используется в Java. Думаю, всё это не учитывается в вашей библиотеке.