elixir-cldr / cldr_currencies

Packages the currency definitions from CLDR into a set of functions to return currency data.
Other
10 stars 7 forks source link

Explanation of %Cldr.Currency{} fields #2

Closed nickdichev closed 4 years ago

nickdichev commented 4 years ago

Hi, first of all thank you for the wonderful library!

I was hoping that you could give a brief explanation of the %Cldr.Currency{} fields, in particular, cash_digits, cash_rounding, digits, iso_digits and rounding.

For some background, we have an in-house library to calculate payment distribution for our ecommerce application. Previously, the library was hardcoded to always round to two decimal places since we only supported these types of currencies. However, now we are adding support for JPY into our application. I made changes to the payment library to accept an option with the number of decimal places should be used for rounding. However, I am a bit confused which field should be used to grab the correct value in the application before calling out to the library.

By just looking at the output of Money.Currency.currency_for_code/1 for "JPY" or "USD" for our use case it has to be either cash_digits, digits or iso_digits. I'm currently using iso_digits to determine the number of decimal places to round with. Is this correct?

kipcole9 commented 4 years ago

Thanks for the question, I will update the docs to explain better. For now I hope this helps, definitely keep asking questions if you have any.

Digits (currency.digits)

The precision of the currency is defined in CLDR and with few exceptions is the same as the precision defined by ISO4217. There are some cases where CLDR and ISO disagree. The disagreement is usually where common practise in a country is different from the formal definition. Therefore in typical usage CLDR is to be preferred (ie use currency.digits). Where you know that you need the formal standard, use currency.iso_digits.

You can find which currencies have digits different from iso_digits with the following snippet:

iex> Cldr.Currency.currencies_for_locale!("en", MyApp.Cldr) 
...  |> Enum.filter(fn {k, v} -> v.digits != v.iso_digits && !is_nil(v.iso_digits) end)

One example is the Lebanese Pound where you can see that digits is 0 suggesting that in common use, decimal digits are not used. iso_digits is 2 noting that the standard has a precision of two digits.

%Cldr.Currency{
  alt_code: nil,
  cash_digits: 0,
  cash_rounding: 0,
  code: "LBP",
  count: %{one: "Lebanese pound", other: "Lebanese pounds"},
  digits: 0,
  from: nil,
  iso_digits: 2,
  name: "Lebanese Pound",
  narrow_symbol: "L£",
  rounding: 0,
  symbol: "LBP",
  tender: true,
  to: nil
}

Cash Rounding (currency.cash_rounding)

Some currencies apply "round to nearest". Canadian Dollar and Swiss Franc are two well known examples. Both of these currencies, in their cash form, have no coin or note below 5 cents or 5 centimes. Therefore when dealing with cash amounts, they must always be rounded to the cash_rounding amount. The following snippet will tell you which currencies this applies to:

iex> Cldr.Currency.currencies_for_locale!("en", MyApp.Cldr) 
...  |> Enum.filter(fn {k, v} -> v.cash_rounding > 0 end)

Rounding (currency.rounding)

Same intent as currency.cash_rounding but applied to non-cash amounts. Today there are no currencies which apply rounding in this way.

Use with the Ex_Money library

ex_money manages all of this for you. Just call Money.round/2 and it will take care of rounding and has options to decide which digits to use and what usage. Using your requirement for JPY for example:

iex> Money.round Money.new("123.7456", :JPY)
#Money<:JPY, 124>

Money.to_string/2 will also round prior to formatting and will also format the appropriate number of decimal digits.

nickdichev commented 4 years ago

Awesome! Thank you so much, that makes a lot of sense. Thanks again for all your hard work on your libraries; we're you're biggest fans at Peek!

kipcole9 commented 4 years ago

Thanks for the feedback, it definitely helps with motivation :-)

Just one more note on rounding if you are doing the rounding yourself:

  1. First round to the number of digits. Use :half_even rounding unless you have some specific requirement. This is the standard rounding method for financial transactions.

  2. Then round to the nearest if required (ie cash_rounding > 0)

  3. Rounding should only be performed on output. Precision should be preserved during all mathematical transformations. Round only at the last minute when presenting a money amount to a user, or when required prior to an API call.