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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +298 -122
  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 +77 -94
  10. data/lib/minting/currency/registry.rb +134 -0
  11. data/lib/minting/currency/rounding.rb +47 -0
  12. data/lib/minting/data/crypto-currencies.yaml +126 -0
  13. data/lib/minting/mint/i18n.rb +79 -29
  14. data/lib/minting/mint/mint.rb +1 -26
  15. data/lib/minting/mint/registry/crypto.rb +59 -0
  16. data/lib/minting/mint/registry/registration.rb +1 -2
  17. data/lib/minting/mint/registry/registry.rb +9 -11
  18. data/lib/minting/mint/registry/symbols.rb +37 -30
  19. data/lib/minting/mint.rb +3 -2
  20. data/lib/minting/money/allocation/allocation.rb +2 -2
  21. data/lib/minting/money/allocation/split.rb +2 -2
  22. data/lib/minting/money/arithmetics/operators.rb +10 -13
  23. data/lib/minting/money/clamp.rb +6 -6
  24. data/lib/minting/money/coercion.rb +1 -1
  25. data/lib/minting/money/comparable.rb +3 -3
  26. data/lib/minting/money/constructors.rb +3 -42
  27. data/lib/minting/money/conversion.rb +22 -18
  28. data/lib/minting/money/format/format.rb +100 -0
  29. data/lib/minting/money/format/formatter.rb +110 -0
  30. data/lib/minting/money/format/to_s.rb +20 -102
  31. data/lib/minting/money/format/validator.rb +34 -0
  32. data/lib/minting/money/money.rb +25 -9
  33. data/lib/minting/money/parse.rb +127 -0
  34. data/lib/minting/money/rounding.rb +26 -0
  35. data/lib/minting/version.rb +1 -1
  36. data/lib/minting.rb +17 -8
  37. metadata +12 -31
  38. data/doc/Mint/Currency.html +0 -2032
  39. data/doc/Mint/Money.html +0 -5139
  40. data/doc/Mint/RangeStepPatch.html +0 -277
  41. data/doc/Mint/Registry.html +0 -863
  42. data/doc/Mint/Rounding.html +0 -506
  43. data/doc/Mint/UnknownCurrency.html +0 -138
  44. data/doc/Mint.html +0 -931
  45. data/doc/Minting.html +0 -142
  46. data/doc/Numeric.html +0 -479
  47. data/doc/String.html +0 -241
  48. data/doc/_index.html +0 -206
  49. data/doc/class_list.html +0 -54
  50. data/doc/css/common.css +0 -1
  51. data/doc/css/full_list.css +0 -206
  52. data/doc/css/style.css +0 -1089
  53. data/doc/file.README.html +0 -291
  54. data/doc/file_list.html +0 -59
  55. data/doc/frames.html +0 -22
  56. data/doc/index.html +0 -291
  57. data/doc/js/app.js +0 -801
  58. data/doc/js/full_list.js +0 -334
  59. data/doc/js/jquery.js +0 -4
  60. data/doc/method_list.html +0 -758
  61. data/doc/top-level-namespace.html +0 -135
  62. data/lib/minting/mint/aliases.rb +0 -16
  63. data/lib/minting/mint/parser/parser.rb +0 -97
  64. data/lib/minting/mint/parser/separators.rb +0 -41
  65. data/lib/minting/mint/rounding.rb +0 -65
  66. 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: 851c39a123584b8a782ef0c610c5e271239e0f20b948963bf926daf11defa269
4
+ data.tar.gz: 9d9c5aab7a6acc499308b2850157f6e8d4eef8ad01f4b0db4aa75e89067162fb
5
5
  SHA512:
6
- metadata.gz: fd6f1a5b8c3fdd281080743065a366b7a0334423f373a0970ecfff386682e0fd545103a3eb42aa516bca0dfd0a6709d2fbcc422b4966e610ca5efe55ddf270a4
7
- data.tar.gz: 76f592147f190433923741d40707b830448c80a7b3fe003392c9c8740c06a41b8f2433e7d832db27b8758f8d46f272b9fd33d9d2b3e33fb0a2742aa2fc6aa19d
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
  [![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
- [![Test Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/gferraz/minting)
3
+ [![Test Coverage](https://img.shields.io/badge/coverage-94%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)
5
+ [![RubyCritic Score](https://img.shields.io/badge/RubyCritic-93/100-brightgreen)](https://github.com/gferraz/minting)
6
+ [![GitHub last commit](https://img.shields.io/github/last-commit/gferraz/minting)](https://github.com/gferraz/minting/commits/main)
7
+ [![License](https://img.shields.io/github/license/gferraz/minting)](https://github.com/gferraz/minting/blob/main/LICENSE)
9
8
 
10
- ## Quick start
9
+ # Minting
11
10
 
12
- ```ruby
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
- ### Exact precision
24
- Amounts are stored as `Rational` and rounded to the currency subunit. No floating-point surprises, ever.
25
+ ## Quickstart
25
26
 
26
- ### Good performance
27
- Minting is fast! See full benchmarks in the [Performance Guide](bench/BENCHMARKS.md).
27
+ Get started in 60 seconds:
28
28
 
29
- ### Clean, modern API
30
- Intuitive interface, descriptive error messages, and sensible defaults. Works the way you expect.
29
+ ```ruby
30
+ # Add to your Gemfile
31
+ gem "minting"
31
32
 
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.
33
+ # Or install directly
34
+ # gem install minting
34
35
 
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**
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') #=> true
61
- ten = 10.dollars #=> [USD 10.00]
62
- 4.to_money('USD') #=> [USD 4.00]
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 #=> true
66
- ten == Money.from(10, 'EUR') #=> false
67
- ten > Money.from(9.99, 'USD') #=> true
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
- # Format (uses Kernel.format syntax)
79
- price = Money.from(9.99, 'USD')
80
- loss = Money.from(-1234.56, 'USD')
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
- # 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"
186
+ # Clamping to a range
187
+ price = Money.from(50, 'USD')
188
+ min_price = Money.from(75, 'USD')
87
189
 
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"
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
- # 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",
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(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"
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(format: '%<integral>d %<fractional>d/100') #=> "9 99/100"
110
- Money.from(0.99, 'USD').format(format: '%<integral>d dollars and %<fractional>02d cents')
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 = 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:
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(format: fmt) #=> "$1,234.56"
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(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)
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
- # Json serialization
231
+ # Hash serialization
232
+ price.to_hash #=> {currency: "USD", amount: "9.99"}
233
+ ```
127
234
 
128
- price.to_json # => "{\"currency\": \"USD\", \"amount\": \"9.99\"}"
235
+ ### Integral & fractional parts
129
236
 
130
- # Hash conversion
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
- price.to_hash #=> {currency: "USD", amount: "9.99"}
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
- # Fractional units (inverse of #fractional) - exact integer arithmetic
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
- 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
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
- # No currency (ISO 4217 XXX)
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
- Mint::Money.no_currency(100) #=> [XXX 100]
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
- # Proportional allocation and split
280
+ # Built-in ISO 4217 currencies (before custom registrations)
281
+ Money::Currency.world_currencies.size #=> 154
149
282
 
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]]
283
+ # By ISO code (direct hash lookup, string only)
284
+ Money::Currency.for_code('USD') #=> #<Currency code="USD" ...>
152
285
 
153
- # Clamping to a range
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
- price = Money.from(50, 'USD')
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
- 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)
294
+ ```ruby
295
+ class Product
296
+ def currency_code = 'USD'
297
+ end
161
298
 
162
- # Clamp accepts Money bounds or Numeric amounts
163
- price.clamp(min_price, 100) #=> [USD 75.00]
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
- # Ranges and enumeration are supported
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
- 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]]
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
- ## Parsing strings
316
+ `Currency.crypto_currencies` lists all available definitions without registering:
173
317
 
174
318
  ```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]
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
- 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]`.
326
+ `register_crypto` raises `KeyError` on duplicate codes and `ArgumentError` on unknown codes. Register all at once:
188
327
 
189
- ## Currency lookup
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
- # By ISO code (direct hash lookup, string only)
193
- Mint::Currency.for_code('USD') #=> #<Currency code="USD" ...>
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
- # 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" ...>
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 `Mint.with_rounding(mode)` to change how amounts are rounded to the subunit:
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
- 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]
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: `: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.
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 `: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**.
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
- **Built-in currencies** — 150+ ISO-4217 world currencies ship in `lib/minting/data/currencies.yaml` and load when the registry is first accessed.
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
- ## Optional top-level `Money` and `Currency`
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
- By default, `require "minting"` exposes `Mint::Money` as the top-level `Money` constant, so you can write `Money.from(10, "USD")` directly:
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") # equivalent to Mint::Money.from
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/mint/aliases" # opt-in top-level Currency
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, you can enable the top-level `Currency` constant in an initializer:
422
+ For Rails applications, enable it in an initializer:
249
423
 
250
424
  ```ruby
251
425
  # config/initializers/minting.rb
252
- require "minting/mint/aliases"
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/mint/aliases`.
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
- ## Roadmap
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
- - Exchange-rate conversion infrastructure
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 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).