Ex_money - money with currency type

ex_money is a package to manage a money data type and provide localised formatting, arithmetic, exchange rates, basic financial calculations and serialization with a special attention on preserving precision.

CLDR data drives the localisation capabilities. As a result ex_money knows about each currencies precision, separators, grouping and symbols in any of about 500 locales.

Version 3.2.4 is out this week with a new ability to parse money strings. Some examples follow:

  # These are the strings available for a given currency
  # and locale that are recognised during parsing
  iex> Cldr.Currency.strings_for_currency :AUD, "de"
  ["aud", "au$", "australischer dollar", "australische dollar"]

  # Parsing can be localised
  iex> Money.parse "12 346 dollar australien", locale: "fr"
  #Money<:AUD, 12346>

  iex> Money.parse "A$ 12346", locale: "en"
  #Money<:AUD, 12346>

  # Note that the decimal separator in the "de" locale
  # is a `.`
  iex> Money.parse "AU$ 12346,45", locale: "de"
  #Money<:AUD, 12346.45>

  # Round trip formatting is supported
  iex> {:ok, string} = Cldr.Number.to_string 1234, Money.Cldr, currency: :AUD
  {:ok, "A$1,234.00"}
  iex> Money.parse string
  #Money<:AUD, 1234.00>

  # Fuzzy matching is possible
  iex> Money.parse("100 eurosports", fuzzy: 0.8)
  #Money<:EUR, 100>

  iex> Money.parse("100 eurosports", fuzzy: 0.9)
  {:error,
   {Money.Invalid, "Unable to create money from \"eurosports\" and \"100\""}}

  # Eligible currencies can be filtered by type
  iex> Money.parse("100 eurosports", fuzzy: 0.8, currency_filter: [:current, :tender])
  #Money<:EUR, 100>
19 Likes

Love it! Used this just the other week to greatly simplify a project i was working on

1 Like

Using already in a project, documentation is clean.

2 Likes

I think money_sql is a great example of an Ecto custom type working in concert with a DB-side user defined type (in this case a composite type).

https://github.com/kipcole9/money_sql/blob/master/lib/money/ecto/money_ecto_composite_type.ex

4 Likes

ex_money 5.0.0 is released today. The main focus is supporting the new Elixir 1.10 Enum.sort/2 functionality that greatly helps express semantic sorting for structs like money, dates and so on.

Enum.sort/2 example

iex> list = [Money.new(:USD, 100), Money.new(:USD, 200)]
[#Money<:USD, 100>, #Money<:USD, 200>]
iex> Enum.sort list, Money
[#Money<:USD, 100>, #Money<:USD, 200>]
iex> Enum.sort list, {:asc, Money}
[#Money<:USD, 100>, #Money<:USD, 200>]
iex> Enum.sort list, {:desc, Money}
[#Money<:USD, 200>, #Money<:USD, 100>]

Breaking Changes

In order to support the new Enum.sort/2, the Money.compare/2 function needs to return :lt, :gt or :eq. Previous releases of Money.compare/2 return 1, 0 or -1 following the model set by Decimal.

In ex_money version 5.0.0, the returns from Money.compare/2 and Money.cmp/2 are swapped to now align with what Elixir expects.

Users of ex_money will need to replace calls to Money.cmp/2 with calls to Money.compare/2. Similarly, calls to Money.compare/2 should be replaced with calls to Money.cmp/2.

Relationship to the Decimal library

decimal is going through the same transition with its releases 1.9 and 2.0. The changes to ex_money have been tested on all releases of decimal from 1.6 up to 1.9.0-rc.0 and on decimal master branch which is 2.0.0-dev. The plan is that no new releases of ex_money should be required to support any modern decimal release.

Links

6 Likes

Do you still suggest using ex_money over money (https://github.com/elixirmoney/money)? If so, why?

I tried both almost a year ago and ex_money resulted to be way more precise for my needs (thanks to decimal). Not sure how they compare today but my suggestion is that you try some arithmetic operations on both.

Update: If I remember correctly, it had to do with the precision I think money had fixed decimals per currency while ex_money didn’t had those limitations. I could be very wrong though.

1 Like

ex_money version 5.1.0 is out today with a primary focus on parsing strings that have a money amount and a currency. In addition, exchange rate HTTPS requests now properly verify the certificate.

In prior versions, parsing a string that had no currency detected would error:

iex> Money.parse("100")
{:error, {Money.Invalid,
  "A currency code, symbol or description must be specified but was not found in \"100\""}}

In this version, the locale of the current process or the provided locale will be used to derive the currency in use for that locale if no currency is detected in the string. For example:

# Specifying a locale will derive the
# currency if none if detected in the
# string
iex> Money.parse("100", locale: "en")
#Money<:USD, 100>

# If no `:locale` is specified the locale of the current
# process is used which should simplify a lot
# of workflows
iex> Money.parse("100")
#Money<:USD, 100>

iex> Money.parse("100", locale: "en-AU")
#Money<:AUD, 100>

iex> assert Money.parse("100", locale: "de")
#Money<:EUR, 100>

# Traditional Chinese in HK implies HK dollars
hex> Money.parse("100", locale: "zh-Hant-hk")                                                                  
#Money<:HKD, 100>

# Traditional Chinese implies New Taiwan dollars
iex> Money.parse("100", locale: "zh-Hant")   
#Money<:TWD, 100>

# Simplified Chinese implies Chinese RMB
iex> Money.parse("100", locale: "zh-Hans")
#Money<:CNH, 100>

# of course detecting the currency in the
# string still applies
iex> Money.parse("12346.45 Australian dollars")
#Money<:AUD, 12346.45>

# Using the locale to derive the currency can be
# disabled by setting `default_currency: false`
# ie the actual `false`, not `nil`
iex> Money.parse("100", default_currency: false) ==
{:error, {Money.Invalid,
  "A currency code, symbol or description must be specified but was not found in \"100\""}}

Bonus content

If you got this far, then thanks! Here’s how the regional override and currency in a more complex language tag interacts with currency parsing:

# A locale that has a regional override. The regional override
# takes precedence and hence the currency is USD
iex> Money.parse("100", locale: "zh-Hans-u-rg-uszzzz")    
#Money<:USD, 100>

# A locale that has a regional override and a currency
# override uses the currency override as precedent over
# the regional override. In this case, EUR
iex> Money.parse("100", locale: "zh-Hans-u-rg-uszzzz-cu-eur") 
#Money<:EUR, 100>
3 Likes

Thanks to some prompting from @ayrat555, I did some further research into supporting crypto currencies in ex_money.

In September, ISO 24165 was introduced to standardise the registration and unique identification of digital tokens (aka “crypto currencies”). This significantly increases the ability to integrate crypto into ex_money as a first-class citizen along side ISO 4217 currencies.

The first step is to make the DTIF registry data available in Elixir. This is now done with the initial release of the library digital_token. The repo is at GitHub - kipcole9/digital_token: Elixir integration for ISO 24165 Digital Tokens through the DTIF registry data.

Next step is to integrate digital_token into ex_money without compromising any of the guiding principles. Digital tokens don’t have standard definitions of digits of precision or rounding rules for example. And there is no CLDR translation data available for them either.

As always, comments, suggestions, bug reports and PRs are most welcome.

3 Likes

ex_money version 5.11.0 has been published. This release adds support for ISO 24165 digital tokens (crypto currencies. The changelog entry is:

Money Enhancements

  • Adds support for ISO 24165 Digital Tokens (crypto currency). Digital Token-based money behaves the same as currency-based money with the following exceptions due to limited data availability:

    • Digital token names are not localized (there is no localised data available in CLDR)
    • Digital token names are not pluralized (also because there is no localised data available)
    • Digital token amounts are never rounded (there is no data available to standardise on rounding rules or the number of fractional digits to round to)

ex_money version 2.11.0 depends on ex_cldr_numbers 2.27.0 that provides the support for formatting numbers and currencies. The changelog entry is:

Cldr Numbers Enhancements

  • Add support for formatting numbers representing ISO 24165 Digital Tokens (aka crypto currencies). The behaviour follows that for currency formatting. Given that the digital token registry does not contain fraction precision data or pluralised or localised token names, the formatting of digital tokens amounts is not localized beyond formatting the number itself.
3 Likes