Как представить валюту или деньги на C

TL; DR 1 Что такое точный и поддерживаемый подход для представления валюты или денег в C?


Предыстория вопроса:
На это ответили для ряда других языков, но я не смог найти надежный ответ для языка C.

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

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

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

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


Упрощение предположений:

  • Предположите архитектуру на основе x86 или x64, но, пожалуйста, обращайтесь к любым предположениям, которые повлияют на архитектуру на основе RISC, такую ​​как чип Power или чип Arm.

  • Точность в расчетах является основным требованием. Простота обслуживания будет следующим требованием. Скорость расчетов важна, но третична по отношению к другим требованиям.

  • Расчеты должны быть в состоянии безопасно поддерживать операции с точностью до mill, а также поддерживать значения, вплоть до триллионы (10 ^ 9)


Отличия от других вопросов:

Как отмечалось выше, этот тип вопроса задавался ранее для нескольких других языков. Этот вопрос отличается от других вопросов по нескольким причинам.

Использование принятого ответа от Почему бы не использовать Double или Float для представления валюты?, выделите различия.

(Решение 1) Решение, которое работает практически на любом языке, это использовать целые числа и подсчитать центы. Например, 1025 будет $10,25. На нескольких языках также есть встроенные типы для работы с деньгами. ( Решение 2). Среди прочего, Java имеет класс BigDecimal, а С# имеет десятичный тип.

Акцент добавлен, чтобы выделить два предложенных решения

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

Другим решением является использование собственного класса decimal, который недоступен в C.

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

Этот вопрос отличается от этого вопроса и других связанных вопросов, потому что C не имеет одного уровня поддержки родного типа и имеет языковые функции, которых нет на других языках. И я не видел, чтобы какие-либо другие вопросы касались нескольких способов, с которыми это можно было бы приблизить в C.


Вопрос:
По моим исследованиям, кажется, что float не является подходящим типом данных, который будет использоваться для представления валюты в программе C из-за ошибки с плавающей запятой.

Что я должен использовать для представления денег на C, и почему этот подход лучше, чем другие подходы?

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

Ответ 1

Используйте либо целочисленные типы данных (long long, long, int), либо BCD (двоично-кодированную десятичную) арифметическую библиотеку. Вы должны хранить десятые или сотые доли наименьшего количества, которое вы будете отображать. То есть, если вы используете доллары США и представляете центы (сотые доли доллара), ваши числовые значения должны быть целыми числами, представляющими мельницы или миллиграммы (десятые или сотые доли процента). Дополнительные существенные цифры обеспечат ваш интерес и аналогичные расчеты последовательно.

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

Ответ 2

Лучшее представление денег/денег - использовать более высокий тип точности с плавающей точкой, например double, который имеет FLT_RADIX == 10. Эти платформы/компиляторы редки, так как подавляющее большинство систем имеют FLT_RADIX == 2.

Четыре альтернативы: целые числа, не десятичная плавающая запятая, специальная десятичная плавающая точка, структура, определенная пользователем.

Целые числа. Общее решение использует целочисленный счет наименьшего номинала в выбранной валюте. Пример подсчета американских центов вместо долларов. Диапазон целых чисел должен быть достаточно широким. Что-то вроде long long вместо int как int может обрабатывать только +/- $320.00. Это отлично подходит для простых задач бухгалтерского учета с добавлением/вычитанием/множественным, но начинает трещать с делениями и сложными функциями, которые используются в расчетах процентов. Ежемесячная платежная формула. Подписанная целочисленная математика не имеет защиты от переполнения. Уход следует применять при округлении результатов округления. q = (a + b/2)/b недостаточно хорош.

Двоичная с плавающей запятой: 2 общих ловушки: 1) с использованием float, которая часто бывает недостаточной точности и 2) неправильного округления. Используя double хорошо адресует проблему №1 для многих учетных ограничений. Тем не менее, код еще часто должен использовать раунд для желаемой минимальной единицы валюты для удовлетворительных результатов.

// Sample - does not properly meet nuanced corner cases.
double RoundToNearestCents(double dollar) {
  return round(dollar * 100.0)/100.0;
}

Вариант double заключается в использовании значения double наименьшей единицы (0,01 или 0,001). Важным преимуществом является возможность просто округлить, используя функцию round(), которая сама по себе встречает угловые случаи.

Специальная десятичная плавающая точка. Некоторые системы предоставляют "десятичный" тип, отличный от double, который соответствует decimal64 или что-то подобное. Несмотря на то, что это касается большинства вышеперечисленных проблем, переносимость приносится в жертву.

Структура, определенная пользователем (например, fixed-point), конечно, может решить все, кроме ошибки, подверженной коду так много, и это работа (Вместо). Результат может отлично функционировать, но при этом не хватает производительности.

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

[Изменить]
При дополнительных дополнительных изменениях в ОП рекомендуется использовать double номер наименьшей единицы валюты (пример: $0.01 → double money = 1.0;). В разных точках кода, когда требуется точное значение, используйте round().

double interest_in_cents = round(
    Monthly_payment(0.07/12 /* percent */, N_payments, principal_in_cents));

Мой хрустальный шар говорит, что к 2022 году США сбросят $0.01, а самая маленькая единица составит $0,05. Я бы использовал подход, который лучше всего справится с этим сдвигом.

Ответ 3

Если ваша основная проблема связана с скоростью, используйте интегральный тип, масштабированный до наименьшей единицы, которую вы должны представлять (например, мельница, которая составляет 0,001 доллара или 0,1 цента). Таким образом, 123456 представляет $123.456.

Проблема с этим подходом заключается в том, что у вас могут закончиться цифры; 32-разрядный unsigned int может представлять что-то вроде десяти десятичных цифр, поэтому наибольшее значение, которое вы могли бы представить по этой схеме, было бы $9,999,999.999. Нехорошо, если вам нужно иметь дело со значениями в миллиардах.

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

struct money {
  long whole_dollars; // long long if you have it and you need it
  int frac_dollar; 
};                          

An int является более чем достаточно широким, чтобы обрабатывать масштабирование любого здравомыслящего человека. Оставляя его подписанным, если часть whole_dollars равна 0.

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

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

Ответ 4

int (32 или 64, как вам нужно) и подумайте в центах или частичных центах по мере необходимости. С 32 бит и мышлением в центах вы можете представить до 40 миллионов долларов в одном значении. С 64-битной версией она намного превосходит все американские подразделения, которые когда-либо сочетались.

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

Это игра о знании диапазонов и когда округление после деления прекрасное.

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

long long res = (amount * interest + 500)/1000;

При общении с пользователем конвертируется только в доллар (или что-то еще).