Работа с денежными значениями в JavaScript

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

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

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

А что не так с деньгами? Банки, брокеры, онлайн-магазины и т. д. — всем им нужно работать с деньгами программными методами. И ведь этот запрос не вчера возник.

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

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

От редакции Techrocks: возможно, вас также заинтересует статья «Лучшие плагины и библиотеки JavaScript для создания календарей».

Распространенные ловушки

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

В 2002 году Мартин Фаулер выпустил свою прославленную книгу «Шаблоны корпоративных приложений». И у нас появилась отличная модель для работы с денежными значениями. Все сводится к двум свойствам: сумме и валюте (amount и currency) и нескольким операциям, включая _+, -, *, /, >, >=, <, <= и =.

Задумайтесь о них на минутку.

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

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

Что касается JavaScript, с этой задачей может справиться, например, объект Money («деньги»), содержащий два свойства и предоставляющий некоторые функции для вычислений.

Не используйте числа с плавающей запятой для работы с денежными значениями

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

Обычно они представляются как степени 10:

10² = 100 центов в долларе
10³ = 1000 центов в 10 долларах

Но в компьютере представление денег в десятичных дробях приводит к некоторым проблемам.

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

0.2233 + 0.1 // дает в результате 0.32330000000000003

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

Вы можете решить округлить результат предыдущей операции самостоятельно, скажем, при помощи Math.ceil:

Math.ceil(0.2233 + 0.1) // дает в результате 1

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

Из-за озвученных проблем представление денег как float-объектов не рекомендуется. Если хотите разобраться в этом вопросе поглубже, я настоятельно советую почитать статью Oracle «What Every Computer Scientist Should Know About Floating-Point Arithmetic».

Number тоже не используйте

Как и во многих других языках, в JavaScript Number — это примитивный объект-обертка. Он используется, когда разработчику нужно представить числа (как целые, так и десятичные дроби) и произвести с ними какие-то манипуляции.

Но поскольку это значение — число двойной точности, оно представляет ту же угрозу, о которой мы говорили в предыдущем разделе.

Кроме того, Number также не имеет одного из условий Фаулера для создания идеальной денежной структуры: валюты. Если ваше приложение в настоящее время работает только с одной валютой, это не проблема. Но это может быть опасно, если что-то изменится в будущем.

Intl API

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

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

Взгляните не следующий пример:

var formatterUSD = new Intl.NumberFormat('en-US');
var formatterBRL = new Intl.NumberFormat('pt-BR');
var formatterJPY = new Intl.NumberFormat('ja-JP');

console.log(formatterUSD.format(0.2233 + 0.1)); // logs "0.323"
console.log(formatterBRL.format(0.2233 + 0.1)); // logs "0,323"
console.log(formatterJPY.format(0.2233 + 0.1)); // logs "0.323"

Мы создаем три разных форматтера, передающих разные локали для валют США, Бразилии и Японии соответственно. Возможности этого API одновременно учитывать и сумму, и валюту, и осуществлять гибкие вычисления с ними просто потрясают.

Обратите внимание, как меняется десятичная система в зависимости от страны и как правильно Intl API вычислил денежную сумму для всех валют.

Если вы хотите установить максимальное количество значащих цифр, просто измените этот код на следующий:

var formatterUSD = new Intl.NumberFormat('en-US', {
  maximumSignificantDigits: 2
});

console.log(formatterUSD.format(0.2233 + 0.1)); // logs "0.32"

Это то, что обычно происходит, когда вы платите за топливо на заправке.

API даже позволяет форматировать денежное значение, включая знак валюты отдельной страны:

var formatterJPY = new Intl.NumberFormat('ja-JP', {
  maximumSignificantDigits: 2,
  style: 'currency',
  currency: 'JPY'
});

console.log(formatterJPY.format(0.2233 + 0.1)); // logs "¥0.32"

Больше того: он позволяет конвертацию различных форматов, таких как скорость (км/ч) и объем (литры). По этой ссылке вы можете найти все доступные опции для NumberFormat в Intl.

Тем не менее, важно обращать внимание на поддержку этой фичи в браузерах. Хотя это стандарт, некоторые версии браузеров не поддерживают часть опций. Например, Internet Explorer и Safari.

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

Dinero.js, Currency.js и Numeral.js

Как обычно, для поддержки недостающих функций в языке можно найти отличные библиотеки. Например, dinero.js, currency.js и numeral.js.

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

Dinero.js

Dinero.js — это легковесная, иммутабельная и chainable JavaScript-библиотека, созданная для работы с денежными значениями. (Chainable — поддерживающая прием, при котором после вызова метода возвращается исходный объект, что позволяет выполнять методы последовательно, а не вызывать их по отдельности, — прим. ред.)

Библиотека предоставляет глобальные настройки, расширенные функции для форматирования и округления, простую конвертацию валют и нативную поддержку Intl.

Для установки библиотеки достаточно запустить одну команду:

npm install dinero.js

Одно из самых больших преимуществ использования Dinero.js состоит в том, что в ней применяется Фаулеровское определение денег. А это означает поддержку значений как суммы, так и валюты:

const money = Dinero({ amount: 100, currency: 'USD' })

Кроме того, эта библиотека предоставляет дефолтные методы для денежных вычислений:

const tax = Dinero({ amount: 10, currency: 'USD' })
const result = money.subtract(tax) // returns new Dinero object

console.log(result.getAmount()) // logs 90

Важно отметить, что Dinero.js предназначена для работы не только с центами. Просто значение суммы указывается в наименьших денежных единицах используемой валюты. Если речь идет о долларах США, то наименьшая единица — цент, а значит, деньги представляются в центах.

Для помощи с форматированием библиотека предоставляет метод toFormat(). Он принимает строку с шаблоном, по которому вы хотите форматировать суммы:

Dinero({ amount: 100 }).toFormat('$0,0') // logs "$1"
Dinero({ amount: 100000 }).toFormat('$0,0.00') // logs "$1,000.00"

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

Dinero({ amount: 100000, precision: 3 }).toFormat('$0,0.000') // logs "$100.000"
Dinero({ amount: 100, currency: 'JPY', precision: 0 }).toFormat() // logs "¥100.00"

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

Dinero({ amount: 10000, currency: 'USD' })
.add(Dinero({ amount: 20000, currency: 'USD' }))
    .divide(2)
    .percentage(50)
    .toFormat() // logs "$75.00"

Dinero.js также дает возможность настроить локальный или удаленный API для конвертации валют при помощи метода convert. Вы можете получить обменные данные из внешнего REST API или настроить локальную базу данных с файлом JSON. Этот файл Dinero.js сможет использовать для выполнения преобразований.

Currency.js

Currency.js — это очень маленькая (всего 1,14 kB) JavaScript-библиотека для работы со значениями валют.

Чтобы решить проблему плавающей запятой, о которой мы говорили выше, currency.js работает с целыми числами (под капотом). Таким образом обеспечивается постоянная точность десятичных значений.

Чтобы установить библиотеку, введите команду:

npm install currency.js

Эта библиотека может быть даже более лаконичной, чем Dinero.js. Денежное значение (строка, десятичное число, валюта) инкапсулируется в объект currency():

currency(100).value // logs 100

API ясный и понятный, поскольку тоже следует chainable-стилю:

currency(100)
.add(currency("$200"))
.divide(2)
.multiply(0.5) // simulates percentage
.format() // logs "$75.00"

Он также принимает строковые параметры, такие как денежное значение с символом валюты, как показано выше. Метод format(), в свою очередь, возвращает более человекочитаемый формат валюты.

Но стоит отметить, что по умолчанию currency.js настроена на работу в локали US. Если вы хотите работать с другими валютами, нужно приложить дополнительные усилия:

const USD = value => currency(value);
const BRL = value => currency(value, {
  symbol: 'R$',
  decimal: ',',
  separator: '.'
});
const JPY = value => currency(value, {
  precision: 0,
  symbol: '¥'
});

console.log(USD(110.223).format()); // logs "$110.22"
console.log(BRL(110.223).format()); // logs "R$110,22"
console.log(JPY(110.223).format()); // logs "¥110"

Библиотка Currency.js более лаконичная, чем Dinero.js, и это отлично. Но в ней нет встроенного способа проводить конвертацию валют. Имейте это в виду, если хотите использовать ее в своем приложении.

Numeral.js

Numeral.js — библиотека JavaScript общего назначения. Она умеет форматировать числа и осуществлять различные операции с ними.

Хотя она также может работать со значениями валют, разработчику предлагается очень гибкий API для создания пользовательских форматов.

Установка библиотеки выполняется одной командой:

npm install numeral

Синтаксис имеет много общего с currency.js, но денежное значение инкапсулируется в объект numeral():

numeral(100).value() // logs 100

Что касается форматирования значений, тут синтаксис больше напоминает Dinero.js:

numeral(100).format('$0,0.00') // logs "$100.00"

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

numeral.register('locale', 'es', {
  delimiters: {
    thousands: ' ',
    decimal: ','
  },
  currency: {
    symbol: '€'
  }
})

numeral.locale('es')

console.log(numeral(10000).format('$0,0.00')) // logs "€10 000,00"

Что касается chainable паттерна, numeral.js тоже обеспечивает отличную читаемость:

const money = numeral(100)
  .add(200)
  .divide(2)
  .multiply(0.5) // simulates percentage
  .format('$0,0.00') // logs "$75.00"

Numeral.js — наиболее гибкая библиотека для работы с числами в нашем списке. Ее гибкость проявляется в возможности создавать локали и форматы по желанию разработчика. Но нужно проявлять осторожность: библиотека не предоставляет, например, дефолтного способа вычислений с нулевой точностью для десятичных чисел.

Итоги

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

  • При работе с деньгами ни в коем случае не следует пользоваться числами с плавающей запятой или объектом-оберткой Number.
  • Предпочтительнее использовать Intl API, который предоставляется вашим браузером по умолчанию. Он более гибкий, безопасный и умный. Но обращайте внимание на ограничения совместимости.
  • Если вы можете немного увеличить вес пакета своего приложения, подумайте об использовании одной из библиотек, которые мы рассмотрели в статье. Они помогут вам с подсчетом денежных значений и их конвертацией. Но обязательно почитайте их документацию. Большинство из них предоставляют отличные тесты, которые должны помочь вам разобраться в плюсах и минусах библиотеки и выбрать наиболее подходящую для своих нужд.

Перевод статьи «Currency Calculations in JavaScript».

[customscript]techrocks_custom_after_post_html[/customscript]

[customscript]techrocks_custom_script[/customscript]

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх