minting 1.9.7 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +298 -122
- data/Rakefile +2 -7
- data/doc/agents/api_review-2026-06-15.md +1 -1
- data/doc/agents/copilot-instructions.md +2 -2
- data/doc/agents/expired/copilot-instructions.md +2 -2
- data/doc/agents/expired/gemini_gem_evaluation.md +2 -2
- data/lib/minting/aliases.rb +22 -0
- data/lib/minting/currency/currency.rb +77 -94
- data/lib/minting/currency/registry.rb +134 -0
- data/lib/minting/currency/rounding.rb +47 -0
- data/lib/minting/data/crypto-currencies.yaml +126 -0
- data/lib/minting/mint/i18n.rb +79 -29
- data/lib/minting/mint/mint.rb +1 -26
- data/lib/minting/mint/registry/crypto.rb +59 -0
- data/lib/minting/mint/registry/registration.rb +1 -2
- data/lib/minting/mint/registry/registry.rb +9 -11
- data/lib/minting/mint/registry/symbols.rb +37 -30
- data/lib/minting/mint.rb +3 -2
- data/lib/minting/money/allocation/allocation.rb +2 -2
- data/lib/minting/money/allocation/split.rb +2 -2
- data/lib/minting/money/arithmetics/operators.rb +10 -13
- data/lib/minting/money/clamp.rb +6 -6
- data/lib/minting/money/coercion.rb +1 -1
- data/lib/minting/money/comparable.rb +3 -3
- data/lib/minting/money/constructors.rb +3 -42
- data/lib/minting/money/conversion.rb +22 -18
- data/lib/minting/money/format/format.rb +100 -0
- data/lib/minting/money/format/formatter.rb +110 -0
- data/lib/minting/money/format/to_s.rb +20 -102
- data/lib/minting/money/format/validator.rb +34 -0
- data/lib/minting/money/money.rb +25 -9
- data/lib/minting/money/parse.rb +127 -0
- data/lib/minting/money/rounding.rb +26 -0
- data/lib/minting/version.rb +1 -1
- data/lib/minting.rb +17 -8
- metadata +12 -31
- data/doc/Mint/Currency.html +0 -2032
- data/doc/Mint/Money.html +0 -5139
- data/doc/Mint/RangeStepPatch.html +0 -277
- data/doc/Mint/Registry.html +0 -863
- data/doc/Mint/Rounding.html +0 -506
- data/doc/Mint/UnknownCurrency.html +0 -138
- data/doc/Mint.html +0 -931
- data/doc/Minting.html +0 -142
- data/doc/Numeric.html +0 -479
- data/doc/String.html +0 -241
- data/doc/_index.html +0 -206
- data/doc/class_list.html +0 -54
- data/doc/css/common.css +0 -1
- data/doc/css/full_list.css +0 -206
- data/doc/css/style.css +0 -1089
- data/doc/file.README.html +0 -291
- data/doc/file_list.html +0 -59
- data/doc/frames.html +0 -22
- data/doc/index.html +0 -291
- data/doc/js/app.js +0 -801
- data/doc/js/full_list.js +0 -334
- data/doc/js/jquery.js +0 -4
- data/doc/method_list.html +0 -758
- data/doc/top-level-namespace.html +0 -135
- data/lib/minting/mint/aliases.rb +0 -16
- data/lib/minting/mint/parser/parser.rb +0 -97
- data/lib/minting/mint/parser/separators.rb +0 -41
- data/lib/minting/mint/rounding.rb +0 -65
- data/lib/minting/money/format/formatting.rb +0 -130
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 851c39a123584b8a782ef0c610c5e271239e0f20b948963bf926daf11defa269
|
|
4
|
+
data.tar.gz: 9d9c5aab7a6acc499308b2850157f6e8d4eef8ad01f4b0db4aa75e89067162fb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d6cca508a6ec6690436d67f70720db03bcebabeb7b561a4769aca306d9350f1d40ba07af63500316c236094dca13b7a6c181d49b324729e7439deff05f69513a
|
|
7
|
+
data.tar.gz: 419ce736af2fd00d90834dc6b82e5459ff83873922871a40d89d62dda69603ce31a5456918fa61ba21564489019eba6c80b1589cefce19841410a6e328ee45fa
|
data/README.md
CHANGED
|
@@ -1,41 +1,144 @@
|
|
|
1
|
-
# Minting
|
|
2
|
-
|
|
3
|
-
Fast, precise, and developer-friendly money handling for Ruby.
|
|
4
|
-
|
|
5
1
|
[](https://badge.fury.io/rb/minting)
|
|
6
2
|
[](https://github.com/gferraz/minting/actions/workflows/ci.yml)
|
|
7
|
-
[](https://github.com/gferraz/minting)
|
|
8
4
|
[](https://www.rubydoc.info/gems/minting/frames)
|
|
5
|
+
[](https://github.com/gferraz/minting)
|
|
6
|
+
[](https://github.com/gferraz/minting/commits/main)
|
|
7
|
+
[](https://github.com/gferraz/minting/blob/main/LICENSE)
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
# Minting
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
require 'minting'
|
|
11
|
+
**Fast, precise, and developer-friendly money handling for Ruby.**
|
|
14
12
|
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
> **Status:** Minting 2.1 is released. The core API (`Money`, `Currency`, formatting, parsing) is stable.
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
15
18
|
price = Money.from(19.99, 'USD') #=> [USD 19.99]
|
|
16
19
|
tax = price * 0.08 #=> [USD 1.60]
|
|
17
20
|
total = price + tax #=> [USD 21.59]
|
|
18
21
|
|
|
19
22
|
total.to_s #=> "$21.59"
|
|
20
|
-
total.currency_code #=> "USD"
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
Amounts are stored as `Rational` and rounded to the currency subunit. No floating-point surprises, ever.
|
|
25
|
+
## Quickstart
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
Minting is fast! See full benchmarks in the [Performance Guide](bench/BENCHMARKS.md).
|
|
27
|
+
Get started in 60 seconds:
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
```ruby
|
|
30
|
+
# Add to your Gemfile
|
|
31
|
+
gem "minting"
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
# Or install directly
|
|
34
|
+
# gem install minting
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
# Require the gem
|
|
37
|
+
require "minting"
|
|
38
|
+
|
|
39
|
+
# Create money objects
|
|
40
|
+
price = Money.from(19.99, "USD")
|
|
41
|
+
tax_rate = 0.08
|
|
42
|
+
tax = price * tax_rate
|
|
43
|
+
total = price + tax
|
|
44
|
+
|
|
45
|
+
# Format as a string
|
|
46
|
+
puts total.to_s # => "$21.59"
|
|
47
|
+
|
|
48
|
+
# Parse from a string
|
|
49
|
+
money = Money.parse("$15.99")
|
|
50
|
+
puts money # => "$15.99"
|
|
51
|
+
|
|
52
|
+
# Allocate money proportionally
|
|
53
|
+
total = Money.from(100, "USD")
|
|
54
|
+
shares = total.allocate([1, 2, 3]) # => [16.67, 33.33, 50.00]
|
|
55
|
+
|
|
56
|
+
# Split into N equal parts
|
|
57
|
+
parts = total.split(3) # => [33.34, 33.33, 33.33]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## What's New in 2.1
|
|
61
|
+
|
|
62
|
+
### Breaking Changes
|
|
63
|
+
- Rounding mode symbols renamed to match `Rational#round` `half:` parameter: `:half_up` → `:up`, `:half_down` → `:down`, `:half_even` → `:even`
|
|
64
|
+
|
|
65
|
+
## What's New in 2.0
|
|
66
|
+
|
|
67
|
+
### Breaking Changes
|
|
68
|
+
- `Mint.parse` and `Mint.parse!` removed — use `Money.parse` and `Money.parse!`
|
|
69
|
+
- `Mint.with_rounding` removed — use `Money.with_rounding`
|
|
70
|
+
- `Mint.world_currencies` removed — use `Currency.world_currencies`
|
|
71
|
+
- `Money#mint` removed — use `Money#copy_with`
|
|
72
|
+
- `Money::Currency` is the canonical name to access the `Currency` class
|
|
73
|
+
- `minting/mint/aliases` abbreviated to `minting/aliases`
|
|
74
|
+
- `Money#format` `formatter_class:` kwarg removed — `Formatter` is now the sole formatter implementation
|
|
75
|
+
- `Money#to_json` and `Money.from_json` — moved to `attribute-money` companion gem
|
|
76
|
+
|
|
77
|
+
### New Features
|
|
78
|
+
- **Crypto currency support**: Opt-in YAML-backed definitions for ~25 popular coins (BTC, ETH, SOL, ...). Use `Currency.register_crypto('BTC', 'ETH')` to register, or `Currency.crypto_currencies` to inspect available definitions.
|
|
79
|
+
- `Currency.registered_currencies` — public access to all registered currencies (frozen hash)
|
|
80
|
+
- `Money.from_hash(hash)` — deserializer symmetric with `to_hash`, accepts `{ currency:, amount: }`
|
|
81
|
+
- `Money#integral` — returns the whole-unit part of the amount (complement to `#fractional`). `#to_i` is now an alias of `#integral`.
|
|
82
|
+
- `%<dsymbol>s` format placeholder — uses `currency.disambiguate_symbol` (e.g. "US$", "C$", "A$") when available, falling back to the primary symbol.
|
|
83
|
+
- **Compiled formatting**: Formatting is now compiled into reusable lambdas at the class level — 1.4–2.2x formatting speedup depending on scenario.
|
|
84
|
+
- **Locale-aware formatting**: `locale:` kwarg on `Money#format` / `#to_fs`, supports per-locale decimal/thousand separators and format templates via `Mint.locale_backend`. Works seamlessly with Rails I18n.
|
|
85
|
+
- **Faster startup**: World currencies are now preloaded at gem initialization, eliminating lazy-loading overhead and mutex contention.
|
|
86
|
+
|
|
87
|
+
### Bugfixes
|
|
88
|
+
- `Money#fractional` now returns a signed value matching the amount's sign (previously always positive for negative amounts). The invariant `integral * multiplier + fractional == subunits` now holds for all amounts.
|
|
89
|
+
- `Money#initialize` now calls `.to_r` on the amount, guaranteeing `@amount` is always a `Rational`. Fixes a hash/`eql?` contract violation for zero-subunit currencies and an `ArgumentError` in `Integer#to_d`.
|
|
90
|
+
|
|
91
|
+
### Removed
|
|
92
|
+
- `Money::Formatter2` — removed; `Formatter` is the sole implementation
|
|
93
|
+
- `Money#to_json` and `Money.from_json` — moved to `attribute-money` companion gem
|
|
94
|
+
|
|
95
|
+
Amounts are stored as `Rational`, so there's no floating-point drift — `0.1 + 0.2` problems simply don't happen here, at any scale.
|
|
96
|
+
|
|
97
|
+
## Table of contents
|
|
98
|
+
|
|
99
|
+
- [Quickstart](#quickstart)
|
|
100
|
+
- [Why Minting](#why-minting)
|
|
101
|
+
- [What's New in 2.1](#whats-new-in-21)
|
|
102
|
+
- [What's New in 2.0](#whats-new-in-20)
|
|
103
|
+
- [How it compares](#how-it-compares)
|
|
104
|
+
- [Installation](#installation)
|
|
105
|
+
- [Usage](#usage)
|
|
106
|
+
- [Creating & comparing money](#creating--comparing-money)
|
|
107
|
+
- [Formatting](#formatting)
|
|
108
|
+
- [Integral & fractional parts](#integral--fractional-parts)
|
|
109
|
+
- [Parsing strings](#parsing-strings)
|
|
110
|
+
- [Currency lookup](#currency-lookup)
|
|
111
|
+
- [Crypto currencies](#crypto-currencies)
|
|
112
|
+
- [Locale formatting](#locale-formatting)
|
|
113
|
+
- [API notes](#api-notes)
|
|
114
|
+
- [Optional top-level `Money` and `Currency`](#optional-top-level-money-and-currency)
|
|
115
|
+
- [Rails integration](#rails-integration)
|
|
116
|
+
- [Roadmap](#roadmap)
|
|
117
|
+
- [License](#license)
|
|
118
|
+
|
|
119
|
+
## Why Minting
|
|
120
|
+
|
|
121
|
+
Minting started as a personal project to learn what it actually takes to build and maintain a real open source Ruby gem — not exactly as a reaction against any existing library. That origin shows in how it's built: it's grown deliberately, with an emphasis on correctness and a clean API.
|
|
122
|
+
|
|
123
|
+
What it's become along the way:
|
|
124
|
+
|
|
125
|
+
- **Exact by construction** — amounts are `Rational` internally, rounded to the currency's subunit only when needed. No silent precision loss from repeated arithmetic.
|
|
126
|
+
- **No Rails dependency** — Minting is a plain Ruby gem. Use it in a script, a Sinatra app, a background job runner, or a Rails app — your choice, not the gem's.
|
|
127
|
+
- **Formatting that doesn't fight you** — `Kernel.format`-style templates, named presets (`:accounting`, `:european`), per-sign formats (parentheses for negatives), and a pluggable locale hook.
|
|
128
|
+
- **Built for real-world currency handling** — 150+ ISO-4217 currencies, correct subunit handling (JPY has none, KWD has three), proportional allocation/split that doesn't lose cents to rounding.
|
|
129
|
+
- **Measured, not assumed, performance** — see the [Performance Guide](bench/BENCHMARKS.md) for actual benchmarks rather than claims.
|
|
130
|
+
- **Rails-ready without being Rails-only** — pair with the companion [MoneyAttribute](https://github.com/gferraz/money-attribute) gem for `ActiveRecord` type casting, validators, and form helpers.
|
|
131
|
+
|
|
132
|
+
## How it compares
|
|
133
|
+
|
|
134
|
+
A few structural differences from the `money` gem (and `money-rails`), for anyone evaluating both:
|
|
135
|
+
|
|
136
|
+
| | **Minting** | **Money** |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| Internal representation | `Rational` | `BigDecimal` (float-backed input coercion) |
|
|
139
|
+
| Rails integration | via `money_attribute` gem | via `money-rails` gem |
|
|
140
|
+
| Exchange rates | Pluggable provider architecture *(planned)* | Built-in bank/exchange abstraction |
|
|
141
|
+
| Currency data | Ships with the gem | Ships with the gem |
|
|
39
142
|
|
|
40
143
|
## Installation
|
|
41
144
|
|
|
@@ -51,20 +154,21 @@ gem 'minting'
|
|
|
51
154
|
|
|
52
155
|
## Usage
|
|
53
156
|
|
|
157
|
+
### Creating & comparing money
|
|
158
|
+
|
|
54
159
|
```ruby
|
|
55
160
|
require 'minting'
|
|
56
161
|
|
|
57
|
-
# Create money
|
|
58
162
|
ten = Money.from(10, 'USD') #=> [USD 10.00]
|
|
59
163
|
|
|
60
|
-
1.dollar == Money.from(1, 'USD')
|
|
61
|
-
ten = 10.dollars
|
|
62
|
-
4.to_money('USD')
|
|
164
|
+
1.dollar == Money.from(1, 'USD') #=> true
|
|
165
|
+
ten = 10.dollars #=> [USD 10.00]
|
|
166
|
+
4.to_money('USD') #=> [USD 4.00]
|
|
63
167
|
|
|
64
168
|
# Comparisons
|
|
65
|
-
ten == 10.dollars
|
|
66
|
-
ten == Money.from(10, 'EUR')
|
|
67
|
-
ten > Money.from(9.99, 'USD')
|
|
169
|
+
ten == 10.dollars #=> true
|
|
170
|
+
ten == Money.from(10, 'EUR') #=> false
|
|
171
|
+
ten > Money.from(9.99, 'USD') #=> true
|
|
68
172
|
|
|
69
173
|
# Zero equality semantics
|
|
70
174
|
# Any zero amount is treated as equal, regardless of currency
|
|
@@ -75,164 +179,234 @@ Money.from(0, 'USD') == 0.0 #=> true
|
|
|
75
179
|
# Non-zero numerics are not equal to Money objects
|
|
76
180
|
Money.from(10, 'USD') == 10 #=> false
|
|
77
181
|
|
|
78
|
-
#
|
|
79
|
-
|
|
80
|
-
|
|
182
|
+
# Ranges and enumeration are supported
|
|
183
|
+
1.dollar..10.dollars #=> [USD 1.00]..[USD 10.00]
|
|
184
|
+
(1.dollar..3.dollars).step(1.dollar).to_a #=> [[USD 1.00], [USD 2.00], [USD 3.00]]
|
|
81
185
|
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
Money.from(
|
|
85
|
-
price.format(:amount) #=> "9.99"
|
|
86
|
-
price.format(:currency) #=> "USD 9.99"
|
|
186
|
+
# Clamping to a range
|
|
187
|
+
price = Money.from(50, 'USD')
|
|
188
|
+
min_price = Money.from(75, 'USD')
|
|
87
189
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
#=>
|
|
190
|
+
price.clamp(0, 100) #=> [USD 50.00] (returns self, no new object)
|
|
191
|
+
price.clamp(0, 25) #=> [USD 25.00] (clamped to max)
|
|
192
|
+
price.clamp(min_price, 100) #=> [USD 75.00] (clamped to min, Money or Numeric bounds both work)
|
|
193
|
+
```
|
|
91
194
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
price.
|
|
96
|
-
|
|
97
|
-
|
|
195
|
+
### Formatting
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
price = Money.from(9.99, 'USD')
|
|
199
|
+
|
|
200
|
+
# Use direct format strings
|
|
201
|
+
price.format #=> "$9.99"
|
|
202
|
+
price.format('%<amount>d') #=> "9"
|
|
203
|
+
price.format('%<symbol>s%<amount>f') #=> "$9.99"
|
|
204
|
+
price.format('%<symbol>s%<amount>+f') #=> "$+9.99"
|
|
205
|
+
(-price).format('%<amount>f') #=> "-9.99"
|
|
98
206
|
|
|
99
207
|
# Format with padding
|
|
100
208
|
price_in_euros = Money.from(12.34, 'EUR')
|
|
101
209
|
|
|
102
|
-
price.format(
|
|
103
|
-
price.format(
|
|
104
|
-
(-price).format(
|
|
105
|
-
|
|
106
|
-
price_in_euros.format(format: '%<symbol>2s%<amount>+10f') #=> " € +12.34"
|
|
210
|
+
price.format('--%<amount>7d') #=> "-- 9"
|
|
211
|
+
price.format(' %<amount>10f %<currency>s') #=> " 9.99 USD"
|
|
212
|
+
(-price).format(' %<amount>10f') #=> " -9.99"
|
|
213
|
+
price_in_euros.format('%<symbol>2s%<amount>+10f') #=> " € +12.34"
|
|
107
214
|
|
|
108
215
|
# Integral & fractional parts
|
|
109
|
-
price.format(
|
|
110
|
-
Money.from(0.99, 'USD').format(
|
|
216
|
+
price.format('%<integral>d %<fractional>d/100') #=> "9 99/100"
|
|
217
|
+
Money.from(0.99, 'USD').format('%<integral>d dollars and %<fractional>02d cents')
|
|
111
218
|
#=> "0 dollars and 99 cents"
|
|
112
219
|
|
|
113
220
|
# Per-sign Hash format (e.g. accounting parentheses for losses)
|
|
114
|
-
loss
|
|
115
|
-
|
|
116
|
-
Money.from(0, 'BRL').format(format: { zero: '--' }) #=> "--"
|
|
117
|
-
# All three keys at once:
|
|
221
|
+
loss.format( { negative: '(%<symbol>s%<amount>f)' }) #=> "($1,234.56)"
|
|
222
|
+
Money.from(0, 'BRL').format( { zero: '--' }) #=> "--"
|
|
118
223
|
fmt = { positive: '%<symbol>s%<amount>f', negative: '(%<symbol>s%<amount>f)', zero: '--' }
|
|
119
|
-
Money.from(1234.56, 'USD').format(
|
|
224
|
+
Money.from(1234.56, 'USD').format( fmt) #=> "$1,234.56"
|
|
120
225
|
|
|
121
226
|
# Disambiguated symbol (e.g. "US$" vs "C$" vs "A$")
|
|
122
|
-
Money.from(10, 'USD').format(
|
|
123
|
-
Money.from(10, 'CAD').format(
|
|
124
|
-
Money.from(10, 'EUR').format(
|
|
227
|
+
Money.from(10, 'USD').format('%<dsymbol>s%<amount>f') #=> "US$10.00"
|
|
228
|
+
Money.from(10, 'CAD').format('%<dsymbol>s%<amount>f') #=> "C$10.00"
|
|
229
|
+
Money.from(10, 'EUR').format('%<dsymbol>s%<amount>f') #=> "€10.00" (falls back to symbol)
|
|
125
230
|
|
|
126
|
-
#
|
|
231
|
+
# Hash serialization
|
|
232
|
+
price.to_hash #=> {currency: "USD", amount: "9.99"}
|
|
233
|
+
```
|
|
127
234
|
|
|
128
|
-
|
|
235
|
+
### Integral & fractional parts
|
|
129
236
|
|
|
130
|
-
|
|
237
|
+
```ruby
|
|
238
|
+
price.integral #=> 9 # whole-unit part
|
|
239
|
+
price.fractional #=> 99 # fractional part (subunits within one unit)
|
|
240
|
+
price.subunits #=> 999 # total amount in smallest unit
|
|
241
|
+
price.to_i #=> 9 # alias of integral
|
|
131
242
|
|
|
132
|
-
|
|
243
|
+
Mint::Money.from_subunits(999, 'USD') #=> [USD 9.99]
|
|
244
|
+
Mint::Money.from_subunits(1234, 'JPY') #=> [JPY 1234] # subunit 0 -> no scaling
|
|
133
245
|
|
|
246
|
+
# No currency (ISO 4217 XXX)
|
|
247
|
+
Mint::Money.no_currency(100) #=> [XXX 100]
|
|
248
|
+
Mint::Money.no_currency(0) #=> [XXX 0]
|
|
134
249
|
|
|
135
|
-
#
|
|
250
|
+
# Proportional allocation and split
|
|
251
|
+
ten = 10.dollars
|
|
252
|
+
ten.split(3) #=> [[USD 3.34], [USD 3.33], [USD 3.33]]
|
|
253
|
+
ten.allocate([1, 2, 3]) #=> [[USD 1.67], [USD 3.33], [USD 5.00]]
|
|
254
|
+
```
|
|
136
255
|
|
|
137
|
-
|
|
138
|
-
Mint::Money.from_subunits(999, 'USD') #=> [USD 9.99]
|
|
139
|
-
Mint::Money.from_subunits(1234, 'JPY') #=> [JPY 1234] # subunit 0 -> no scaling
|
|
256
|
+
### Parsing strings
|
|
140
257
|
|
|
258
|
+
```ruby
|
|
259
|
+
Money.parse('$19.99') #=> [USD 19.99]
|
|
260
|
+
Money.parse('19,99 €') #=> [EUR 19.99]
|
|
261
|
+
Money.parse('1.234,56', 'EUR') #=> [EUR 1234.56]
|
|
262
|
+
Money.parse('USD 1,234.56') #=> [USD 1234.56]
|
|
263
|
+
```
|
|
141
264
|
|
|
142
|
-
|
|
265
|
+
Notes:
|
|
266
|
+
- Pass a currency code when the string has no symbol or code.
|
|
267
|
+
- `1,234` means 1234, not 1.234, and `1,23` means 1.23, not 123.
|
|
268
|
+
- `1,234.00` is unambiguous (thousands + decimal).
|
|
269
|
+
- Accounting negatives like `($1.23)` or `(USD 10.00)` are supported — the parser detects parentheses and negates the amount.
|
|
270
|
+
- Ambiguous symbols like `$` resolve by currency priority (currently USD).
|
|
271
|
+
- The parser scans all uppercase words for registered codes, so spurious non-currency words before the real code are correctly ignored: `Money.parse("MAX 10.00 USD")` yields `[USD 10.00]`.
|
|
143
272
|
|
|
144
|
-
|
|
145
|
-
Mint::Money.no_currency(0) #=> [XXX 0]
|
|
273
|
+
### Currency lookup
|
|
146
274
|
|
|
275
|
+
```ruby
|
|
276
|
+
# All registered currencies (150+ ISO 4217 + custom)
|
|
277
|
+
Money::Currency.registered_currencies.size #=> 164
|
|
278
|
+
Money::Currency.registered_currencies.each { |code, c| puts "#{code}: #{c.name}" }
|
|
147
279
|
|
|
148
|
-
#
|
|
280
|
+
# Built-in ISO 4217 currencies (before custom registrations)
|
|
281
|
+
Money::Currency.world_currencies.size #=> 154
|
|
149
282
|
|
|
150
|
-
|
|
151
|
-
|
|
283
|
+
# By ISO code (direct hash lookup, string only)
|
|
284
|
+
Money::Currency.for_code('USD') #=> #<Currency code="USD" ...>
|
|
152
285
|
|
|
153
|
-
#
|
|
286
|
+
# By display symbol (highest-priority currency for ambiguous symbols)
|
|
287
|
+
Money::Currency.for_symbol('$') #=> #<Currency code="USD" ...>
|
|
288
|
+
Money::Currency.for_symbol('R$') #=> #<Currency code="BRL" ...>
|
|
289
|
+
Money::Currency.for_symbol('€') #=> #<Currency code="EUR" ...>
|
|
290
|
+
```
|
|
154
291
|
|
|
155
|
-
|
|
156
|
-
min_price = Money.from(75, 'USD')
|
|
292
|
+
**Polymorphic currency resolution** — `Currency.resolve` also accepts objects that implement `#to_currency` or `#currency_code`:
|
|
157
293
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
294
|
+
```ruby
|
|
295
|
+
class Product
|
|
296
|
+
def currency_code = 'USD'
|
|
297
|
+
end
|
|
161
298
|
|
|
162
|
-
|
|
163
|
-
|
|
299
|
+
Money::Currency.resolve(Product.new) #=> #<Currency code="USD" ...>
|
|
300
|
+
Money::Currency.resolve!(Product.new) # raises Mint::UnknownCurrency if code is unknown
|
|
301
|
+
```
|
|
164
302
|
|
|
165
|
-
|
|
303
|
+
`#to_currency` takes precedence when both methods exist. It must return a `Currency` object; `#currency_code` must return a `String`. Wrong types raise `ArgumentError`.
|
|
166
304
|
|
|
167
|
-
|
|
168
|
-
|
|
305
|
+
### Crypto currencies
|
|
306
|
+
|
|
307
|
+
Minting ships with opt-in definitions for ~25 popular crypto currencies (BTC, ETH, SOL, ...). They are not registered by default — use `register_crypto` to enable them:
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
Currency.register_crypto('BTC', 'ETH', 'SOL')
|
|
169
311
|
|
|
312
|
+
Money.parse("0.01 BTC") #=> [BTC 0.01000000]
|
|
313
|
+
Mint.money(1, 'ETH') #=> [ETH 1.000000000000000000]
|
|
170
314
|
```
|
|
171
315
|
|
|
172
|
-
|
|
316
|
+
`Currency.crypto_currencies` lists all available definitions without registering:
|
|
173
317
|
|
|
174
318
|
```ruby
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
319
|
+
Currency.crypto_currencies.each { |c| puts "#{c.code}: #{c.name}" }
|
|
320
|
+
# BTC: Bitcoin
|
|
321
|
+
# ETH: Ethereum
|
|
322
|
+
# SOL: Solana
|
|
323
|
+
# ...
|
|
179
324
|
```
|
|
180
325
|
|
|
181
|
-
|
|
182
|
-
- Pass a currency code when the string has no symbol or code.
|
|
183
|
-
- `1,234` means 1234, not 1.234 and `1,23` means 1.23, not 123
|
|
184
|
-
- `1,234.00` is unambiguous (thousands + decimal).
|
|
185
|
-
- Accounting negatives like `($1.23)` or `(USD 10.00)` are supported — the parser detects parentheses and negates the amount.
|
|
186
|
-
- Ambiguous symbols like `$` resolve by currency priority (currently USD).
|
|
187
|
-
- 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]`.
|
|
326
|
+
`register_crypto` raises `KeyError` on duplicate codes and `ArgumentError` on unknown codes. Register all at once:
|
|
188
327
|
|
|
189
|
-
|
|
328
|
+
```ruby
|
|
329
|
+
# Register all at once:
|
|
330
|
+
Currency.register_all_crypto # raises KeyError on any conflict
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Locale formatting
|
|
334
|
+
|
|
335
|
+
Minting doesn't ship built-in locale data, but the `Mint.locale_backend` hook lets you wire in locale-specific decimal/thousand separators and format templates:
|
|
190
336
|
|
|
191
337
|
```ruby
|
|
192
|
-
|
|
193
|
-
|
|
338
|
+
LOCALE_DATA = {
|
|
339
|
+
'en' => { decimal: '.', thousand: ',', format: '%<symbol>s%<amount>f' },
|
|
340
|
+
'pt' => { decimal: ',', thousand: '.', format: '%<symbol>s%<amount>f' },
|
|
341
|
+
'pt-BR' => { decimal: ',', thousand: '.', format: '%<symbol>s%<amount>f' },
|
|
342
|
+
'de' => { decimal: ',', thousand: '.', format: '%<amount>f %<currency>s' },
|
|
343
|
+
'fr' => { decimal: ',', thousand: ' ', format: '%<amount>f %<symbol>s' },
|
|
344
|
+
'ja' => { decimal: '.', thousand: ',', format: '%<symbol>s%<amount>f' },
|
|
345
|
+
}.freeze
|
|
346
|
+
|
|
347
|
+
Mint.locale_backend = ->(locale) { LOCALE_DATA[locale.to_s] || {} }
|
|
348
|
+
|
|
349
|
+
Mint.money(1234.56, 'USD').format(locale: :en) #=> "$1,234.56"
|
|
350
|
+
Mint.money(9.99, 'BRL').format(locale: 'pt') #=> "R$9,99"
|
|
351
|
+
Mint.money(9.99, 'EUR').format(locale: :de) #=> "9,99 EUR"
|
|
352
|
+
Mint.money(9.99, 'EUR').format(locale: 'fr') #=> "9,99 €"
|
|
353
|
+
Mint.money(9.99, 'USD').format(locale: :ja) #=> "$9.99"
|
|
354
|
+
```
|
|
194
355
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
Mint::Currency.for_symbol('€') #=> #<Currency code="EUR" ...>
|
|
356
|
+
Pass `locale:` as a keyword to `format` / `to_fs`. Accepts both symbols (`:en`, `:'pt-BR'`) and strings (`'pt-BR'`, `'en-US'`) — passed through as-is, matching Rails' `I18n.locale` convention. The backend returns a hash with `:decimal`, `:thousand`, and optionally `:format` (defaults to `'%<symbol>s%<amount>f'`). String and symbol keys are interchangeable. Return `{}` or `nil` for unknown locales — defaults apply.
|
|
357
|
+
|
|
358
|
+
Rails I18n key names (`:separator`, `:delimiter`) are also accepted — no mapping needed:
|
|
199
359
|
|
|
360
|
+
```ruby
|
|
361
|
+
Mint.locale_backend = ->(locale = nil) {
|
|
362
|
+
I18n.with_locale(locale || I18n.default_locale) do
|
|
363
|
+
I18n.t('number.currency.format', default: {})
|
|
364
|
+
end
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Minting names take precedence when both are present (e.g. `{ decimal: '.', separator: ',' }` uses `'.'`).
|
|
369
|
+
|
|
370
|
+
Arity-0 callables (`-> { ... }`) are called without arguments and work unchanged:
|
|
371
|
+
|
|
372
|
+
```ruby
|
|
373
|
+
Mint.locale_backend = -> { { decimal: ',', thousand: '.' } }
|
|
374
|
+
Mint.money(9.99, 'BRL').format #=> "R$9,99"
|
|
200
375
|
```
|
|
201
376
|
|
|
202
377
|
## API notes
|
|
203
378
|
|
|
204
379
|
**Exact amounts** — Amounts are stored as `Rational` and rounded to the currency subunit.
|
|
205
380
|
|
|
206
|
-
**Rounding modes** — Wrap operations in `
|
|
381
|
+
**Rounding modes** — Wrap operations in `Money.with_rounding(mode)` to change how amounts are rounded to the subunit:
|
|
207
382
|
|
|
208
383
|
```ruby
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
384
|
+
Money.with_rounding(:down) { Money.from(1.005, 'USD') } #=> [USD 1.00]
|
|
385
|
+
Money.with_rounding(:even) { Money.from(1.015, 'USD') } #=> [USD 1.02]
|
|
386
|
+
Money.with_rounding(:up) { Money.from(1.005, 'USD') } #=> [USD 1.01]
|
|
212
387
|
```
|
|
213
388
|
|
|
214
|
-
Modes: `:
|
|
389
|
+
Modes: `:up` (default), `:down`, `:even`. Applies to construction, parsing, `change`, `split`, and `allocate`. Restores the previous mode when the block exits, even on exception.
|
|
215
390
|
|
|
216
|
-
> **Performance note:** Rounding-mode support is not loaded by default — `require 'minting'` uses the fastest possible rounding (equivalent to `:
|
|
391
|
+
> **Performance note:** Rounding-mode support is not loaded by default — `require 'minting'` uses the fastest possible rounding (equivalent to `:up`) with zero dispatch overhead. The first call to `Money.with_rounding` activates the rounding dispatch in `Currency#normalize_amount`, adding ~10–35 ns per money creation or mutation. If your application never uses custom rounding modes, there is **no performance cost**.
|
|
217
392
|
|
|
218
393
|
**Division** — `money / 5` returns new `Money`; `money / other_money` returns a numeric ratio, not money.
|
|
219
394
|
|
|
220
395
|
**Zero equality** — Any zero amount is considered equal across currencies and to numeric zero (`Money.from(0, 'USD') == Money.from(0, 'EUR')` is intentionally `true`). Non-zero amounts must match currency and value.
|
|
221
396
|
|
|
222
|
-
**Zero helper** — `Currency.zero('USD')` returns a frozen zero-Money, useful as a default value for discounts, totals, or counters.
|
|
223
397
|
|
|
224
|
-
**Registered currencies** — `Currency.register(code:, subunit:, symbol:, priority:)` adds custom currencies. Only registered codes and symbols are recognized by the parser or searches. Nonetheless, you don't need to register a currency to use it with most of the features.
|
|
225
398
|
|
|
226
|
-
**
|
|
399
|
+
**Registered currencies** — `Currency.register(code:, subunit:, symbol:, priority:)` adds custom currencies. Only registered codes and symbols are recognized by the parser or searches. You don't need to register a currency to use it with most features.
|
|
227
400
|
|
|
228
|
-
|
|
401
|
+
**Built-in currencies** — 150+ ISO-4217 world currencies ship in `lib/minting/data/world-currencies.yaml` and are preloaded at gem initialization.
|
|
229
402
|
|
|
230
|
-
|
|
403
|
+
## Optional top-level `Money` (opt-out) and `Currency` (opt-in)
|
|
231
404
|
|
|
405
|
+
By default, `require "minting"` exposes `Mint::Money` as the top-level `Money` constant, so you can write `Money.from(10, "USD")` directly:
|
|
232
406
|
```ruby
|
|
233
407
|
require "minting"
|
|
234
408
|
|
|
235
|
-
price = Money.from(10, "USD")
|
|
409
|
+
price = Money.from(10, "USD") # equivalent to Mint::Money.from
|
|
236
410
|
tax = Money.from(2.50, "USD")
|
|
237
411
|
```
|
|
238
412
|
|
|
@@ -240,26 +414,28 @@ tax = Money.from(2.50, "USD")
|
|
|
240
414
|
|
|
241
415
|
```ruby
|
|
242
416
|
require "minting"
|
|
243
|
-
require "minting/
|
|
417
|
+
require "minting/aliases" # opt-in top-level Currency
|
|
244
418
|
|
|
245
419
|
cur = Currency.new(code: "EUR", symbol: "€", subunit: 2, priority: 0)
|
|
246
420
|
```
|
|
247
421
|
|
|
248
|
-
For Rails applications,
|
|
422
|
+
For Rails applications, enable it in an initializer:
|
|
249
423
|
|
|
250
424
|
```ruby
|
|
251
425
|
# config/initializers/minting.rb
|
|
252
|
-
require "minting/
|
|
426
|
+
require "minting/aliases"
|
|
253
427
|
```
|
|
254
428
|
|
|
255
|
-
If another `Money` is already defined when `require "minting"` runs (e.g. the `money` gem was loaded first), Minting warns and skips the auto-bind — use `Mint::Money` in that case. The same applies to `Currency` via `minting/
|
|
429
|
+
If another `Money` is already defined when `require "minting"` runs (e.g. the `money` gem was loaded first), Minting warns and skips the auto-bind — use `Mint::Money` in that case. The same applies to `Currency` via `minting/aliases`.
|
|
256
430
|
|
|
257
431
|
**Good fit:** Application code, especially Rails apps.
|
|
258
432
|
**Not recommended:** Reusable gems/libraries — stick to `Mint::Money` to avoid conflicts.
|
|
259
433
|
|
|
260
|
-
##
|
|
434
|
+
## Rails integration
|
|
435
|
+
|
|
436
|
+
Minting itself has no Rails dependency. For `ActiveRecord` type casting, validators, and form helpers, pair it with the companion gem:
|
|
261
437
|
|
|
262
|
-
-
|
|
438
|
+
- **[MoneyAttribute](https://github.com/gferraz/money-attribute)** — a `money_attribute` macro for models, with `ActiveRecord::Type` integration and `composed_of`-based support for multi-column (amount + currency) attributes.
|
|
263
439
|
|
|
264
440
|
## License
|
|
265
441
|
|
data/Rakefile
CHANGED
|
@@ -2,10 +2,9 @@ require 'bundler/audit/task'
|
|
|
2
2
|
require 'bundler/gem_tasks'
|
|
3
3
|
require 'rake/testtask'
|
|
4
4
|
require 'rubocop/rake_task'
|
|
5
|
-
require 'rubycritic/rake_task'
|
|
6
5
|
require 'yard'
|
|
7
6
|
|
|
8
|
-
CLOBBER.include %w[doc/css doc/js doc/Mint doc/*.html tmp .yardoc]
|
|
7
|
+
CLOBBER.include %w[doc/css doc/js doc/Mint doc/*.html tmp .yardoc pkg tmp/*]
|
|
9
8
|
|
|
10
9
|
Rake::TestTask.new(:test) do |t|
|
|
11
10
|
t.libs << 'test'
|
|
@@ -50,7 +49,7 @@ desc 'Run core benchmarks and update the baseline'
|
|
|
50
49
|
task 'bench:baseline' do
|
|
51
50
|
platform = RUBY_PLATFORM
|
|
52
51
|
baseline = "bench/check/results/baseline-#{platform}.json"
|
|
53
|
-
sh "ruby bench/check/runner.rb #{baseline}"
|
|
52
|
+
sh "bundle exec ruby bench/check/runner.rb #{baseline}"
|
|
54
53
|
puts "Baseline updated for #{platform}."
|
|
55
54
|
end
|
|
56
55
|
|
|
@@ -65,10 +64,6 @@ RuboCop::RakeTask.new(:cop) do |task|
|
|
|
65
64
|
task.patterns = ['lib']
|
|
66
65
|
end
|
|
67
66
|
|
|
68
|
-
RubyCritic::RakeTask.new do |task|
|
|
69
|
-
task.name = 'critic'
|
|
70
|
-
end
|
|
71
|
-
|
|
72
67
|
YARD::Rake::YardocTask.new do |t|
|
|
73
68
|
t.files = ['lib/**/*.rb']
|
|
74
69
|
t.stats_options = ['--list-undoc']
|
|
@@ -127,7 +127,7 @@ This is odd: the param type is a `Currency` in the docs, but you call `other.cur
|
|
|
127
127
|
other_currency =
|
|
128
128
|
case other
|
|
129
129
|
when Mint::Money then other.currency
|
|
130
|
-
when
|
|
130
|
+
when Money::Currency then other
|
|
131
131
|
else
|
|
132
132
|
Currency.resolve!(other)
|
|
133
133
|
end
|
|
@@ -2,7 +2,7 @@ Repository overview
|
|
|
2
2
|
|
|
3
3
|
- Language: Ruby gem (minting)
|
|
4
4
|
- Location of main code: lib/minting and its subfolders (mint/, money/)
|
|
5
|
-
- Public API surface: Mint (factory/helpers), Mint::Money,
|
|
5
|
+
- Public API surface: Mint (factory/helpers), Mint::Money, Money::Currency
|
|
6
6
|
- Data: built-in currencies in lib/minting/data/currencies.yaml
|
|
7
7
|
- Tests: Minitest (unit) + performance benchmarks under bench/
|
|
8
8
|
|
|
@@ -67,4 +67,4 @@ Notes for Copilot sessions
|
|
|
67
67
|
- When changing numeric/rounding behavior, run both unit tests and relevant performance benchmarks.
|
|
68
68
|
- Respect zero-equality semantics and currency code validation when modifying equality/hash logic.
|
|
69
69
|
|
|
70
|
-
If you want, update this file with project-specific conventions to capture workflow choices (e.g., backport policy, DI patterns).
|
|
70
|
+
If you want, update this file with project-specific conventions to capture workflow choices (e.g., backport policy, DI patterns).
|
|
@@ -2,7 +2,7 @@ Repository overview
|
|
|
2
2
|
|
|
3
3
|
- Language: Ruby gem (minting)
|
|
4
4
|
- Location of main code: lib/minting and its subfolders (mint/, money/)
|
|
5
|
-
- Public API surface: Mint (factory/helpers), Mint::Money,
|
|
5
|
+
- Public API surface: Mint (factory/helpers), Mint::Money, Money::Currency
|
|
6
6
|
- Data: built-in currencies in lib/minting/data/currencies.yaml
|
|
7
7
|
- Tests: Minitest (unit) + performance benchmarks under test/performance
|
|
8
8
|
|
|
@@ -72,4 +72,4 @@ Notes for Copilot sessions
|
|
|
72
72
|
- When changing numeric/rounding behavior, run both unit tests and relevant performance benchmarks.
|
|
73
73
|
- Respect zero-equality semantics and currency code validation when modifying equality/hash logic.
|
|
74
74
|
|
|
75
|
-
If you want, update this file with project-specific conventions to capture workflow choices (e.g., backport policy, DI patterns).
|
|
75
|
+
If you want, update this file with project-specific conventions to capture workflow choices (e.g., backport policy, DI patterns).
|