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"

Why Minting?

Minting
Precision Rational-based, zero floating-point error
Performance 2Γ— faster (10Γ—+ formatting)
Ruby support 3.3+ (including Ruby 4.0)
Rails Dedicated companion gem
Code quality 100% coverage, 93/100 RubyCritic

🎯 Exact precision

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

⚑ Blazing performance

Minting is 2Γ— 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 you can trust

  • 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].

API notes

Exact amounts β€” Amounts are stored as Rational and rounded to the currency subunit.

Refinements β€” 10.dollars and similar helpers require using Mint in the current scope (see Usage above).

Division β€” money / 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.

Registered currencies β€” Mint.register_currency. Only registered currency 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!

After opting in:

price = Money.create(10, "USD")     # equivalent to Mint::Money.create
tax   = Money.money(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