minting 1.9.7 → 2.0.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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +234 -116
  3. data/Rakefile +2 -7
  4. data/doc/agents/api_review-2026-06-15.md +1 -1
  5. data/doc/agents/copilot-instructions.md +2 -2
  6. data/doc/agents/expired/copilot-instructions.md +2 -2
  7. data/doc/agents/expired/gemini_gem_evaluation.md +2 -2
  8. data/lib/minting/aliases.rb +22 -0
  9. data/lib/minting/currency/currency.rb +81 -9
  10. data/lib/minting/data/crypto-currencies.yaml +126 -0
  11. data/lib/minting/mint/i18n.rb +79 -29
  12. data/lib/minting/mint/mint.rb +1 -26
  13. data/lib/minting/mint/registry/crypto.rb +59 -0
  14. data/lib/minting/mint/registry/registration.rb +1 -2
  15. data/lib/minting/mint/registry/symbols.rb +37 -30
  16. data/lib/minting/mint/rounding.rb +9 -7
  17. data/lib/minting/mint.rb +1 -2
  18. data/lib/minting/money/allocation/allocation.rb +2 -2
  19. data/lib/minting/money/allocation/split.rb +1 -1
  20. data/lib/minting/money/arithmetics/operators.rb +10 -13
  21. data/lib/minting/money/clamp.rb +6 -6
  22. data/lib/minting/money/coercion.rb +1 -1
  23. data/lib/minting/money/comparable.rb +3 -3
  24. data/lib/minting/money/constructors.rb +3 -42
  25. data/lib/minting/money/conversion.rb +22 -18
  26. data/lib/minting/money/format/format.rb +100 -0
  27. data/lib/minting/money/format/formatter.rb +102 -0
  28. data/lib/minting/money/format/to_s.rb +20 -102
  29. data/lib/minting/money/format/validator.rb +34 -0
  30. data/lib/minting/money/money.rb +25 -9
  31. data/lib/minting/money/parse.rb +127 -0
  32. data/lib/minting/money/rounding.rb +27 -0
  33. data/lib/minting/version.rb +1 -1
  34. data/lib/minting.rb +17 -8
  35. metadata +9 -29
  36. data/doc/Mint/Currency.html +0 -2032
  37. data/doc/Mint/Money.html +0 -5139
  38. data/doc/Mint/RangeStepPatch.html +0 -277
  39. data/doc/Mint/Registry.html +0 -863
  40. data/doc/Mint/Rounding.html +0 -506
  41. data/doc/Mint/UnknownCurrency.html +0 -138
  42. data/doc/Mint.html +0 -931
  43. data/doc/Minting.html +0 -142
  44. data/doc/Numeric.html +0 -479
  45. data/doc/String.html +0 -241
  46. data/doc/_index.html +0 -206
  47. data/doc/class_list.html +0 -54
  48. data/doc/css/common.css +0 -1
  49. data/doc/css/full_list.css +0 -206
  50. data/doc/css/style.css +0 -1089
  51. data/doc/file.README.html +0 -291
  52. data/doc/file_list.html +0 -59
  53. data/doc/frames.html +0 -22
  54. data/doc/index.html +0 -291
  55. data/doc/js/app.js +0 -801
  56. data/doc/js/full_list.js +0 -334
  57. data/doc/js/jquery.js +0 -4
  58. data/doc/method_list.html +0 -758
  59. data/doc/top-level-namespace.html +0 -135
  60. data/lib/minting/mint/aliases.rb +0 -16
  61. data/lib/minting/mint/parser/parser.rb +0 -97
  62. data/lib/minting/mint/parser/separators.rb +0 -41
  63. data/lib/minting/money/format/formatting.rb +0 -130
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 88ad41982102280b92486a6c6b1e05ae274afca3ccaf9daed2b778bc88f89764
4
- data.tar.gz: 40c71dfc229055ddeb61c59c6a05977d8cce464c9ea869d07556be3efa529920
3
+ metadata.gz: 20bb9d0f4d0437f84ab790d3fdbd27650bee49d0b9d1231b3ba19888987583b4
4
+ data.tar.gz: 8787d2359db2b7c75a2054fdcc6d56525e819f25b4d1cc746d2d6bcb01fd03ff
5
5
  SHA512:
6
- metadata.gz: fd6f1a5b8c3fdd281080743065a366b7a0334423f373a0970ecfff386682e0fd545103a3eb42aa516bca0dfd0a6709d2fbcc422b4966e610ca5efe55ddf270a4
7
- data.tar.gz: 76f592147f190433923741d40707b830448c80a7b3fe003392c9c8740c06a41b8f2433e7d832db27b8758f8d46f272b9fd33d9d2b3e33fb0a2742aa2fc6aa19d
6
+ metadata.gz: 29d3c3b52a8f5786587caf516fce6828341ec10ca6dd57f2882b9fb6dda13ae136237d3fe087613d6a832553be05a5bb1fb3e77d54952d030fdca87a879923e2
7
+ data.tar.gz: f5d8ef6c9bb9bffdbe9b7b84b94badb678d4a04a4648b63fa49b9e0086130d0abf73bd20988c6e544b7b154e671677ae0d06bc3dcc739ce410e034b5fedd53fe
data/README.md CHANGED
@@ -1,41 +1,68 @@
1
- # Minting
2
-
3
- Fast, precise, and developer-friendly money handling for Ruby.
4
-
5
1
  [![Gem Version](https://badge.fury.io/rb/minting.svg)](https://badge.fury.io/rb/minting)
6
2
  [![CI](https://github.com/gferraz/minting/actions/workflows/ci.yml/badge.svg)](https://github.com/gferraz/minting/actions/workflows/ci.yml)
7
3
  [![Test Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/gferraz/minting)
8
4
  [![Documentation](https://img.shields.io/badge/docs-rubydoc.info-blue)](https://www.rubydoc.info/gems/minting/frames)
9
5
 
10
- ## Quick start
6
+ # Minting
11
7
 
12
- ```ruby
13
- require 'minting'
8
+ **Fast, precise, and developer-friendly money handling for Ruby.**
14
9
 
10
+
11
+
12
+ > **Status:** Minting 2.0 is released. The core API (`Money`, `Currency`, formatting, parsing) is stable.
13
+
14
+ ```ruby
15
15
  price = Money.from(19.99, 'USD') #=> [USD 19.99]
16
16
  tax = price * 0.08 #=> [USD 1.60]
17
17
  total = price + tax #=> [USD 21.59]
18
18
 
19
19
  total.to_s #=> "$21.59"
20
- total.currency_code #=> "USD"
21
20
  ```
22
21
 
23
- ### Exact precision
24
- Amounts are stored as `Rational` and rounded to the currency subunit. No floating-point surprises, ever.
22
+ 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.
23
+
24
+ ## Table of contents
25
+
26
+ - [Why Minting](#why-minting)
27
+ - [How it compares](#how-it-compares)
28
+ - [Installation](#installation)
29
+ - [Usage](#usage)
30
+ - [Creating & comparing money](#creating--comparing-money)
31
+ - [Formatting](#formatting)
32
+ - [Integral & fractional parts](#integral--fractional-parts)
33
+ - [Parsing strings](#parsing-strings)
34
+ - [Currency lookup](#currency-lookup)
35
+ - [Crypto currencies](#crypto-currencies)
36
+ - [Locale formatting](#locale-formatting)
37
+ - [API notes](#api-notes)
38
+ - [Optional top-level `Money` and `Currency`](#optional-top-level-money-and-currency)
39
+ - [Rails integration](#rails-integration)
40
+ - [Roadmap](#roadmap)
41
+ - [License](#license)
25
42
 
26
- ### Good performance
27
- Minting is fast! See full benchmarks in the [Performance Guide](bench/BENCHMARKS.md).
43
+ ## Why Minting
28
44
 
29
- ### Clean, modern API
30
- Intuitive interface, descriptive error messages, and sensible defaults. Works the way you expect.
45
+ 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.
31
46
 
32
- ### Rails-ready
33
- Use with the [attribute-money](https://github.com/gferraz/attribute-money) companion gem for drop-in ActiveRecord type casting, validators, and form helpers.
47
+ What it's become along the way:
34
48
 
35
- ### Quality code
36
- - **100% test coverage** — every line exercised
37
- - **94/100 RubyCritic score** — clean, maintainable code
38
- - **CI-tested on Ruby 3.3 and 4.0**
49
+ - **Exact by construction** — amounts are `Rational` internally, rounded to the currency's subunit only when needed. No silent precision loss from repeated arithmetic.
50
+ - **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.
51
+ - **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.
52
+ - **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.
53
+ - **Measured, not assumed, performance** — see the [Performance Guide](bench/BENCHMARKS.md) for actual benchmarks rather than claims.
54
+ - **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.
55
+
56
+ ## How it compares
57
+
58
+ A few structural differences from the `money` gem (and `money-rails`), for anyone evaluating both:
59
+
60
+ | | **Minting** | **Money** |
61
+ |---|---|---|
62
+ | Internal representation | `Rational` | `BigDecimal` (float-backed input coercion) |
63
+ | Rails integration | via `money_attribute` gem | via `money-rails` gem |
64
+ | Exchange rates | Pluggable provider architecture *(planned)* | Built-in bank/exchange abstraction |
65
+ | Currency data | Ships with the gem | Ships with the gem |
39
66
 
40
67
  ## Installation
41
68
 
@@ -51,20 +78,21 @@ gem 'minting'
51
78
 
52
79
  ## Usage
53
80
 
81
+ ### Creating & comparing money
82
+
54
83
  ```ruby
55
84
  require 'minting'
56
85
 
57
- # Create money
58
86
  ten = Money.from(10, 'USD') #=> [USD 10.00]
59
87
 
60
- 1.dollar == Money.from(1, 'USD') #=> true
61
- ten = 10.dollars #=> [USD 10.00]
62
- 4.to_money('USD') #=> [USD 4.00]
88
+ 1.dollar == Money.from(1, 'USD') #=> true
89
+ ten = 10.dollars #=> [USD 10.00]
90
+ 4.to_money('USD') #=> [USD 4.00]
63
91
 
64
92
  # Comparisons
65
- ten == 10.dollars #=> true
66
- ten == Money.from(10, 'EUR') #=> false
67
- ten > Money.from(9.99, 'USD') #=> true
93
+ ten == 10.dollars #=> true
94
+ ten == Money.from(10, 'EUR') #=> false
95
+ ten > Money.from(9.99, 'USD') #=> true
68
96
 
69
97
  # Zero equality semantics
70
98
  # Any zero amount is treated as equal, regardless of currency
@@ -75,153 +103,234 @@ Money.from(0, 'USD') == 0.0 #=> true
75
103
  # Non-zero numerics are not equal to Money objects
76
104
  Money.from(10, 'USD') == 10 #=> false
77
105
 
78
- # Format (uses Kernel.format syntax)
79
- price = Money.from(9.99, 'USD')
80
- loss = Money.from(-1234.56, 'USD')
106
+ # Ranges and enumeration are supported
107
+ 1.dollar..10.dollars #=> [USD 1.00]..[USD 10.00]
108
+ (1.dollar..3.dollars).step(1.dollar).to_a #=> [[USD 1.00], [USD 2.00], [USD 3.00]]
109
+
110
+ # Clamping to a range
111
+ price = Money.from(50, 'USD')
112
+ min_price = Money.from(75, 'USD')
113
+
114
+ price.clamp(0, 100) #=> [USD 50.00] (returns self, no new object)
115
+ price.clamp(0, 25) #=> [USD 25.00] (clamped to max)
116
+ price.clamp(min_price, 100) #=> [USD 75.00] (clamped to min, Money or Numeric bounds both work)
117
+ ```
81
118
 
82
- # Built-in named presets
83
- loss.format(:accounting) #=> "($1,234.56)"
84
- Money.from(1234.56, 'EUR').format(:european) #=> "1.234,56 €"
85
- price.format(:amount) #=> "9.99"
86
- price.format(:currency) #=> "USD 9.99"
119
+ ### Formatting
87
120
 
88
- # Presets can be overridden with explicit kwargs
89
- Money.from(1234.56, 'EUR').format(:european, format: '%<amount>f %<currency>s')
90
- #=> "1.234,56 EUR"
121
+ ```ruby
122
+ price = Money.from(9.99, 'USD')
91
123
 
92
- # Or use direct format strings
93
- price.format #=> "$9.99",
94
- price.format(format: '%<amount>d') #=> "9",
95
- price.format(format: '%<symbol>s%<amount>f') #=> "$9.99",
96
- price.format(format: '%<symbol>s%<amount>+f') #=> "$+9.99",
97
- (-price).format(format: '%<amount>f') #=> "-9.99",
124
+ # Use direct format strings
125
+ price.format #=> "$9.99"
126
+ price.format('%<amount>d') #=> "9"
127
+ price.format('%<symbol>s%<amount>f') #=> "$9.99"
128
+ price.format('%<symbol>s%<amount>+f') #=> "$+9.99"
129
+ (-price).format('%<amount>f') #=> "-9.99"
98
130
 
99
131
  # Format with padding
100
132
  price_in_euros = Money.from(12.34, 'EUR')
101
133
 
102
- price.format(format: '--%<amount>7d') #=> "-- 9"
103
- price.format(format: ' %<amount>10f %<currency>s') #=> " 9.99 USD"
104
- (-price).format(format: ' %<amount>10f') #=> " -9.99"
105
-
106
- price_in_euros.format(format: '%<symbol>2s%<amount>+10f') #=> " € +12.34"
134
+ price.format('--%<amount>7d') #=> "-- 9"
135
+ price.format(' %<amount>10f %<currency>s') #=> " 9.99 USD"
136
+ (-price).format(' %<amount>10f') #=> " -9.99"
137
+ price_in_euros.format('%<symbol>2s%<amount>+10f') #=> " € +12.34"
107
138
 
108
139
  # Integral & fractional parts
109
- price.format(format: '%<integral>d %<fractional>d/100') #=> "9 99/100"
110
- Money.from(0.99, 'USD').format(format: '%<integral>d dollars and %<fractional>02d cents')
140
+ price.format('%<integral>d %<fractional>d/100') #=> "9 99/100"
141
+ Money.from(0.99, 'USD').format('%<integral>d dollars and %<fractional>02d cents')
111
142
  #=> "0 dollars and 99 cents"
112
143
 
113
144
  # Per-sign Hash format (e.g. accounting parentheses for losses)
114
- loss = Money.from(-1234.56, 'USD')
115
- loss.format(format: { negative: '(%<symbol>s%<amount>f)' }) #=> "($1,234.56)"
116
- Money.from(0, 'BRL').format(format: { zero: '--' }) #=> "--"
117
- # All three keys at once:
145
+ loss.format( { negative: '(%<symbol>s%<amount>f)' }) #=> "($1,234.56)"
146
+ Money.from(0, 'BRL').format( { zero: '--' }) #=> "--"
118
147
  fmt = { positive: '%<symbol>s%<amount>f', negative: '(%<symbol>s%<amount>f)', zero: '--' }
119
- Money.from(1234.56, 'USD').format(format: fmt) #=> "$1,234.56"
148
+ Money.from(1234.56, 'USD').format( fmt) #=> "$1,234.56"
120
149
 
121
150
  # Disambiguated symbol (e.g. "US$" vs "C$" vs "A$")
122
- Money.from(10, 'USD').format(format: '%<dsymbol>s%<amount>f') #=> "US$10.00"
123
- Money.from(10, 'CAD').format(format: '%<dsymbol>s%<amount>f') #=> "C$10.00"
124
- Money.from(10, 'EUR').format(format: '%<dsymbol>s%<amount>f') #=> "€10.00" (no dsymbol, falls back to symbol)
151
+ Money.from(10, 'USD').format('%<dsymbol>s%<amount>f') #=> "US$10.00"
152
+ Money.from(10, 'CAD').format('%<dsymbol>s%<amount>f') #=> "C$10.00"
153
+ Money.from(10, 'EUR').format('%<dsymbol>s%<amount>f') #=> "€10.00" (falls back to symbol)
125
154
 
126
- # Json serialization
155
+ # Hash serialization
156
+ price.to_hash #=> {currency: "USD", amount: "9.99"}
157
+ ```
127
158
 
128
- price.to_json # => "{\"currency\": \"USD\", \"amount\": \"9.99\"}"
159
+ ### Integral & fractional parts
129
160
 
130
- # Hash conversion
161
+ ```ruby
162
+ price.integral #=> 9 # whole-unit part
163
+ price.fractional #=> 99 # fractional part (subunits within one unit)
164
+ price.subunits #=> 999 # total amount in smallest unit
165
+ price.to_i #=> 9 # alias of integral
131
166
 
132
- price.to_hash #=> {currency: "USD", amount: "9.99"}
167
+ Mint::Money.from_subunits(999, 'USD') #=> [USD 9.99]
168
+ Mint::Money.from_subunits(1234, 'JPY') #=> [JPY 1234] # subunit 0 -> no scaling
133
169
 
170
+ # No currency (ISO 4217 XXX)
171
+ Mint::Money.no_currency(100) #=> [XXX 100]
172
+ Mint::Money.no_currency(0) #=> [XXX 0]
134
173
 
135
- # Fractional units (inverse of #fractional) - exact integer arithmetic
174
+ # Proportional allocation and split
175
+ ten = 10.dollars
176
+ ten.split(3) #=> [[USD 3.34], [USD 3.33], [USD 3.33]]
177
+ ten.allocate([1, 2, 3]) #=> [[USD 1.67], [USD 3.33], [USD 5.00]]
178
+ ```
136
179
 
137
- price.subunits #=> 999
138
- Mint::Money.from_subunits(999, 'USD') #=> [USD 9.99]
139
- Mint::Money.from_subunits(1234, 'JPY') #=> [JPY 1234] # subunit 0 -> no scaling
180
+ ### Parsing strings
140
181
 
182
+ ```ruby
183
+ Money.parse('$19.99') #=> [USD 19.99]
184
+ Money.parse('19,99 €') #=> [EUR 19.99]
185
+ Money.parse('1.234,56', 'EUR') #=> [EUR 1234.56]
186
+ Money.parse('USD 1,234.56') #=> [USD 1234.56]
187
+ ```
141
188
 
142
- # No currency (ISO 4217 XXX)
189
+ Notes:
190
+ - Pass a currency code when the string has no symbol or code.
191
+ - `1,234` means 1234, not 1.234, and `1,23` means 1.23, not 123.
192
+ - `1,234.00` is unambiguous (thousands + decimal).
193
+ - Accounting negatives like `($1.23)` or `(USD 10.00)` are supported — the parser detects parentheses and negates the amount.
194
+ - Ambiguous symbols like `$` resolve by currency priority (currently USD).
195
+ - 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
196
 
144
- Mint::Money.no_currency(100) #=> [XXX 100]
145
- Mint::Money.no_currency(0) #=> [XXX 0]
197
+ ### Currency lookup
146
198
 
199
+ ```ruby
200
+ # All registered currencies (150+ ISO 4217 + custom)
201
+ Money::Currency.registered_currencies.size #=> 154
202
+ Money::Currency.registered_currencies.each { |code, c| puts "#{code}: #{c.name}" }
147
203
 
148
- # Proportional allocation and split
204
+ # Built-in ISO 4217 currencies (before custom registrations)
205
+ Money::Currency.world_currencies.size #=> 154
149
206
 
150
- ten.split(3) #=> [[USD 3.34], [USD 3.33], [USD 3.33]]
151
- ten.allocate([1, 2, 3]) #=> [[USD 1.67], [USD 3.33], [USD 5.00]]
207
+ # By ISO code (direct hash lookup, string only)
208
+ Money::Currency.for_code('USD') #=> #<Currency code="USD" ...>
152
209
 
153
- # Clamping to a range
210
+ # By display symbol (highest-priority currency for ambiguous symbols)
211
+ Money::Currency.for_symbol('$') #=> #<Currency code="USD" ...>
212
+ Money::Currency.for_symbol('R$') #=> #<Currency code="BRL" ...>
213
+ Money::Currency.for_symbol('€') #=> #<Currency code="EUR" ...>
214
+ ```
154
215
 
155
- price = Money.from(50, 'USD')
156
- min_price = Money.from(75, 'USD')
216
+ **Polymorphic currency resolution** — `Currency.resolve` also accepts objects that implement `#to_currency` or `#currency_code`:
157
217
 
158
- price.clamp(0, 100) #=> [USD 50.00] (returns self, no new object)
159
- price.clamp(0, 25) #=> [USD 25.00] (clamped to max)
160
- price.clamp(min_price, 100) #=> [USD 75.00] (clamped to min)
218
+ ```ruby
219
+ class Product
220
+ def currency_code = 'USD'
221
+ end
161
222
 
162
- # Clamp accepts Money bounds or Numeric amounts
163
- price.clamp(min_price, 100) #=> [USD 75.00]
223
+ Money::Currency.resolve(Product.new) #=> #<Currency code="USD" ...>
224
+ Money::Currency.resolve!(Product.new) # raises Mint::UnknownCurrency if code is unknown
225
+ ```
164
226
 
165
- # Ranges and enumeration are supported
227
+ `#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
228
 
167
- 1.dollar..10.dollars #=> [USD 1.00]..[USD 10.00]
168
- (1.dollar..3.dollars).step(1.dollar).to_a #=> [[USD 1.00], [USD 2.00], [USD 3.00]]
229
+ ### Crypto currencies
230
+
231
+ 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:
232
+
233
+ ```ruby
234
+ Currency.register_crypto('BTC', 'ETH', 'SOL')
169
235
 
236
+ Money.parse("0.01 BTC") #=> [BTC 0.01000000]
237
+ Mint.money(1, 'ETH') #=> [ETH 1.000000000000000000]
170
238
  ```
171
239
 
172
- ## Parsing strings
240
+ `Currency.crypto_currencies` lists all available definitions without registering:
173
241
 
174
242
  ```ruby
175
- Mint.parse('$19.99') #=> [USD 19.99]
176
- Mint.parse('19,99 €') #=> [EUR 19.99]
177
- Mint.parse('1.234,56', 'EUR') #=> [EUR 1234.56]
178
- Mint.parse('USD 1,234.56') #=> [USD 1234.56]
243
+ Currency.crypto_currencies.each { |c| puts "#{c.code}: #{c.name}" }
244
+ # BTC: Bitcoin
245
+ # ETH: Ethereum
246
+ # SOL: Solana
247
+ # ...
179
248
  ```
180
249
 
181
- Notes:
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]`.
250
+ `register_crypto` raises `KeyError` on duplicate codes and `ArgumentError` on unknown codes. Register all at once:
188
251
 
189
- ## Currency lookup
252
+ ```ruby
253
+ # Register all at once:
254
+ Currency.register_all_crypto # raises KeyError on any conflict
255
+ ```
256
+
257
+ `Currency.crypto_currencies` lists all available definitions without registering:
190
258
 
191
259
  ```ruby
192
- # By ISO code (direct hash lookup, string only)
193
- Mint::Currency.for_code('USD') #=> #<Currency code="USD" ...>
260
+ Currency.crypto_currencies.each { |c| puts "#{c.code}: #{c.name}" }
261
+ # BTC: Bitcoin
262
+ # ETH: Ethereum
263
+ # SOL: Solana
264
+ # ...
265
+ ```
194
266
 
195
- # By display symbol (highest-priority currency for ambiguous symbols)
196
- Mint::Currency.for_symbol('$') #=> #<Currency code="USD" ...>
197
- Mint::Currency.for_symbol('R$') #=> #<Currency code="BRL" ...>
198
- Mint::Currency.for_symbol('€') #=> #<Currency code="EUR" ...>
267
+ ### Locale formatting
199
268
 
269
+ 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:
270
+
271
+ ```ruby
272
+ LOCALE_DATA = {
273
+ 'en' => { decimal: '.', thousand: ',', format: '%<symbol>s%<amount>f' },
274
+ 'pt' => { decimal: ',', thousand: '.', format: '%<symbol>s%<amount>f' },
275
+ 'pt-BR' => { decimal: ',', thousand: '.', format: '%<symbol>s%<amount>f' },
276
+ 'de' => { decimal: ',', thousand: '.', format: '%<amount>f %<currency>s' },
277
+ 'fr' => { decimal: ',', thousand: ' ', format: '%<amount>f %<symbol>s' },
278
+ 'ja' => { decimal: '.', thousand: ',', format: '%<symbol>s%<amount>f' },
279
+ }.freeze
280
+
281
+ Mint.locale_backend = ->(locale) { LOCALE_DATA[locale.to_s] || {} }
282
+
283
+ Mint.money(1234.56, 'USD').format(locale: :en) #=> "$1,234.56"
284
+ Mint.money(9.99, 'BRL').format(locale: 'pt') #=> "R$9,99"
285
+ Mint.money(9.99, 'EUR').format(locale: :de) #=> "9,99 EUR"
286
+ Mint.money(9.99, 'EUR').format(locale: 'fr') #=> "9,99 €"
287
+ Mint.money(9.99, 'USD').format(locale: :ja) #=> "$9.99"
288
+ ```
289
+
290
+ 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.
291
+
292
+ Rails I18n key names (`:separator`, `:delimiter`) are also accepted — no mapping needed:
293
+
294
+ ```ruby
295
+ Mint.locale_backend = ->(locale = nil) {
296
+ I18n.with_locale(locale || I18n.default_locale) do
297
+ I18n.t('number.currency.format', default: {})
298
+ end
299
+ }
300
+ ```
301
+
302
+ Minting names take precedence when both are present (e.g. `{ decimal: '.', separator: ',' }` uses `'.'`).
303
+
304
+ Arity-0 callables (`-> { ... }`) are called without arguments and work unchanged:
305
+
306
+ ```ruby
307
+ Mint.locale_backend = -> { { decimal: ',', thousand: '.' } }
308
+ Mint.money(9.99, 'BRL').format #=> "R$9,99"
200
309
  ```
201
310
 
202
311
  ## API notes
203
312
 
204
313
  **Exact amounts** — Amounts are stored as `Rational` and rounded to the currency subunit.
205
314
 
206
- **Rounding modes** — Wrap operations in `Mint.with_rounding(mode)` to change how amounts are rounded to the subunit:
315
+ **Rounding modes** — Wrap operations in `Money.with_rounding(mode)` to change how amounts are rounded to the subunit:
207
316
 
208
317
  ```ruby
209
- Mint.with_rounding(:half_down) { Money.from(1.005, 'USD') } #=> [USD 1.00]
210
- Mint.with_rounding(:ceil) { Money.from(1.001, 'USD') } #=> [USD 1.01]
211
- Mint.with_rounding(:floor) { Mint.parse('1.009', 'USD') } #=> [USD 1.00]
318
+ Money.with_rounding(:half_down) { Money.from(1.005, 'USD') } #=> [USD 1.00]
319
+ Money.with_rounding(:ceil) { Money.from(1.001, 'USD') } #=> [USD 1.01]
320
+ Money.with_rounding(:floor) { Money.parse('1.009', 'USD') } #=> [USD 1.00]
212
321
  ```
213
322
 
214
323
  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.
215
324
 
216
- > **Performance note:** Rounding-mode support is not loaded by default — `require 'minting'` uses the fastest possible rounding (equivalent to `:half_up`) with zero dispatch overhead. The first call to `Mint.with_rounding` loads the rounding module and patches `Currency#normalize_amount`, adding ~10–35ns per money creation or mutation. If your application never uses custom rounding modes (the common case), there is **no performance cost**.
325
+ > **Performance note:** Rounding-mode support is not loaded by default — `require 'minting'` uses the fastest possible rounding (equivalent to `:half_up`) with zero dispatch overhead. The first call to `Money.with_rounding` loads the rounding module and patches `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
326
 
218
327
  **Division** — `money / 5` returns new `Money`; `money / other_money` returns a numeric ratio, not money.
219
328
 
220
329
  **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
330
 
222
- **Zero helper** — `Currency.zero('USD')` returns a frozen zero-Money, useful as a default value for discounts, totals, or counters.
223
331
 
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.
332
+
333
+ **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.
225
334
 
226
335
  **Built-in currencies** — 150+ ISO-4217 world currencies ship in `lib/minting/data/currencies.yaml` and load when the registry is first accessed.
227
336
 
@@ -232,7 +341,7 @@ By default, `require "minting"` exposes `Mint::Money` as the top-level `Money` c
232
341
  ```ruby
233
342
  require "minting"
234
343
 
235
- price = Money.from(10, "USD") # equivalent to Mint::Money.from
344
+ price = Money.from(10, "USD") # equivalent to Mint::Money.from
236
345
  tax = Money.from(2.50, "USD")
237
346
  ```
238
347
 
@@ -240,26 +349,35 @@ tax = Money.from(2.50, "USD")
240
349
 
241
350
  ```ruby
242
351
  require "minting"
243
- require "minting/mint/aliases" # opt-in top-level Currency
352
+ require "minting/aliases" # opt-in top-level Currency
244
353
 
245
354
  cur = Currency.new(code: "EUR", symbol: "€", subunit: 2, priority: 0)
246
355
  ```
247
356
 
248
- For Rails applications, you can enable the top-level `Currency` constant in an initializer:
357
+ For Rails applications, enable it in an initializer:
249
358
 
250
359
  ```ruby
251
360
  # config/initializers/minting.rb
252
- require "minting/mint/aliases"
361
+ require "minting/aliases"
253
362
  ```
254
363
 
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/mint/aliases`.
364
+ 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
365
 
257
366
  **Good fit:** Application code, especially Rails apps.
258
367
  **Not recommended:** Reusable gems/libraries — stick to `Mint::Money` to avoid conflicts.
259
368
 
369
+ ## Rails integration
370
+
371
+ Minting itself has no Rails dependency. For `ActiveRecord` type casting, validators, and form helpers, pair it with the companion gem:
372
+
373
+ - **[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.
374
+
260
375
  ## Roadmap
261
376
 
262
- - Exchange-rate conversion infrastructure
377
+ Toward a stable 2.0: (no later than August 2026)
378
+
379
+ - Exchange-rate conversion infrastructure (provider abstraction, stateless injection)
380
+ - API surface review — locking in the public interface for `Money`, `Currency`, and formatting before the tag
263
381
 
264
382
  ## License
265
383
 
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 Mint::Currency then other
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, Mint::Currency
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, Mint::Currency
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).
@@ -29,7 +29,7 @@ lib/
29
29
  │ ├── data/
30
30
  │ │ └── currencies.yaml # built-in ISO 4217 database
31
31
  │ ├── mint/
32
- │ │ ├── currency.rb # Mint::Currency model
32
+ │ │ ├── currency.rb # Money::Currency model
33
33
  │ │ ├── refinements.rb # Scoped Numeric/String refinements
34
34
  │ │ └── registry.rb # Global lookup & registration
35
35
  │ ├── money/
@@ -70,7 +70,7 @@ lib/
70
70
 
71
71
  The gem exhibits top-tier Ruby development practices:
72
72
 
73
- - **Immutability & Safety**: Both `Mint::Currency` and `Mint::Money` objects are frozen upon initialization:
73
+ - **Immutability & Safety**: Both `Money::Currency` and `Mint::Money` objects are frozen upon initialization:
74
74
  ```ruby
75
75
  def initialize(amount, currency)
76
76
  # ...
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @!parse
4
+ # # Top-level alias for {Money::Currency}, opt-in via +require 'minting/aliases'+.
5
+ # Currency = Money::Currency
6
+
7
+ # Top-level alias for {Money::Currency}, opt-in via +require 'minting/aliases'+.
8
+ #
9
+ # {::Currency} is **not** auto-bound by `require 'minting'` because
10
+ # application domain models are commonly named +Currency+ (e.g. a Rails
11
+ # model). Load this file explicitly to opt in.
12
+ #
13
+ # If {::Currency} is already defined, a warning is emitted and the existing
14
+ # constant is preserved — use {Money::Currency} explicitly in that case.
15
+ #
16
+ # @see Money::Currency
17
+
18
+ Currency = Money::Currency unless defined?(Currency)
19
+
20
+ if Currency != Money::Currency
21
+ warn "minting: top-level Currency was already defined (#{Currency}); skipping alias. Use Money::Currency"
22
+ end