О реализации точного представления чисел или «где хранить деньги?»

    Введение

     «Где хранить деньги?» это шутливое название поста, периодически появляющегося на компьютерных форумах. Имеется в виду вопрос: в переменных какого типа следует хранить результаты вычислений финансово-экономических расчетов. Обычно такой вопрос возникает у программистов, впервые сталкивающихся с подобными расчетами. Интуитивно они понимают, что здесь округления нежелательны или недопустимы. Интересно, что при этом часто вспоминают легенды о хитрых программистах, сумевших разницу между точным и округленным значением финансовых расчетов перечислять на свой собственный счет. Правда, эти истории названы сказками еще в книге [1], написанной 50 лет назад, причем даже там речь шла лишь о ревизиях банковских систем, основаниями для которых (т.е. для ревизий) могли бы служить подобные истории.

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

    Пример необходимости точных вычислений

    Рассмотрим простейший пример: рассчитать выплату 13% с суммы 1 рубль 30 копеек. Это значение легко сосчитать на бумаге «столбиком» и притом точно: 16.9 копейки. Но если запрограммировать этот же расчет с использованием данных типа «double», то получится ответ вроде 1.68999999999999E-001 рубля, поскольку значение 0.169 точно представить в формате IEEE-754 нельзя. Вот тут и начинаются сомнения: не приведет ли ряд подобных вычислений к накоплению погрешности? Определение итоговой погрешности для нетривиальных расчетов само является нетривиальной математической задачей и лучший способ ее решения – не допускать погрешностей вообще. Но как это сделать? И какой тип данных должен быть, если тип «целый» не годится, а тип «float» обязательно даст погрешность?

    Виды представления чисел

    Может показаться, что автор ломится в открытую дверь. Есть же в языках типы для работы с «деньгами», например, тип DECIMAL в языке C#. Как следует из описания языка [2], «Тип значения Decimal подходит для финансовых вычислений, требующих большого количества значимых целых и дробных цифр и отсутствия ошибок округления. Избежать округления с помощью типа Decimal нельзя. Однако он позволяет минимизировать связанные с ним ошибки». Странно, требуется избежать ошибок округления и одновременно утверждается, что этого сделать нельзя.

    Одной из причин затруднений в таких случаях является, на мой взгляд, не совсем четкая классификация представления чисел. Многие привыкли делить числа только на целые и действительные, хотя правильнее было бы вначале разделить их на точные и приближенные. При этом целые – это подмножество точных, а не все множество. В приведенном примере расчета «на бумаге» хотя и получились копейки с долями, т.е. нецелое значение, никакой потери точности не произошло. Тип DECIMAL в языке C# корректнее было бы назвать «модифицированным FLOAT», поскольку он содержит те же знак, мантиссу и порядок. И значения этого типа все же приближенные, хотя и с очень большим числом значащих цифр.

    Аппаратная поддержка точных вычислений

    А ведь в процессорах архитектуры IA-32 имеется набор специальных команд так называемой десятичной и двоично-десятичной арифметики. Они предназначены как раз для проведения точных вычислений. Удивительно, но в 64-разрядном режиме эти команды становятся недоступными, можно предположить, что в фирме Intel разработчики заранее «расчищают» кодовое пространство для новых команд. Но в 32-разрядном режиме такие команды доступны, упрощая вычисления со сколь угодно большой точностью и без округлений. Для проверки автор задавал вопросы коллегам по поводу назначения двоично-десятичной арифметики и убедился, что многие имеют о ней нечеткие представления, например, отвечая, что она нужна только для того, чтобы не переводить число из текста в двоичное и обратно. Такой ответ тоже допустим, поскольку числа действительно представлены не в «обычном» виде. Но все-таки следует еще раз подчеркнуть, что главное назначение нестандартной арифметики – упростить организацию вычислений без потери точности.

    Программная поддержка точных вычислений

    Механизм вычислений без округлений был встроен, например, в язык PL/1. Этот язык изначально разрабатывался для применения и в области экономических расчетов, для чего он получил «наследство» от языка экономических задач Кобола. Можно даже предположить, что команды двоично-десятичной арифметики были вставлены в архитектуру IA-32 (точнее, еще в IA-16 в 1978 году) именно для поддержки в языках высокого уровня типа PL/1 аппарата точных вычислений. Поскольку автор сопровождает и использует транслятор языка PL/1 [3], дальнейшее изложение будет относиться к конкретной реализации. Сама реализация довольна проста и объем команд транслятора, обеспечивающих генерацию точных вычислений, не превышает 3-4% от размера транслятора.

    Представление чисел FIXED DECIMAL

    Граница между представлениями чисел в PL/1 проведена корректно: все числа точного представления имеют тип FIXED, а приближенного представления – FLOAT. К сожалению, сами названия «FIXED/FLOAT» (вместо «ТОЧНОЕ/ПРИБЛИЖЕННОЕ») не вносят ясности и интуитивно непонятны. Как, кстати, и распространенное в языках, но совершенно неинформативное название «double».

    В операторах описания на PL/1 точное представление чисел имеет атрибуты FIXED DECIMAL, после которых указывается общий размер числа и размер дробной части числа, например, FIXED DECIMAL(15,0)или FIXED DECIMAL(12,4) т.е. так можно точно представлять и целые и нецелые значения. Значения можно также представлять и как FIXED BINARY, но далее будет рассматриваться только тип, который может иметь десятичную дробную часть.

    В рассматриваемом трансляторе этот тип реализован в двоично-десятичном упакованном коде BCD (Binary Coded Decimal). Каждая цифра BCD записывается в половину байта. При этом старшая значащая пара цифр BCD размещается по старшему адресу. Самая старшая позиция BCD резервируется для знака, который для положительных чисел - ноль, для отрицательных - девять. Число байт для FIXED DECIMAL определяется заданной «точностью» p (допустимой в стандарте языка от 1 до 15) и равно FLOOR((p+2)/2).

    Например, число 12345 с точностью 5, для которого PL/1 выделит FLOOR((5+2)/2)=3 байта памяти, будет записано в байтах как 45 23 01. Максимально допустимое значение, которое можно записать в таком виде 999 999 999 999 999. Например, чтобы представить точно значение государственного долга США, который,например, на начало 2013 года составлял около $16,400,000,000,000, потребуется переменная типа FIXED DECIMAL(14,0). Правда, если долг вырастет еще раз в 60, представить его точно в формате стандарта PL/1 будет уже нельзя.

    Отрицательное число записывается в дополнительном до 9 коде, который получается вычитанием каждой цифры из 9 и прибавлением к числу единицы. Например, число -2 имеет код (9-2)+1=8 и с точностью 1 будет записано как байт 98, а с точностью 5 будет выглядеть в байтах как 98 99 99.

    Может показаться странным, что в этом представлении нет указания положения десятичной точки. Но ведь и в логарифмической линейке не было указателя точки – приходилось держать ее позицию в уме. В данном случае за положением десятичной точки следит транслятор и явно указывает ее как параметр при вызове подпрограмм преобразований, ввода-вывода и т.п. А при генерации самих вычислений положение точки рассчитывается транслятором так, чтобы не терялась точность, и тоже «держится в уме» пока ее не потребуется использовать. Если вернуться к примеру, при генерации умножения транслятор назначит константе 1.3 атрибуты FIXED DECIMAL(2,1), константе 0.13 - атрибуты FIXED DECIMAL(3,2), а результату (значению 0.169) атрибуты FIXED DECIMAL(6,3), совсем как при умножении «столбиком», когда «ширина» результата складывается из размеров множителей. Правда в данном случае «ширина» должна была бы быть 5, а не 6, но по особенности PL/1 он считает длину результата при точном умножении как p1+p2+1, поскольку стандарт учитывает даже случай умножения комплексных чисел по формуле (a1a2-b1b2)+(a1b2+b1a2)i, где становится возможен еще один перенос разряда.

    Системные вызовы точных вычислений

    Поскольку точные вычисления не могут быть реализованы за одну-две команды, транслятор генерирует множество вызовов системных подпрограмм, обеспечивающих все необходимые действия: арифметику, функции FLOOR/CEIL/TRUNC, сравнение и т.п. Операнды типа FIXED DECIMAL передаются через стек и туда же возвращается результат операции. При этом операнды всегда расширяются до максимальной длины 8 байт и дописываются или нулями или байтами 99 (для отрицательных).

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

    ;══════════════════ ВЫЧИТАНИЕ DECIMAL В СТЕКЕ ═════════════════
    PUBLIC ?DSUOP:                  ;ГЕНЕРИРУЕТСЯ ВЫЗОВ ИЗ PL/1
          LEA       ESI,[ESP]+4     ;НАЧАЛО 1 DECIMAL (УЧЛИ RET)
          MOV       ECX,8           ;МАКСИМАЛЬНАЯ ДЛИНА DECIMAL
          CLC                       ;СБРОС ПЕРЕНОСА
          LEA       EDI,[ESI+ECX]   ;НАЧАЛО 2 DECIMAL
    
    ;---- ЦИКЛ ВЫЧИТАНИЯ ----
    
    M2187:MOV       AL,[EDI]
          SBB       AL,[ESI]        ;ВЫЧИТАНИЕ С КОРРЕКЦИЕЙ
          DAS
          STOSB                     ;ЗАПИСЬ РЕЗУЛЬТАТА
          INC       ESI
          LOOP      M2187
    
    			POP       ECX             ;АДРЕС ВОЗВРАТА
          MOV       ESP,ESI         ;ОЧИСТКА СТЕКА
          JMP       ECX
          
    ;══════════════════ СЛОЖЕНИЕ DECIMAL В СТЕКЕ ══════════════════
    EXTRN ?DOVER:NEAR
    
    PUBLIC ?DADOP:                  ;ГЕНЕРИРУЕТСЯ ВЫЗОВ ИЗ PL/1
          LEA       ESI,[ESP]+4     ;НАЧАЛО 1 DECIMAL (УЧЕТ RET)
          MOV       ECX,8           ;МАКСИМАЛЬНАЯ ДЛИНА DECIMAL
          CLC                       ;СБРОС ПЕРЕНОСА
          LEA       EDI,[ESI+ECX]   ;НАЧАЛО 2 DECIMAL
    
    ;---- ЦИКЛ СЛОЖЕНИЯ ДВУХ DECIMAL В СТЕКЕ ----
    
    M2283:LODSB
    			ADC       AL,[EDI]        ;СЛОЖЕНИЕ С КОРРЕКЦИЕЙ
          DAA
          STOSB                     ;ЗАПИСЬ ОТВЕТА
          LOOP      M2283
    
    ;---- ПРОВЕРКА НА ПЕРЕПОЛНЕНИЕ ----
    
          AND       AL,0F0H         ;ВЫДЕЛИЛИ ПОСЛЕДНЮЮ ЦИФРУ
          JZ        @
          CMP       AL,90H          ;ОТРИЦАТЕЛЬНЫЙ НЕ ПЕРЕПОЛНИЛСЯ ?
          JNZ       ?DOVER          ;OVERFLOW ДЛЯ DECIMAL
       
    ;---- ВЫХОД С ОЧИСТКОЙ СТЕКА ----
    
    @:    POP       ECX             ;АДРЕС ВОЗВРАТА
          MOV       ESP,ESI         ;ОЧИСТКА СТЕКА
          JMP       ECX

    Как следует из этого текста, для двоично-десятичной арифметики используются команды сложения и вычитания с учетом переноса (ADC/SBB) и две специфические команды коррекции результата (DAA/DAS), те самые, которые исключены в 64-разрядном режиме.

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

    Обратите внимание, что нет принципиальных ограничений на длину данных типа FIXED DECIMAL: цикл побайтной обработки пар цифр можно сделать любой длины (а, значит, и точности), хотя в соответствии со стандартом языка PL/1 под каждый операнд при вычислениях выделяется 8 байт и эта максимальная длина записана как константа. Если в трансляторе изменить предел размера типа, например с 15 до 31, а в системных подпрограммах заменить константу 8 на 16, то переменные автоматически станут получать длину до 16 байт и обрабатываться с точностью до 31 десятичного разряда теми же самыми подпрограммами. Скорость обработки при этом уменьшится.

    Выполнение точных вычислений в PL/1

    Вернемся к простейшему примеру точных вычислений, теперь на языке PL/1, добавив деление на ту же константу, чтобы вернуть исходное значение переменной. Вычисления проведем и для типа FIXED DECIMAL и для типа FLOAT(53) – аналога типа «double» в других языках.

    test:proc main;
    
    dcl x fixed decimal(6,3);
    x=1.3;
    x=x*0.13;
    put skip data(x);
    x=x/0.13;
    put skip data(x);
    
    dcl y float(53);
    y=1.3;
    y=y*0.13;
    put skip data(y);
    y=y/0.13;
    put skip data(y);
    
    end test;

    Результат вычислений представлен на рисунке.

    вычисления с точным и приближенным значениями
    вычисления с точным и приближенным значениями

    Последовательное умножение и деление на одно и то же число для типа FIXED DECIMAL вернуло точное исходное значение переменной, а для типа FLOAT - нет. Хотя, если, например, сначала разделить на три переменную типа FIXED DECIMAL, а затем умножить на три, может появиться вынужденная погрешность, обусловленная невозможностью точно представить рациональное число «одна треть» десятичной дробью.

    Таким образом, аппарат точных вычислений, встроенный в язык PL/1, достаточно прост, компактен, легко реализуется в системной библиотеке и всегда готов к применению. Но иногда такие возможности играют злую шутку с программистами, ранее имевших дело только с целыми и действительными числами и даже не подозревающими о наличии в языке точных вычислений. Например, если в программе на PL/1 в числовой константе нет показателя степени E и при этом явно не указано на преобразование в тип FLOAT, то по умолчанию считается, что требуется точное представление и точные вычисления. Поэтому, например, выражение 25.0+1/3 даст исключение переполнения при выполнении, поскольку транслятор разместит рациональное число 1/3 с максимальной точностью (т.е. займет все допустимые 15 десятичных разрядов) и потом добавить еще два разряда целой части результата сложения будет уже «некуда». В то же время выражение 25.0+1Е0/3 будет вычислено приближенно и никакого переполнения не произойдет.

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

    Заключение

    «Хранить деньги», т.е. проводить расчеты, избегая округлений, лучше всего на языках, специально разработанных для решения экономических задач типа Кобола или PL/1 или «Бухгалтерия 1С». Однако как показано выше, реализация средств точных вычислений достаточно проста и практически на любом языке можно самостоятельно создать свой аппарат такой обработки. Например, в книге [4] приводится пример организации вычислений со «сверхточностью» и в «сверхдиапазоне». Процессоры архитектуры IA-32 даже имеют специальные команды, облегчающие арифметические вычисления без округлений. Правда в 64-разрядном режиме используемая для этой цели пара команд DAA/DAS становится недоступной, однако это не сильно затруднит реализацию, поскольку такие команды несложно эмулировать программно, например:

    ;--------------- ЭМУЛЯЦИЯ DAA, ЗАПРЕЩЕННОЙ В РЕЖИМЕ X86-64 ------------------
    
    PUBLIC DAA_X86_64:
    
          PUSH      RDX,RAX
          LAHF
          MOV       EDX,EAX         ;OLD CF И OLD AL
          AND       AH,NOT 1B       ;СБРОСИЛИ CF
    
    ;---- ОБРАБОТКА МЛАДШЕЙ ТЕТРАДЫ ----
    
          TEST      AH,10000B       ;ЕСЛИ ЕСТЬ AF
          JNZ       @
          PUSH      RAX
          AND       AL,0FH
          CMP       AL,9            ;ИЛИ ЕСЛИ ЦИФРА БОЛЬШЕ 9
          POP       RAX
          JBE       M2270
    @:    ADD       AL,6            ;КОРРЕКЦИЯ ЦИФРЫ
          OR        AH,10000B       ;УСТАНАВЛИВАЕМ AF
    
    ;---- ОБРАБОТКА СТАРШЕЙ ТЕТРАДЫ ----
    
    M2270:TEST      DH,1B           ;ЕСЛИ СТОЯЛ OLD CF
          JNZ       @
          CMP       DL,99H          ;ИЛИ НУЖЕН ПЕРЕНОС
          JBE       M2271
    @:    OR        AH,1B           ;УСТАНАВЛИВАЕМ CF
          ADD       AL,60H          ;КОРРЕКЦИЯ ТЕТРАДЫ
    
    ;---- ПИШЕМ ГОТОВЫЙ БАЙТ И ВОССТАНАВЛИВАЕМ РЕГИСТРЫ И ФЛАГИ ----
    
    M2271:SAHF
          MOV       [RSP],AL
          POP       RAX,RDX
          RET

    Основой точных вычислений является точное представление чисел в памяти компьютера. Двоично-десятичное представление позволяет легко организовать точные вычисления для чисел любой длины, т.е. любой точности. Проверено на практике, что для большинства экономических расчетов вполне хватает длины в 15 десятичных разрядов.

    Литература

    1.       Д.Р. Джадд «Работа с файлами». Издательство «Мир» Москва, 1975

    2.       http://msdn.microsoft.com/ru-ru/library/system.decimal.aspx

    3.       Д. Караваев «К вопросу о совершенствовании языка программирования». RSDN Magazine #4 2011

    4.       Л.Ф. Штернберг «Ошибки программирования и приемы работы на языке ПЛ/1», Москва «Машиностроение», 1993

    Реклама
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее

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

      +1
      PL/1 сегодня (специально глянул на календарь и дату публикации), серьезно?

      Почти все денежные системы пришли к десятичным дробным частям (копейка = 1/100 рубля и т.п.)

      При таком подходе любой INT в условных копейках — самое элементарное решение которое придется только минимально подтьюнить при вычислении процентов и делении.

      Ну а в том же ORACLE и других SQL базах данных есть специальные типы под деньги. Ну собственно базы данных они как правило и хранят «деньги». А часто еще и обрабатывают.
        0
        придется только минимально подтьюнить при вычислении процентов и делении.

        А вот и не минимально. Пример — инвестиции толпы с выплатой процентов. В случае лишних денег — проблемы с законом и репутацией.


        Начать изучение можно с парадокса Алабамы.


        А ещё есть Биткойн.

          +2
          инвестиции толпы с выплатой процентов

          Выплата процентов подлежит бухгалтерскому учету. Бухучет всегда оперирует минимальным квантов денег в соответствующей национальной валюте, т. е. в современной России — 1 копейка (много-много лет назад это была ¼ копейки). Следовательно, любая выплата процентов должна быть округлена до 1 копейки.
            0
            В расчетах копеек участвуют проценты, с точностью до… (подставить значение по вкусу) знака после запятой.
            Эти проценты нужно хранить в бухгалтерском учете, наряду с копейками, также без потери точности.
              +1
              В бухучете процены не хранятся. Бухучет — это учет транзакций с денежной оценкой имущества. Проценты будут храниться в первичной документации, на основе которой происходят изменения имущества. Там их можно хранить как угодно, главное, чтобы они с течением времени самопроизвольно не менялись.
          +2
          С PL/1 не сталкивался, сейчас много пишу на RPG (на AS/400). Это то, что пришло на смену COBOL для коммерческих расчетов на платформах IBM. COBOL тут тоже поддерживается, но, в отличии от RPG он уже не развивается давно и на нем ничего нового не пишется.

          Так вот там два типа данных с фиксированной точкой:

          PACKED — фактически тот же FIXED DECIMAL в PL/1, BCD. Аналог в SQL — DECIMAL

          и

          ZONED — фактически это представление числа в виде строки. Аналог в SQL — NUMERIC

          Что же касается представления сумм в миноритарных единицах, то тут ноги растут немного из другого места. Дело в том, что не во всех валютах количество миноритарных единиц в основной равно 100. Есть валюты (бельгийский франк, итальянская лира, японская йена), в которых вообще миноритарных единиц нет. А есть такие (бахрейнский динар, оманский риал, тунисский динар), где 1000 миноритарных единиц в основной.

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

          Но для операций с валютами всегда используется арифметика с фиксированной точкой.

          В RPG еще есть функция округления %DECH
            0
            Спасибо за объяснение текущего состояния. Поскольку эти проблемы решались еще когда не то, что большинства программистов на свете не было, а их родителей еще не было, то, разумеется, задачи были как-то решены. А в PL/1 все тупо перенесли из Кобола и дело с концом.
            Сейчас программиста, не сталкивающегося с финансово-экономическими расчетами, легко узнать по предложению хранить все в целых копейках. Им кажется, что деньги только складывают и умножают ))
              0
              Как мне сказал один из наших молодых разработчиков — ты начал программировать когда я еще не родился :-)

              Просто в банке работаю. Пишу под AS/400 (IBM i нынче которая). Преимущественно на RPG (но и на C/C++ тоже — там, кстати, тип Decimal встроен для этой платформы, это аналог RPGшного PACKED и поддерживается он на уровне собственно системы — весь код работы с ним, я почти уверен, уже ниже уровня SLIC).

              А хранится все, действительно «в копейках». Но причина там совсем другая — выше написал. Специально глянул по нашему справочнику валют — не у всех валют в «рубле» 100 «копеек». Есть и 0 (т.е. вообще нет понятия «копейка»), есть и 1000 (этих мало, но они есть).

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

              А вообще тут про COBOL уже было. Именно про его арифметику с фиксированной точкой, правда, в разрезе скорости расходимости рекуррентного соотношения Мюллера — на фиксированной точке оно раза в два более устойчиво чем на плавающей.
              0
              О, коллега! Я с этим зверем RPG (на AS/400) тоже сталкивался и даже кое что там подправлял. Но было это еще в 2002-2004-х годах. Не думал что сейчас это все еще до сих пор актуально. Но вы меня порадовали — оно оказывается почти так же неубиваемо как Cobol.
                0
                Живет и здравствует :-) В LinkedIn есть вполне активные группы разработчиков.
                «Столпы» (Клемент, Хатчинсон — который RPGPGM ведет) там тоже присутствуют и активны

                У нас точно знаю что на нем сидят Альфа, Райф и Росбанк.

                Мы сейчас на версии 7.3, послед НГ обещали накатить TR9 (на бой, на тестовом уже накатили), а ближе к лету — 7.4

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

                Так что все нормально :-)
            0
            Последний стандарт IEEE754-2008 содержит типы decimal для вычислений. Поэтому и не стоит пользоваться двоично-десятичной арифметикой.
              +3

              Вот не надо путать читателей, называя десятичное представление с фиксированной точкой "точным".

                0

                В тред врывается НДС 20%. Пусть отпускная цена товара 100р. Представьте, пожалуйста, цену без НДС в виде DECIMAL.

                  0

                  Кстати, а как в реальной жизни решают такие проблемы? Округляют до копейки? Берут в партии столько единиц товара, чтобы всё нормально поделилось?
                  P.S. Забавно — можно подобрать НДС так, чтобы в десятичной системе счисления получались конечные дроби. Например, 25%, 2.4% или 19.20928955078125%

                    0
                    Кстати, а как в реальной жизни решают такие проблемы? Округляют до копейки?

                    Округляют. Потому что операцию надо отразить в бухгалтерском учете, а там все операции должны быть проведены в рублях и копейках, но не долях копейки. Если введут монеты типа «½ копейки» или «¼ копейки», как это уже было, значит будут округлять то 0,5 или 0,25 копейки.

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

                    Кстати, некоторые налоги при начислении и уплате округляют до рубля.

                    Забавно — можно подобрать НДС так, чтобы в десятичной системе счисления получались конечные дроби. Например, 25%, 2.4% или 19.20928955078125%

                    Логика НДС другая. Если Вы хотите продать товар за 100 рублей, то сверх этой суммы Вы должны взять с покупателя еще 20% (или 10%, или 0%) — налог-то с оборота берется. А в бюджет нужно заплатить разницу между тем, что Вы взяли с покупателей сверх своей цены, и тем, что Вы сами заплатили сверх цены продавца при покупке сырья.
                  0
                  Цена 1 ед. без НДС — 83,33 руб.
                  НДС — 16,67 руб.
                  Итого к оплате: 100,00 руб.
                    0

                    Вот-вот. Всё равно где-то да вылезет округление, так что нелепо надеяться, что представление точное.
                    Представлять надо не точно, а как налоговая говорит :-D

                      0
                      Представлять надо не точно, а как налоговая говорит

                      А это не налоговая говорит, а Налоговый Кодекс и документы ЦБ, связанные с денежным обращением. Собственно, это не только в России так, для чего и придумали денежные типы данных для всяких баз данных — чтобы их движки сами ничего не округляли и не повышали/понижали точность представления.
                    0
                    К сожалению, сами названия «FIXED/FLOAT» (вместо «ТОЧНОЕ/ПРИБЛИЖЕННОЕ») не вносят ясности и интуитивно непонятны.

                    Они интуитивно понятны, если знать, что такое "число с фиксированной точкой" и что такое "число с плавающей точкой". А именно, это сокращения от fixed point и floating point.


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

                      +2
                      Вернемся к простейшему примеру точных вычислений, теперь на языке PL/1, добавив деление на ту же константу, чтобы вернуть исходное значение переменной. Вычисления проведем и для типа FIXED DECIMAL и для типа FLOAT(53) – аналога типа «double» в других языках.


                      Не стоит забывать, что тут мы можем столкнуться с потерей точности. Пример на RPG:

                      dcl-s fltValue float(8) inz(165.783);
                      dcl-s pkdValue packed(6 : 3) inz(165.783);
                      dcl-s zndValue zoned(6 : 3) inz(165.783);

                      dsply (%char(fltValue));
                      dsply (%char(pkdValue));
                      dsply (%char(zndValue));
                      dsply ('-------------');

                      fltValue *= 0.137;
                      pkdValue *= 0.137;
                      zndValue *= 0.137;
                      dsply (%char(fltValue));
                      dsply (%char(pkdValue));
                      dsply (%char(zndValue));
                      dsply ('-------------');

                      fltValue /= 0.137;
                      pkdValue /= 0.137;
                      zndValue /= 0.137;
                      dsply (%char(fltValue));
                      dsply (%char(pkdValue));
                      dsply (%char(zndValue));


                      Результат:

                      +1.657830000000000E+002
                      165.783
                      165.783
                      -------------
                      +2.271227100000000E+001
                      22.712
                      22.712
                      -------------
                      +1.657830000000000E+002
                      165.781
                      165.781


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

                      Это то, что следует иметь ввиду при работе с переменными с фиксированной точкой.

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое