DEV Community

Honeybadger Staff for Honeybadger

Posted on • Originally published at honeybadger.io

Currency Calculations in JavaScript

This article was originally written by Julio Sampaio on the Honeybadger Developer Blog.

One of the most curious things about modern programming languages is that when someone decides to create a new one, lots of thinking about the accepted data types and internal auxiliary libraries takes place.

Think about all the programming languages you've worked with before. How many ways to deal with dates and times do they have? Most of them will probably release at least one way to deal with such data types because it is a very present type in a developer's programming life.

What happened to the money, then? Banks, brokers, online shopping, etc. need to handle money programmatically. And it's been like that for a long time.

Because of the lack of representativity, money types are handled in many different ways, depending on the language you're using. Consequently, some pitfalls show up.

In this article, we will explore these common pitfalls in greater detail and the best options to deal with money in JavaScript.

Commons Pitfalls

Before we dive into the pitfalls, let's first understand what is required to perform monetary calculations.

Since 2002, when Martin Fowler released his acclaimed book titled Patterns of Enterprise Application Architecture, we have a great model to deal with monetary values. It all comes down to two properties, amount and currency, and several expected operations, including _+, -, *, /, >, >=, <, <=, and =.

Think about them for a bit. If we stop seeing money as a simple number and start viewing it as a data structure composed of two essential properties and some methods to deal with comparison, conversion, and calculations, then we're tackling most of the problems involving this data type.

In other words, to do monetary calculations, you will always need an amount and a currency, as well as a way to perform operations on them (i.e., via methods/functions).

From a JavaScript perspective, a Money object that can, for example, hold the two props and expose some functions for calculations would do the job.

Don't Use a Floating Point

When dealing with money you'll need to store cents as well. For many devs, storing such values into decimal numbers is the right decision because there are decimal places.

Usually, they're represented as a unit of a power of 10:

10² = 100 cents in a dollar
10³ = 1000 cents in 10 dollars
...
Enter fullscreen mode Exit fullscreen mode

However, representing money as floating-point numbers in a computer presents some problems, as we've seen here.

Floating-point numbers exist through different arithmetic on your computer. Since your computer makes use of the binary system to store decimal numbers, you'll eventually produce inconsistent results with your calculations:

0.2233 + 0.1 // results in 0.32330000000000003
Enter fullscreen mode Exit fullscreen mode

This happens because the computer tries to round off as much as it can to obtain the best result. It also cuts off numbers that are too large, such as periodic tithes, for example.

You could decide to round the result of the previous operation by yourself via, for example, Math.ceil:

Math.ceil(0.2233 + 0.1) // results in 1
Enter fullscreen mode Exit fullscreen mode

However, this approach would still be problematic because you'd lose a couple of pennies during the process. Depending on the type of application you're developing, a loss like that could represent a lot of lost money for customers or your business.

Because of these problems, representing money as a float object is not a recommended approach. If you're still interested in knowing more about the specifics of this issue, I strongly recommend reading Oracle's article: What Every Computer Scientist Should Know About Floating-Point Arithmetic.

Don't Use Number Either

Like in many other languages, a Number is a primitive wrapper object that is used when developers need to represent or manipulate numbers, from integers to decimals.

Additionally, because it is adouble-precision 64-bit binary format IEEE 754 value, it also presents the same threat we just talked about in the previous section.

Furthermore, Number also lacks one of Fowler's conditions to create a perfect monetary structure: currency. It would be perfectly fine if your application currently only deals with one currency. However, it could be dangerous if things change in the future.

The Intl API

The ECMAScript Internationalization API is a collective effort to provide standardized formatting for international purposes. It allows applications to decide which functionalities they need and how they will be approached.

Among the many features provided, we have number formatting, which also embraces monetary value formatting based on the specified locale.

Take a look at the following example:

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"
Enter fullscreen mode Exit fullscreen mode

We're creating three different formatters passing different locales for US, Brazilian, and Japanese currencies, respectively. This is great to see how powerful this API is in terms of embracing both the amount and the currency at the same time and performing flexible calculations on them.

Note how the decimal system changes from one country to another and how the Intl API properly calculated the result of our monetary sum for all the different currencies.

If you'd like to set the maximum number of significant digits, simply change the code to:

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

console.log(formatterUSD.format(0.2233 + 0.1)); // logs "0.32"
Enter fullscreen mode Exit fullscreen mode

This is what usually happens when you pay for gas at a gas station.

The API can even allow you to format a monetary value, including the currency sign of the specific country:

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

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

Furthermore, it allows the conversion of various formats, such as speed (e.g., kilometer-per-hour) and volume (e.g., _liters). At this link, you may find all the available options for the Intl NumberFormat.

However, it's important to pay attention to the browser compatibility limitations of this feature. Since it is a standard, some browser versions won’t support part of its options, such as Internet Explorer and Safari.

For these cases, a fallback approach would be welcome if you're willing to support your app on these web browsers.

Dinero.js, Currency.js, and Numeral.js

However, there are always great libraries that the community develops to support missing features, such as dinero.js, currency.js, and numeral.js.

There are more available on the market, but we will focus on these three because they represent a significant percentage of developers using currency formatting features.

Dinero.js

Dinero.js is a lightweight, immutable, and chainable JavaScript library developed to work with monetary values and enables global settings, extended formatting/rounding options, easy currency conversions, and native support to Intl.

Installing it is as easy as running a single command:

npm install dinero.js
Enter fullscreen mode Exit fullscreen mode

One of the greatest benefits of using this library is that it completely embraces Fowler's definition of money, which means it supports both amount and currency values:

const money = Dinero({ amount: 100, currency: 'USD' })
Enter fullscreen mode Exit fullscreen mode

Furthermore, it also provides default methods to deal with monetary calculations:

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

console.log(result.getAmount()) // logs 90
Enter fullscreen mode Exit fullscreen mode

It's important to state that Dinero.js doesn't deal with cents separately. The amounts are specified in minor currency units, depending on the currency you're using. If you're using USD, then money is represented in cents.

To help with the formatting part, it provides us with the toFormat() method, which receives a string with the currency pattern with which you'd like to format:

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

You're in control over how the library handles the formats. For example, if you're dealing with currencies that have a different exponent (i.e., more than two decimal places), you can explicitly define the precision, as shown below:

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

Perhaps one of its greatest features is the chainable support for its methods, which leads to better readability and code maintenance:

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

Dinero.js also provides a way to set up a local or remote exchange conversion API via its convert method. You can either fetch the exchange data from an external REST API or configure a local database with a JSON file that Dinero.js can use to perform conversions.

Currency.js

Currency.js is a very small (only 1.14 kB) JavaScript library for working with currency values.

To tackle the floating-point issue we talked about, currency.js works with integers behind the scenes and ensures that decimal precision is always correct.

To install it, you just need a single command:

npm install currency.js
Enter fullscreen mode Exit fullscreen mode

The library can be even less verbose than Dinero.js, by encapsulating the monetary value (whether it is a string, a decimal, a number, or a currency) into its currency() object:

currency(100).value // logs 100
Enter fullscreen mode Exit fullscreen mode

The API is very clean and straightforward since it also adopts a chainable style:

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

It also accepts string parameters, such as a monetary value, with the sign, as seen above. The format() method, in turn, returns a human-friendly currency format.

However, when it comes to internationalization, currency.js defaults to the US locale. If you're willing to work with other currencies, some extra work must be done:

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"
Enter fullscreen mode Exit fullscreen mode

Currency.js is cleaner than Dinero.js in terms of verbosity, which is great. However, it doesn't have a built-in way to perform exchange conversions, so be aware of this limitation if your application needs to do so.

Numeral.js

As the library name suggests, Numeral.js is more of a general-purpose library that deals with formatting and manipulating numbers in general in JavaScript.

Although it can also manipulate currency values, it offers a very flexible API to create custom formats.

To install it, only one command is required:

npm install numeral
Enter fullscreen mode Exit fullscreen mode

Its syntax is very similar to currency.js when encapsulating a monetary value into its numeral() object:

numeral(100).value() // logs 100
Enter fullscreen mode Exit fullscreen mode

When it comes to formatting these values, it is closer to Dinero.js' syntax:

numeral(100).format('$0,0.00') // logs "$100.00"
Enter fullscreen mode Exit fullscreen mode

Since the library has limited built-in internationalization features, you'd have to set yours up in case a new currency system is needed:

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

numeral.locale('es')

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

When it comes to the chainable pattern, numeral.js also delivers the same readability we're looking for:

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

Numeral.js is, by far, the most flexible library to deal with numbers on our list. Its flexibility includes the capacity to create locales and formats as you wish. However, be careful when using it since it doesn't provide a default way to calculate with zero precision for decimal numbers, for example.

Wrapping Up

In this blog post, we've explored some of the best alternatives to deal with monetary values within JavaScript, whether it is for client or backend applications. Let's recap some of the important points discussed so far:

  • If you're dealing with money, never use the floating-point primitive numbers of the language or the Number wrapper object.
  • Instead, the Intl API provided by default by your web browser is preferred. It is more flexible, secure, and smart. However, be aware of its compatibility limitations.
  • Regardless of each, if you can add a bit more weight to your app bundle, consider adopting one of the demonstrated libraries when dealing with currency calculations and/or conversions.

Finally, make sure to refer to their official docs and tests. Most of them provide great tests to help you understand the pros and cons of each library and choose the one that best suits your needs.

Oldest comments (0)