Minting

Fast, precise, and developer-friendly money handling for Ruby.

Gem Version CI Test Coverage RubyCritic Documentation

Quick start

require 'minting'

price = Mint.money(19.99, 'USD')       #=> [USD 19.99]
tax   = price * 0.08                   #=> [USD 1.60]
total = price + tax                    #=> [USD 21.59]

total.to_s                             #=> "$21.59"
total.currency_code                    #=> "USD"

Exact precision

Amounts are stored as Rational and rounded to the currency subunit. No floating-point surprises, ever.

Blazing performance

Minting is faster than the Money gem for everyday operations and over 10× faster for formatting. See full benchmarks in the Performance Guide.

Clean, modern API

Intuitive interface, descriptive error messages, and sensible defaults. Works the way you expect.

Rails-ready

Use with the minting-rails companion gem for drop-in ActiveRecord type casting, validators, and form helpers.

Quality code

  • 100% test coverage — every line exercised
  • 93/100 RubyCritic score — clean, maintainable code
  • CI-tested on Ruby 3.3 and 4.0

Installation

bundle add minting

Or add to your Gemfile:

gem 'minting'

Usage

require 'minting'

# Create money
ten = Mint.money(10, 'USD')            #=> [USD 10.00]

# Create money using Numeric refinements
using Mint

1.dollar == Mint.money(1, 'USD') #=> true
ten = 10.dollars                 #=> [USD 10.00]
4.to_money('USD')                #=> [USD 4.00]

# Comparisons
ten == 10.dollars                #=> true
ten == Mint.money(10, 'EUR')     #=> false
ten > Mint.money(9.99, 'USD')    #=> true

# Zero equality semantics
# Any zero amount is treated as equal, regardless of currency
Mint.money(0, 'USD') == Mint.money(0, 'EUR')   #=> true
Mint.money(0, 'USD') == 0                      #=> true
Mint.money(0, 'USD') == 0.0                    #=> true

# Non-zero numerics are not equal to Money objects
Mint.money(10, 'USD') == 10                    #=> false

# Format (uses Kernel.format syntax)
price = Mint.money(9.99, 'USD')

price.to_s                                  #=> "$9.99",
price.to_s(format: '%<amount>d')            #=> "9",
price.to_s(format: '%<symbol>s%<amount>f')  #=> "$9.99",
price.to_s(format: '%<symbol>s%<amount>+f') #=> "$+9.99",
(-price).to_s(format: '%<amount>f')         #=> "-9.99",

# Format with padding
price_in_euros = Mint.money(12.34, 'EUR')

price.to_s(format: '--%<amount>7d')               #=> "--      9"
price.to_s(format: '  %<amount>10f %<currency>s') #=> "        9.99 USD"
(-price).to_s(format: '  %<amount>10f')           #=> "       -9.99"

price_in_euros.to_s(format: '%<symbol>2s%<amount>+10f')    #=> " €    +12.34"

# Per-sign Hash format (e.g. accounting parentheses for losses)
loss = Mint.money(-1234.56, 'USD')
loss.to_s(format: { negative: '(%<symbol>s%<amount>f)' })  #=> "($1,234.56)"
Mint.money(0, 'BRL').to_s(format: { zero: '--' })          #=> "--"
# All three keys at once:
fmt = { positive: '%<symbol>s%<amount>f', negative: '(%<symbol>s%<amount>f)', zero: '--' }
Mint.money(1234.56, 'USD').to_s(format: fmt)               #=> "$1,234.56"

# Json serialization

price.to_json # => "{\"currency\": \"USD\", \"amount\": \"9.99\"}"

# Hash conversion

price.to_hash #=> {currency: "USD", amount: "9.99"}


# Fractional units (inverse of #fractional) - exact integer arithmetic

price.fractional                        #=> 999
Mint::Money.from_fractional(999, 'USD') #=> [USD 9.99]
Mint::Money.from_fractional(1234, 'JPY') #=> [JPY 1234]  # subunit 0 -> no scaling


# Proportional allocation and split

ten.split(3)                           #=> [[USD 3.34], [USD 3.33], [USD 3.33]]
ten.allocate([1, 2, 3])                #=> [[USD 1.67], [USD 3.33], [USD 5.00]]

# Clamping to a range

price = Mint.money(50, 'USD')
min_price = Mint.money(75, 'USD')

price.clamp(0, 100)                    #=> [USD 50.00]  (returns self, no new object)
price.clamp(0, 25)                     #=> [USD 25.00]  (clamped to max)
price.clamp(min_price, 100)                   #=> [USD 75.00]  (clamped to min)

# Clamp accepts Money bounds or Numeric amounts
price.clamp(min_price, 100) #=> [USD 75.00]

# Ranges and enumeration are supported

1.dollar..10.dollars                      #=> [USD 1.00]..[USD 10.00]
(1.dollar..3.dollars).step(1.dollar).to_a #=> [[USD 1.00], [USD 2.00], [USD 3.00]]

Parsing strings

Mint.parse('$19.99')           #=> [USD 19.99]
Mint.parse('19,99 €')          #=> [EUR 19.99]
Mint.parse('1.234,56', 'EUR')  #=> [EUR 1234.56]
Mint.parse('USD 1,234.56')     #=> [USD 1234.56]

Notes:

  • Pass a currency code when the string has no symbol or code.
  • 1,234 means 1234, not 1.234 and 1,23 means 1.23, not 123
  • 1,234.00 is unambiguous (thousands + decimal).
  • Accounting negatives like ($1.23) are unsupported for now.
  • Ambiguous symbols like $ resolve by currency priority (currently USD).
  • The parser scans all uppercase words for registered codes, so spurious non-currency words before the real code are correctly ignored: Mint.parse("MAX 10.00 USD") yields [USD 10.00].

Currency lookup

# By ISO code (direct hash lookup, string only)
Mint::Currency.for_code('USD')        #=> #<Currency code="USD" ...>

# By display symbol (highest-priority currency for ambiguous symbols)
Mint::Currency.for_symbol('$')        #=> #<Currency code="USD" ...>
Mint::Currency.for_symbol('R$')       #=> #<Currency code="BRL" ...>
Mint::Currency.for_symbol('')        #=> #<Currency code="EUR" ...>

API notes

Exact amounts — Amounts are stored as Rational and rounded to the currency subunit.

Rounding modes — Wrap operations in Mint.with_rounding(mode) to change how amounts are rounded to the subunit:

Mint.with_rounding(:half_down) { Mint.money(1.005, 'USD') }  #=> [USD 1.00]
Mint.with_rounding(:ceil)      { Mint.money(1.001, 'USD') }  #=> [USD 1.01]
Mint.with_rounding(:floor)     { Mint.parse('1.009', 'USD') } #=> [USD 1.00]

Modes: :half_up (default), :half_down, :floor, :ceil, :truncate, :down. Applies to construction, parsing, change, split, and allocate. Restores the previous mode when the block exits, even on exception.

Refinements10.dollars and similar helpers require using Mint in the current scope (see Usage above).

Divisionmoney / 5 returns new Money; money / other_money returns a numeric ratio, not money.

Zero equality — Any zero amount is considered equal across currencies and to numeric zero (Mint.money(0, 'USD') == Mint.money(0, 'EUR') is intentionally true). Non-zero amounts must match currency and value.

Zero helperCurrency.zero('USD') returns a frozen zero-Money, useful as a default value for discounts, totals, or counters.

Registered currenciesCurrency.register(code:, subunit:, symbol:, priority:) adds custom currencies. Only registered codes and symbols are recognized by the parser.

Built-in currencies — 150+ ISO-4217 world currencies ship in lib/minting/data/currencies.yaml and load when the registry is first accessed.

Optional top-level Money and Currency

By default, Minting keeps everything namespaced under Mint to coexist nicely with other gems. If you prefer shorter constants, opt in:

require "minting"
require "minting/dsl"  # opt‑in top‑level Money / Currency

Or at runtime:

Minting.use_top_level_constants!

For Rails applications, you can enable the top-level constants in an initializer:

# config/initializers/minting.rb
require "minting/dsl"

After opting in:

price = Money.from(10, "USD")                   # equivalent to Mint::Money.from
tax   = Money.from(2.50, "USD")
cur   = Currency.new(code: "EUR", symbol: "", subunit: 2, priority: 0)

Good fit: Application code, especially Rails apps. Not recommended: Reusable gems/libraries — stick to Mint::Money to avoid conflicts.

Roadmap

  • Localization (I18n-aware formatting)
  • Exchange-rate conversion infrastructure

License

MIT