TL;DR
{
"sourceAmount": "GBP 12.50",
"targetAmount": "JPY 5030",
"negativeExample": "EUR -1460.36"
}
Работа с деньгами в вашем коде
Если вы какое-то время разрабатывали финансовое программное обеспечение, вы наверняка усвоили несколько золотых правил, которые позволяют поддерживать хорошие манеры в основе ваших программ:
- Избегайте использования типов с плавающей запятой для представления сумм в валюте. Десятичные форматы существуют в большинстве языков для точного представления десятичных чисел. ☕ 🐍
- Если вы выберете целочисленное представление второстепенных сумм в валюте (пенсы, центы), то 21 474 836,47 фунтов стерлингов — это наибольшая сумма денег, которую вы можете представить в виде числа пенсов в примитиве со знаком int (32). Это ((2³¹ - 1) / 100). 92 233 720 368 547 758,07 фунтов стерлингов (PayPal ошибочно «зачисляет американцу 92 квадриллиона долларов») — это максимум для длинной позиции (64). Int (32) недостаточно во многих случаях, а long (64) может не хватить в некоторых ограниченных случаях 💰💰💰.
- Без наличия процессов, таких как использование объектов для инкапсуляции валюты и суммы, значения разных валют могут быть объединены в бессмысленные суммы, такие как 55,10 фунтов стерлингов + 100,20 долларов США.
Currency currency1 = GBP BigDecimal amount1 = new BigDecimal("55.10") // not new BigDecimal(55.10) 👎 to floats
Currency currency2 = USD BigDecimal amount2 = new BigDecimal("100.20")
Currency currency3 = GBP // Picked at random BigDecimal meaninglessAmount3 = amount1.add(amount2)
Глядя дальше на BigDecimal в Java, у него есть несколько неортодоксальное поведение:
new BigDecimal(“1.2”).equals(new BigDecimal(“1.20”)); // results in false due to the scales being different
Использование объекта для переноса валюты и суммы и обеспечения того, чтобы шкала суммы была установлена на меньшую шкалу валюты, предотвращает неожиданные сбои сравнения. Java Money — это готовый вариант, но вы можете выбрать собственный код, если хотите что-то более простое или более близкое к вашему внутреннему представлению.
Сериализация в JSON
Предположительно, вы также захотите отправлять денежные значения другим службам и клиентам в вашей системе. Если вы используете двоичный формат для сериализации своих значений, вам нужно придумать что-то разумное, соответствующее вашему контексту.
Если вы используете хорошо зарекомендовавший себя формат, такой как JSON, вы можете ожидать, что существует согласованный стандарт для сериализации. Нет.
«Большая часть компьютеров в мире манипулирует деньгами, поэтому меня всегда озадачивало, что деньги на самом деле не являются первоклассным типом данных ни в одном из основных языков программирования». — Мартин Фаулер, Patterns of Enterprise Application Architecture
Сумма в формате JSON
JSON не имеет десятичного типа данных, поэтому любое представление с десятичной точкой может быть обработано ничего не подозревающей принимающей системой как число с плавающей запятой.
{
"paymentAmount": {
"currency": "GBP",
"amount": 15.56,
"_comment": "👎 will be interpreted by most JSON libraries as a float"
}
}
Это может подойти для ваших целей, если у вас есть контроль над кодовой базой приложений для iPhone и Android, а ваш API предназначен только для внутреннего потребления. Вам нужно будет предпринять шаги, чтобы убедиться, что каждое значение поступает на уровень вашего приложения в виде десятичного или целочисленного представления, которое вы выбрали.
Сумма в виде строки
Использование количества String гарантирует, что точность не будет потеряна на пути через библиотеку JSON к уровню вашего приложения; ваше приложение будет представлено строкой и должно будет ее интерпретировать.
{
"paymentAmount": {
"currency": "GBP",
"amount": "15.56"
}
}
Принудительно форматируя сумму как строку с ведущими нулями, 15,00 фунтов стерлингов строго форматируются как «15,00», а не как «15» или «15,0», что имеет то преимущество, что неявно сообщает «младшую шкалу» 2 для фунтов стерлингов. JPY имеет малую шкалу 0, поэтому суммы всегда будут кодироваться как «123», а не как «123,00».
Незначительная сумма в виде целого числа JSON
Если вы выбрали целочисленное представление для денежных сумм в своем коде, может иметь смысл отображать суммы в пенсах в JSON.
{
"paymentAmount": {
"currency": "GBP",
"amountMinor": 1556,
"minorScale": 2
}
}
Это устраняет любые проблемы, связанные с потерей точности, но вы также можете счесть необходимым указать меньшую шкалу, которая используется для преобразования основной суммы в второстепенную. Маловероятно, что отношения между фунтами и пенсами или долларами и центами изменятся, но неразумно полагаться на то, что библиотеки отправителя и получателя сообщения JSON будут в полном согласии относительно малых масштабов всех мировых валют.
Переключение на второстепенные суммы снижает удобочитаемость для человека и, в зависимости от того, как ваша библиотека JSON моделирует целые числа, потенциально ограничивает максимальную денежную сумму, которая может быть представлена.
⬆️ Все форматы, в которых валюта отделена от суммы
Все рассмотренные до сих пор форматы поддерживают отдельные поля валюты и суммы. Полученный JSON выглядит немного запутанным для читателей. Это представление объекта с двумя полями и не похоже на языковой примитив.
Отдельная сумма может побудить клиентов игнорировать значение валюты, считывать все суммы с помощью запроса JSON и начинать обработку сумм на основе некоторых предположений: «Я уверен, что все значения — GBP…».
И победитель…
Если мы решили использовать строковый атрибут для суммы, а наше приложение все равно вынуждено анализировать значение, мы можем объединить валюту и сумму в одну строку: код валюты ISO, за которым следует пробел, за которым следует число со знаком с десятичными знаками, соответствующими младшей шкале валюты.
🥇🥇🥇🥇🥇🥇🥇🥇
{
"paymentAmount": "GBP 15.56"
}
🥇🥇🥇🥇🥇🥇🥇🥇
Прикладной уровень
Если выбранная вами библиотека JSON позволяет подключать преобразователи, чтобы «123,45 евро» можно было преобразовать непосредственно в объект, который вы используете для представления денежных сумм, то, вероятно, стоит потратить время на написание плагина. Ваши объекты данных будут автоматически сериализованы и десериализованы в выбранном вами формате.
Приведите в порядок вопросы
Почему десятичная точка ASCII не является запятой?
- В европейских языках, как правило, используется запятая для разделения основных и второстепенных сумм в валюте. Например, «12,34 евро» во Франции обычно имеет формат «12,34 евро». Десятичные точки ASCII используются в исходном коде для основных языков программирования, что делает их естественным первым выбором.
Почему нет группировки основной суммы в тысячи и миллионы?
- Во многих языках большие числа группируются запятой, например «123 345,78 фунтов стерлингов». Такое поведение зависит от локали и добавляет ненужные сложности к простому, неукрашенному представлению числа в виде строки в JSON.
Почему вместо символов используются трехбуквенные коды валют ISO?
- Символы зависят от локали. Коды ISO, хотя и выглядят не так хорошо на странице, однозначны.
Почему код валюты ISO стоит в начале, а не в конце?
- Зная, что первые 3 символа будут кодом ISO, за которым следует пробел, а затем числовое значение, упрощается разбор строки с использованием фиксированных позиций слева направо. Конечно, вы можете привести противоположный аргумент, если предпочитаете анализировать строки с конца к началу…
Не является ли запрос уровня приложения на синтаксический анализ строки ненужной дополнительной работой?
- Если для вашего приложения важна правильная обработка денежных сумм, то это хорошая возможность остановить синтаксические анализаторы JSON, использующие их поведение по умолчанию, и указать, какое поведение вы хотите.
Я уже использую другой формат. Как мне переключиться, если я захочу?
- Общее правило сериализации состоит в том, чтобы сделать ваш сериализатор строгим, чтобы он всегда представлял значения одним способом, но сделать ваш десериализатор терпимым и прощающим. Начните с того, что убедитесь, что все десериализаторы в вашей системе могут обрабатывать как ваш существующий формат, так и новый формат. Оказавшись на месте, измените ваши сериализаторы один за другим, чтобы сгенерировать новый формат.