Лучший способ хранить значения валюты в C++

Я знаю, что число с плавающей запятой не подходит для хранения значений валюты из-за ошибок округления. Есть ли стандартный способ представления денег на C++?

Я поискал в библиотеке ускорения и ничего не нашел. В java кажется, что BigInteger - это способ, но я не смог найти эквивалента в C++. Я мог бы написать свой собственный денежный класс, но предпочел бы не делать этого, если есть что-то протестированное.

Для информации: нет более или менее ошибок округления при использовании двоичного или десятичного представления (см. 1/3 = 0,333 ...). Использование десятичного представления позволяет получить такие же ошибки округления, как если бы вы делали это вручную. (легче проверить / сопоставить результаты)

Offirmo 29.08.2012 12:04

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

Sebastian Mach 25.01.2017 08:26
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
65
2
46 332
19

Ответы 19

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

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

Orion Adrian 29.09.2008 22:23

Как и в моем ответе, точность значения равна точности наименее точного вычисления. Integer * Float будет использовать точность с плавающей запятой. Для C++ вся цепочка должна быть длинной двойной точности.

Orion Adrian 29.09.2008 22:29

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

Douglas Mayle 30.09.2008 22:15

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

На самом деле это не решает проблему, поскольку вам часто приходится делать больше, чем просто прибавлять к этим числам, и тогда у вас могут возникнуть проблемы, поскольку вы потеряете точность. 100,25 доллара в переводе на 10025 * 0,0745234 APR вызовут проблемы.

Orion Adrian 29.09.2008 19:50

Если я правильно помню, где-то есть стандарт, который гласит, что вы должны хранить как минимум 4 цифры для общих операций - поэтому «Валюта» COM дала вам 4. Если речь идет о иностранной валюте, вам, вероятно, понадобится больше.

Joe Pineda 30.09.2008 00:22

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

Orion Adrian 30.09.2008 00:23

@Joe: 4 знака после запятой - это на самом деле минимум. Я закончил использовать 6 для своих расчетов, чтобы получить пенсовое разрешение при проверке операций. Но если вы не сделаете всю свою математику в целочисленной форме, у вас будут проблемы, потому что, если вы приведете (неявно или явно), вы попадете в страну с плавающей запятой.

Orion Adrian 30.09.2008 00:27

Разобравшись с этим в реальных финансовых системах, я могу сказать, что вы, вероятно, захотите использовать число с точностью не менее 6 десятичных знаков (при условии, что это доллар США). Надеюсь, раз уж вы говорите о валютных ценностях, вы не выйдете из строя. Есть предложения по добавлению десятичных типов в C++, но я пока не знаю ни одного из них.

Лучшим родным типом C++ для использования здесь будет long double.

Проблема с другими подходами, в которых просто используется int, заключается в том, что вам нужно хранить не только свои центы. Часто финансовые транзакции умножаются на нецелочисленные значения, и это доставит вам неприятности, поскольку 100,25 доллара, переведенные в 10025 * 0,000123523 (например, годовая процентная ставка), вызовут проблемы. В конечном итоге вы попадете в страну с плавающей запятой, и преобразования будут вам дорого стоить.

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

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

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


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

Не могли бы вы расширить свое возражение на ints или дать ссылку? Приведенный вами пример расчета приводит к результату 0,01 доллара США или 1 с использованием целых чисел. Для меня не очевидно, почему это неправильный ответ.

Jeffrey L Whitledge 29.09.2008 21:54

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

Orion Adrian 29.09.2008 22:21

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

Jeffrey L Whitledge 29.09.2008 23:15

@ Джеффри (и другие) - В дополнение к тому, что уже сказал Орион, финансовые системы должны иметь возможность справляться с очень широким диапазоном чисел. Акции на фондовых рынках (и, в частности, курсы валют) рассчитываются с точностью до долей пенни (0,000001 доллара США), в то время как другие валюты, такие как доллар Зимбабве, испытывают гиперинфляцию (en.wikipedia.org/wiki/Zimbabwean_dollar#Exchange_rate_histo‌ ry) до такой степени, что даже системы, работающие с двойными ставками, не могут справиться с большими используемые значения. Поэтому использование int, long int и т. д. Действительно не вариант.

jmc 26.06.2012 00:59

Я бы рекомендовал использовать long int для хранения валюты наименьшего достоинства (например, американские деньги будут центами), если используется десятичная валюта.

Очень важно: обязательно назовите все ваши денежные значения в соответствии с тем, что они на самом деле содержат. (Пример: account_balance_cents) Это позволит избежать многих проблем в будущем.

(Другой пример, когда это происходит, - это проценты. Никогда не называйте значение «XXX_percent», если оно фактически содержит отношение, не умноженное на сотню.)

Посмотрите на относительно недавний Математическая библиотека Intelr Decimal с плавающей запятой. Это специально для финансовых приложений и реализует некоторые из новые стандарты двоичной арифметики с плавающей запятой (IEEE 754r).

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

Какой бы тип вы ни выбрали, я бы рекомендовал заключить его в «typedef», чтобы вы могли изменить его в другое время.

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

Sebastian Mach 25.01.2017 08:31

Один из вариантов - сохранить 10,01 доллара как 1001 и производить все вычисления в пенни с делением на 100D при отображении значений.

Или используйте поплавки и округляйте их только в самый последний момент.

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

Вместо значения * .10 для 10% скидки используйте (значение * 10) / 100, что значительно поможет. (помните, что .1 - это повторяющийся двоичный файл)

Никогда не используйте поплавки. Попробуйте представить 0,60 доллара в виде числа с плавающей запятой. Финансовый код (также известный как код банка) не может иметь ошибок округления => нет числа с плавающей запятой.

Martin York 29.09.2008 20:02

0.6 не может храниться как float или double. Большинство реальных чисел не может быть, числа с плавающей запятой - это всего лишь приближения. Вот результат, который я получаю для пары чисел (0,6 и 8,075): float: 0.60000002384185791000 float: 8.07499980926513670000 double: 0.59999999999999998000 double: 8.07499999999999930000

Superfly Jon 27.03.2015 12:21

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

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

Наше финансовое учреждение использует «двойник». Поскольку мы являемся магазином с фиксированным доходом, у нас есть множество неприятных сложных алгоритмов, которые в любом случае используют double. Уловка состоит в том, чтобы быть уверенным, что ваша презентация для конечного пользователя не выходит за рамки точности double. Например, когда у нас есть список сделок на общую сумму в триллионы долларов, мы должны быть уверены, что не печатаем мусор из-за проблем с округлением.

Не храните их просто как центы, так как вы довольно быстро накапливаете ошибки при умножении на налоги и проценты. По крайней мере, оставьте две лишние значащие цифры: 12,45 доллара будут сохранены как 124 500. Если вы сохраните его в виде 32-битного целого числа со знаком, у вас будет 200000 долларов для работы (положительное или отрицательное). Если вам нужны большие числа или более высокая точность, 64-битное целое число со знаком, вероятно, даст вам все пространство, которое вам понадобится в течение длительного времени.

Может оказаться полезным заключить это значение в класс, чтобы дать вам одно место для создания этих значений, выполнения над ними арифметических операций и их форматирования для отображения. Это также даст вам центральное место для хранения хранимой валюты (доллары США, канадские доллары, евро и т. д.).

как вы попали на 2 000 000? вы можете сохранить до 2 миллиардов центов в 32-битном целом числе со знаком, что составляет около 20 миллионов долларов. отнимите 2 цифры для большей точности, и у вас останется около 200 тысяч долларов.

wilhelmtell 24.09.2011 23:26

Насколько большим может удерживаться 64-битное целое число с использованием двух цифр дополнительной точности?

Assimilater 04.04.2015 08:59

Кроме того, я вижу, что этот пост довольно старый, отражает ли он лучший способ хранения валюты? Или что-то было добавлено в C++ 14 и / или boost, что было бы лучше сейчас?

Assimilater 04.04.2015 09:00

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

einpoklum 09.04.2018 22:33

Для центов @einpoklum требуется только до 2 знаков после запятой, но инвестиционные транзакции часто оперируют значениями от 4 до 6 знаков после запятой. Таким образом, для хранения может потребоваться более высокая точность, чем обеспечивает центов.

Remy Lebeau 04.04.2019 08:14

@RemyLebeau: Я этого не знал. Итак, я думаю, это зависит от того, что пытается сделать OP.

einpoklum 04.04.2019 11:08

Знайте СВОЙ диапазон данных.

Число с плавающей запятой подходит только для точности от 6 до 7 цифр, так что это означает, что максимальное значение составляет около + -9999,99 без округления. Это бесполезно для большинства финансовых приложений.

Двойное значение подходит для 13 цифр, например: + -99 999 999 999,99. Тем не менее будьте осторожны при использовании больших чисел. Обратите внимание на то, что вычитание двух похожих результатов сильно снижает точность (см. Книгу по числовому анализу, чтобы узнать о потенциальных проблемах).

32-битное целое число хорошо равно + -2 миллиарда (при масштабировании до копейки 2 десятичных разряда упадут)

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

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

вперёд и напишите свой собственный класс денег (http://junit.sourceforge.net/doc/testinfected/testing.htm) или currency () (в зависимости от того, что вам нужно). и протестируйте это.

Самая большая проблема - это округление!

19% от 42,50 € = 8,075 €. Согласно немецким правилам округления это 8,08 €. Проблема в том, что (по крайней мере, на моей машине) 8075 не могут быть представлены как двойные. Даже если я изменю переменную в отладчике на это значение, я получу 8,0749999 ....

И здесь моя функция округления (и любая другая логика с плавающей запятой, о которой я могу думать) терпит неудачу, поскольку она дает 8,07 евро. Значащая цифра - 4, поэтому значение округляется в меньшую сторону. И это совершенно неправильно, и вы ничего не можете с этим поделать, если не избегаете использования значений с плавающей запятой везде, где это возможно.

Он отлично работает, если вы представите 42,50 € как целое число 42500000.

42500000 * 19/100 = 8075000. Теперь вы можете применить правило округления выше 8080000. Его можно легко преобразовать в денежное значение для отображения. 8,08 €.

Но я всегда заканчивал это на уроке.

Вы можете попробовать десятичный тип данных:

https://github.com/vpiotr/decimal_for_cpp

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

Это решение для C++ только для заголовков.

Решение простое, сохраните с любой требуемой точностью в виде сдвинутого целого числа. Но при чтении конвертировать в двойное число с плавающей запятой, чтобы в вычислениях было меньше ошибок округления. Затем при сохранении в базе данных умножьте на любую требуемую целочисленную точность, но перед усечением в виде целого числа добавьте +/- 1/10 для компенсации ошибок усечения или +/- 51/100 для округления. Очень просто.

Сохраните сумму в долларах и центах как два отдельных целых числа.

Почему голос против? Так хранят цены некоторые крупные финансовые учреждения. ; (

kmiklas 03.04.2018 23:43

Я бы использовал длинную подпись для 32-битной и подписанную длинную для 64-битной. Это даст вам максимальную емкость для хранения самого основного количества. Затем я разработал два нестандартных манипулятора. Один конвертирует это количество на основе обменных курсов, а другой форматирует это количество в выбранную вами валюту. Вы можете разработать больше манипуляторов для различных финансовых операций / и правил.

Вы говорите, что заглянули в библиотеку ускорения и ничего там не нашли. Но у вас есть multiprecision / cpp_dec_float, который говорит:

The radix of this type is 10. As a result it can behave subtly differently from base-2 types.

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

Видеть:

#include <iostream>
#include <iomanip>
#include <boost/multiprecision/cpp_dec_float.hpp>

int main()
{
    float bogus = 1.0 / 3.0;
    boost::multiprecision::cpp_dec_float_50 correct = 1.0 / 3.0;

    std::cout << std::setprecision(16) << std::fixed 
              << "float: " << bogus << std::endl
              << "cpp_dec_float: " << correct << std::endl;
          
    return 0;
}

Выход:

float: 0.3333333432674408

cpp_dec_float: 0.3333333333333333

* Я не говорю, что число с плавающей запятой (основание 2) - это плохо, а десятичное (основание 10) - это хорошо. Просто они по-другому ведут себя ...

** Я знаю, что это старый пост, а boost :: multiprecision был представлен в 2013 году, поэтому хотел бы отметить его здесь.

Другие вопросы по теме