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, использующие их поведение по умолчанию, и указать, какое поведение вы хотите.

Я уже использую другой формат. Как мне переключиться, если я захочу?

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