money v1.3.0

Crystal shard for dealing with money and currency conversion

money CI Releases License

Hey there! 👋 Welcome to money, a Crystal shard for handling money and currency conversion, inspired by RubyMoney.

Why Use This Library?

Here’s what you get out of the box:

  • A Money class to represent amounts and their currencies.
  • A flexible Money::Currency class for all your currency info needs.
  • A growing list of 200+ supported currencies (metals, fiat, cryptocurrencies).
  • BigDecimal-based values—no more floating point rounding headaches!
  • Easy APIs for currency exchange.
  • Multiple exchange rate providers (use built-in or roll your own).
  • Formatting and parsing money values.
  • Rounding and truncation helpers.
  • JSON/YAML serialization and deserialization support.

Installation

Add this to your application's shard.yml:

dependencies:
  money:
    github: crystal-money/money

Then run:

shards install

And require it in your project:

require "money"

[!TIP] If you wish to use YAML serialization, remember to require "yaml" before requiring money.

Quick Examples

Creating Money

[!NOTE] Money.new first positional argument will treat the given value as the fractional if it's an integer, and the amount otherwise.

money = Money.new(10_00, "USD")
money.to_s          # => "$10.00"
money.amount        # => 10.0
money.fractional    # => 1000.0
money.currency.code # => "USD"

From fractional amount

Money.new(fractional: 10_00.0, currency: "USD")
Money.new(fractional: 10_00, currency: "USD")
Money.new(10_00, "USD")

Money.from_fractional(10_00.0, "USD")
Money.from_fractional(10_00, "USD")

From whole amount

Money.new(amount: 10.0, currency: "USD")
Money.new(amount: 10, currency: "USD")
Money.new(10.0, "USD")

Money.from_amount(10.0, "USD")
Money.from_amount(10, "USD")

Comparing Money

[!NOTE] Performs currency conversion if necessary.

Money.new(11_00, "USD") == Money.new(11_00, "USD") # => true
Money.new(11_00, "USD") == Money.new(11_00, "EUR") # => false

Money.new(11_00, "USD") < Money.new(33_00, "USD") # => true
Money.new(11_00, "USD") > Money.new(33_00, "USD") # => false

[!CAUTION] Two Money objects with 0 amount are considered equal, regardless of their currency.

Money.zero("USD") == Money.zero("EUR") # => true

Arithmetic

[!NOTE] Performs currency conversion if necessary.

Money.new(10_00, "USD") + Money.new(5_00, "USD") # => Money(@amount=15.00, @currency="USD")
Money.new(22_00, "USD") - Money.new(2_00, "USD") # => Money(@amount=20.00, @currency="USD")
Money.new(22_00, "USD") / 2                      # => Money(@amount=11.00, @currency="USD")
Money.new(11_00, "USD") * 5                      # => Money(@amount=55.00, @currency="USD")

Unit/Subunit Conversions

Money.from_amount(5, "USD").fractional # => 500.0
Money.from_amount(5, "JPY").fractional # => 5.0
Money.from_amount(5, "TND").fractional # => 5000.0

Currency Conversion

In order to perform currency exchange, you need to set up a Money::Currency::Exchange::RateProvider or add the rates manually:

Money.default_exchange.rate_store["USD", "EUR"] = 1.24515
Money.default_exchange.rate_store["EUR", "USD"] = 0.803115

Then you can perform the exchange:

Money.new(1_00, "USD").exchange_to("EUR") # => Money(@amount=1.24, @currency="EUR")
Money.new(1_00, "EUR").exchange_to("USD") # => Money(@amount=0.8, @currency="USD")

Comparison and arithmetic operations work as expected:

Money.new(10_00, "EUR") == Money.new(10_00, "USD") # => false
Money.new(10_00, "EUR") + Money.new(10_00, "USD")  # => Money(@amount=22.45, @currency="EUR")

Formatting

Money.new(1_00, "USD").format # => "$1.00"
Money.new(1_00, "GBP").format # => "£1.00"
Money.new(1_00, "EUR").format # => "€1.00"

Money Ranges

range = Money.new(1_00, "USD")..Money.new(3_00, "USD")
range.to_a(&.to_s)
# => ["$1.00", "$1.01", "$1.02", ..., "$2.99", "$3.00"]

Steppable Ranges

range = Money.new(1_00, "USD")..Money.new(3_00, "USD")
range
  .step(by: Money.new(1_00, "USD"))
  .to_a(&.to_s)
# => ["$1.00", "$2.00", "$3.00"]

Infinite Precision

By default, Money objects are rounded to the nearest cent and the extra precision is not preserved:

Money.new(2.34567, "USD").to_s # => "$2.35"

If you want to keep all the digits, you can enable infinite precision globally:

Money.infinite_precision = true
Money.new(2.34567, "USD").to_s # => "$2.34567"

Or use the block-scoped Money.with_infinite_precision:

Money.with_infinite_precision do
  Money.new(2.34567, "USD").to_s # => "$2.34567"
end

Currencies

A Money::Currency instance holds all the info about the currency:

currency = Money::Currency.find("USD")
currency.code   # => "USD"
currency.name   # => "United States Dollar"
currency.symbol # => "$"
currency.fiat?  # => true

Most APIs let you use a String, Symbol, or a Money::Currency:

# All of the following are equivalent:

Money.default_currency = Money::Currency.find("CAD")
Money.default_currency = "CAD"
Money.default_currency = :cad

Currency Lookup

Money::Currency.find and Money::Currency.[] methods let you find a currency by its code:

Money::Currency.find("USD") # => #<Money::Currency @code="USD">
Money::Currency[:usd]       # => #<Money::Currency @code="USD">
Money::Currency[:foo]       # raises Money::UnknownCurrencyError

There are also Money::Currency.find? and Money::Currency.[]? non-raising methods:

Money::Currency.find?("USD") # => #<Money::Currency @code="USD">
Money::Currency[:usd]?       # => #<Money::Currency @code="USD">
Money::Currency[:foo]?       # => nil

Currency Enumeration

[!TIP] Money::Currency class implements Enumerable module, so you can use all of its methods like each, map, find, select, etc.

For example, to find a currency by ISO 4217 numeric code:

Money::Currency.find(&.iso_numeric.==(978)) # => #<Money::Currency @code="EUR">

Or to select all the ISO currencies:

Money::Currency.select(&.iso?)
# => [#<Money::Currency @code="USD">, #<Money::Currency @code="EUR">, ...]

In addition, there are Money::Currency.metal, Money::Currency.fiat and Money::Currency.crypto methods to get all the currencies of a particular type:

Money::Currency.metal
# => [#<Money::Currency @code="XAG">, #<Money::Currency @code="XAU">, ...]
Money::Currency.fiat
# => [#<Money::Currency @code="USD">, #<Money::Currency @code="EUR">, ...]
Money::Currency.crypto
# => [#<Money::Currency @code="BTC">, #<Money::Currency @code="ETH">, ...]

# or
Money::Currency.reject(&.metal?)
# => [#<Money::Currency @code="USD">, #<Money::Currency @code="EUR">, ...]

To return an array of registered currencies (ordered by their priority), call Money::Currency.all or .to_a:

Money::Currency.all # => [#<Money::Currency @code="USD">, #<Money::Currency @code="EUR">, ...]

Registering a New Currency

currency = Money::Currency.new(
  type:                :fiat,
  priority:            1,
  code:                "USD",
  iso_numeric:         840,
  name:                "United States Dollar",
  symbol:              "$",
  symbol_first:        true,
  subunit:             "Cent",
  subunit_to_unit:     100,
  decimal_mark:        ".",
  thousands_separator: ","
)

Money::Currency.register(currency)

Currency Attributes

  • :type — a Money::Currency::Type - either Metal, Fiat or Crypto
  • :priority — a numerical value you can use to sort/group the currency list
  • :code — the international 3-letter code as defined by the ISO 4217 standard
  • :iso_numeric — the international 3-digit code as defined by the ISO 4217 standard
  • :name — the currency name
  • :symbol — the currency symbol (UTF-8 encoded)
  • :symbol_first — whether a money symbol should go before the amount
  • :subunit — the name of the fractional monetary unit
  • :subunit_to_unit — the proportion between the unit and the subunit
  • :decimal_mark — character between the whole and fraction amounts
  • :thousands_separator — character between each thousands place

All attributes except :code and :subunit_to_unit are optional.

Priority

You can use the priority attribute to sort or group currencies:

# Returns an array of currencies where priority is less than 10
def major_currencies(currencies)
  currencies.take_while(&.priority.try(&.<(10)))
end

major_currencies(Money::Currency)
# => [#<Money::Currency @code="USD">, #<Money::Currency @code="EUR">, ...]

Default Currency

By default, Money does not have a default currency. You can set it like so:

Money.default_currency = :xag

Currency Exponent

The exponent of a money value is the number of digits after the decimal separator (which separates the major unit from the minor unit). See e.g. ISO 4217 for more information.

Money::Currency.find("USD").exponent # => 2
Money::Currency.find("JPY").exponent # => 0
Money::Currency.find("MGA").exponent # => 1

Currency Exchange

Exchanging money is performed through a Money::Currency::Exchange object. This is done by fetching the exchange rate from a #rate_store first. If the rate is not available (or stale), it is then fetched from a #rate_provider.

The default Money::Currency::Exchange object uses Memory rate store in conjunction with Null rate provider, which requires one to manually specify the exchange rate.

Here's an example of how it works:

Money.default_exchange.rate_store["USD", "EUR"] = 1.24515
Money.default_exchange.rate_store["EUR", "USD"] = 0.803115

Money.new(1_00, "USD").exchange_to("EUR") # => Money(@amount=1.24, @currency="EUR")
Money.new(1_00, "EUR").exchange_to("USD") # => Money(@amount=0.8, @currency="USD")

Exchange Rate Stores

The default exchange uses an in-memory store:

Money.default_exchange = Money::Currency::Exchange.new(
  rate_store: Money::Currency::RateStore::Memory.new
)

Rate stores can be configured with Time::Span controlling the time-to-live (TTL) of the exchange rates:

Money.default_exchange = Money::Currency::Exchange.new(
  rate_store: Money::Currency::RateStore::Memory.new(ttl: 1.hour)
)

Or use your own store (database, file, cache, etc):

Money.default_exchange.rate_store = MyCustomStore.new

The store can be used directly:

# Add to the underlying store
Money.default_exchange.rate_store["USD", "CAD"] = 0.9

# Retrieve from the underlying store
Money.default_exchange.rate_store["USD", "CAD"] # => 0.9

As long as the store holds the exchange rates, Money will use them.

Money.new(10_00, "USD").exchange_to("CAD")        # => Money(@amount=9.0 @currency="CAD")
Money.new(10_00, "CAD") + Money.new(10_00, "USD") # => Money(@amount=19.0 @currency="CAD")

Exchange Rate Providers

By default, the exchange uses a Null provider, which returns nil for all rates.

Money.default_exchange = Money::Currency::Exchange.new(
  rate_provider: Money::Currency::RateProvider::Null.new
)

There are multiple providers available under the Money::Currency::RateProvider namespace which can be used OOTB to fetch exchange rates from different sources.

You can choose one of them, roll your own, or combine them with the Compound provider:

Money.default_exchange.rate_provider =
  Money::Currency::RateProvider::Compound.new([
    Money::Currency::RateProvider::ECB.new,
    Money::Currency::RateProvider::FloatRates.new,
    Money::Currency::RateProvider::UniRateAPI.new(
      api_key: "valid-api-key"
    ),
  ])

[!TIP] Compound rate provider takes an array of Money::Currency::RateProvider instances which are used in order to fetch the exchange rate.

Disabling Currency Conversion

If you want to prevent automatic currency conversion:

Money.disallow_currency_conversion!

Rounding

By default, Money rounds to the nearest cent:

Money.new(2.34567, "USD").to_s # => "$2.35"

You can change the rounding precision:

Money.new(2.34567, "USD").round(1).to_s # => "$2.30"

You can change the rounding mode:

Money.new(2.34567, "USD").round(1, :to_positive).to_s # => "$2.40"
Money.new(2.34567, "USD").round(1, :to_negative).to_s # => "$2.30"

To keep extra digits, enable infinite precision:

Money.infinite_precision = true

Money.new(2.34567, "USD").to_s                    # => "$2.34567"
Money.new(2.34567, "USD").round(4).to_s           # => "$2.3457"
Money.new(2.34567, "USD").round(4, :to_zero).to_s # => "$2.3456"

# or

Money.with_rounding_mode(:to_zero) do
  Money.new(2.34567, "USD").round(4).to_s         # => "$2.3456"
end

Nearest Cash Value

If you want to round to the nearest cash value, use Money#round_to_nearest_cash_value:

Money.new(10_07, "CHF").round_to_nearest_cash_value
# => Money(@amount=10.05, @currency="CHF")

Money.new(10_08, "CHF").round_to_nearest_cash_value
# => Money(@amount=10.1, @currency="CHF")

JSON/YAML Serialization

Money, Money::Currency, Money::Currency::Rate and Money::Currency::RateProvider implements JSON::Serializable and YAML::Serializable:

Money

Money.new(10_00, "USD").to_json # => "{\"amount\":10.0,\"currency\":\"USD\"}"
Money.new(10_00, "USD").to_yaml # => "---\namount: 10.0\ncurrency: USD\n"

Money.from_json(%({"amount": 10.0, "currency": "USD"}))
# => Money(@amount=10.0, @currency="USD")

Money.from_yaml("{ amount: 10.0, currency: USD }")
# => Money(@amount=10.0, @currency="USD")

Money::Currency

# Serialize existing `Money::Currency`

Money::Currency.find("USD").to_json # => "{\"code\":\"USD\", ...}"
Money::Currency.find("USD").to_yaml # => "---\ncode: USD\n ..."

# Instantiate new `Money::Currency`

Money::Currency.from_json(%({"code": "FOO", ...})) # => #<Money::Currency @code="FOO">
Money::Currency.from_yaml("{ code: FOO, ... }")    # => #<Money::Currency @code="FOO">

# Lookup existing `Money::Currency`

Money::Currency.from_json(%("USD")) # => #<Money::Currency @code="USD">
Money::Currency.from_yaml("USD")    # => #<Money::Currency @code="USD">

Money::Currency::Rate

rate = Money::Currency::Rate.new(
  Money::Currency.find("USD"),
  Money::Currency.find("EUR"),
  1.25.to_big_d,
  Time.parse_utc("2025-05-22", "%F"),
)

rate.to_json # => "{\"base\":\"USD\",\"target\":\"EUR\",\"value\":1.25,\"updated_at\":\"2025-05-22T00:00:00.000Z\"}"
rate.to_yaml # => "---\nbase: USD\ntarget: EUR\nvalue: 1.25\nupdated_at: 2025-05-22\n"

Money::Currency::RateProvider

You can use .from_json and .from_yaml methods to deserialize generic rate provider instances providing the name (in CamelCase or snake_case) and options - optional hash that's being passed to the provider initializer.

provider = Money::Currency::RateProvider.from_yaml <<-YAML
  name: Compound
  options:
    providers:
    - name: ECB
    - name: FloatRates
    - name: UniRateAPI
      options:
        api_key: valid-api-key
  YAML

provider.class # => Money::Currency::RateProvider::Compound

For specific providers you pass the options directly:

compound_provider = Money::Currency::RateProvider::Compound.from_yaml <<-YAML
  providers:
  - name: ECB
  - name: FloatRates
  YAML

compound_provider.providers << Money::Currency::RateProvider::UniRateAPI.from_yaml <<-YAML
  api_key: valid-api-key
  YAML

compound_provider.providers.size # => 3

Using with JSON::Serializable and YAML::Serializable

In order to (de)serialize generic Money::Currency::RateProvider instances, you need to add a JSON/YAML::Field annotation with a custom converter — Money::Currency::RateProvider::Converter.

class FooWithGenericProvider
  include JSON::Serializable
  include YAML::Serializable

  @[JSON::Field(converter: Money::Currency::RateProvider::Converter)]
  @[YAML::Field(converter: Money::Currency::RateProvider::Converter)]
  property provider : Money::Currency::RateProvider

  def initialize(@provider)
  end
end

foo = FooWithGenericProvider.from_yaml <<-YAML
  provider:
    name: Compound
    options:
      providers:
      - name: ECB
      - name: FloatRates
      - name: UniRateAPI
        options:
          api_key: valid-api-key
  YAML

foo.provider.class # => Money::Currency::RateProvider::Compound

Working with Fibers

Global settings are being kept in a single, fiber-local Money.context object, and are not shared between fibers by default.

Use this to spawn a fiber with the same settings as the current one:

Money.default_currency = "EUR"

Money.spawn_with_same_context do
  Money.default_currency.code # => "EUR"
end

All of the Money APIs and classes are (or at least should be) fiber-safe.

[!CAUTION] Money.spawn_with_same_context duplicates the Money.context instance, by calling #dup on it and thus only the values are being duplicated, references are shared.

Formatting

There are several formatting rules for when Money#format is called. For more info, check out the formatting module source, or the docs.

If you want to format money according to the EU's Rules for expressing monetary units:

money = Money.new(1_23, "GBP")                       # => Money(@amount=1.23 @currency="GBP")
money.format(format: "%{currency} %{sign}%{amount}") # => "GBP 1.23"

Parsing

You can parse a string with an amount and currency code or symbol:

Money.parse("$12.34")    # => Money(@amount=12.34, @currency="USD")
Money.parse("12.34 USD") # => Money(@amount=12.34, @currency="USD")

Contributors

  • Sija Sijawusz Pur Rahnama (creator & maintainer)
Repository

money

Owner
Statistic
  • 39
  • 10
  • 0
  • 3
  • 1
  • about 8 hours ago
  • December 8, 2017
License

MIT License

Links
Synced at

Thu, 31 Jul 2025 02:18:48 GMT

Languages