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.
- checksums.yaml +4 -4
- data/README.md +234 -116
- 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 +81 -9
- 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/symbols.rb +37 -30
- data/lib/minting/mint/rounding.rb +9 -7
- data/lib/minting/mint.rb +1 -2
- data/lib/minting/money/allocation/allocation.rb +2 -2
- data/lib/minting/money/allocation/split.rb +1 -1
- 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 +102 -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 +27 -0
- data/lib/minting/version.rb +1 -1
- data/lib/minting.rb +17 -8
- metadata +9 -29
- 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/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: 20bb9d0f4d0437f84ab790d3fdbd27650bee49d0b9d1231b3ba19888987583b4
|
|
4
|
+
data.tar.gz: 8787d2359db2b7c75a2054fdcc6d56525e819f25b4d1cc746d2d6bcb01fd03ff
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
[](https://badge.fury.io/rb/minting)
|
|
6
2
|
[](https://github.com/gferraz/minting/actions/workflows/ci.yml)
|
|
7
3
|
[](https://github.com/gferraz/minting)
|
|
8
4
|
[](https://www.rubydoc.info/gems/minting/frames)
|
|
9
5
|
|
|
10
|
-
|
|
6
|
+
# Minting
|
|
11
7
|
|
|
12
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
Minting is fast! See full benchmarks in the [Performance Guide](bench/BENCHMARKS.md).
|
|
43
|
+
## Why Minting
|
|
28
44
|
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
- **
|
|
37
|
-
- **
|
|
38
|
-
- **
|
|
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')
|
|
61
|
-
ten = 10.dollars
|
|
62
|
-
4.to_money('USD')
|
|
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
|
|
66
|
-
ten == Money.from(10, 'EUR')
|
|
67
|
-
ten > Money.from(9.99, 'USD')
|
|
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
|
-
#
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
Money.from(
|
|
90
|
-
#=> "1.234,56 EUR"
|
|
121
|
+
```ruby
|
|
122
|
+
price = Money.from(9.99, 'USD')
|
|
91
123
|
|
|
92
|
-
#
|
|
93
|
-
price.format #=> "$9.99"
|
|
94
|
-
price.format(
|
|
95
|
-
price.format(
|
|
96
|
-
price.format(
|
|
97
|
-
(-price).format(
|
|
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(
|
|
103
|
-
price.format(
|
|
104
|
-
(-price).format(
|
|
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(
|
|
110
|
-
Money.from(0.99, 'USD').format(
|
|
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
|
|
115
|
-
|
|
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(
|
|
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(
|
|
123
|
-
Money.from(10, 'CAD').format(
|
|
124
|
-
Money.from(10, 'EUR').format(
|
|
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
|
-
#
|
|
155
|
+
# Hash serialization
|
|
156
|
+
price.to_hash #=> {currency: "USD", amount: "9.99"}
|
|
157
|
+
```
|
|
127
158
|
|
|
128
|
-
|
|
159
|
+
### Integral & fractional parts
|
|
129
160
|
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
204
|
+
# Built-in ISO 4217 currencies (before custom registrations)
|
|
205
|
+
Money::Currency.world_currencies.size #=> 154
|
|
149
206
|
|
|
150
|
-
|
|
151
|
-
|
|
207
|
+
# By ISO code (direct hash lookup, string only)
|
|
208
|
+
Money::Currency.for_code('USD') #=> #<Currency code="USD" ...>
|
|
152
209
|
|
|
153
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
218
|
+
```ruby
|
|
219
|
+
class Product
|
|
220
|
+
def currency_code = 'USD'
|
|
221
|
+
end
|
|
161
222
|
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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
|
-
|
|
240
|
+
`Currency.crypto_currencies` lists all available definitions without registering:
|
|
173
241
|
|
|
174
242
|
```ruby
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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
|
-
|
|
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 `
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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 `
|
|
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
|
-
|
|
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")
|
|
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/
|
|
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,
|
|
357
|
+
For Rails applications, enable it in an initializer:
|
|
249
358
|
|
|
250
359
|
```ruby
|
|
251
360
|
# config/initializers/minting.rb
|
|
252
|
-
require "minting/
|
|
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/
|
|
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
|
-
|
|
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
|
|
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).
|
|
@@ -29,7 +29,7 @@ lib/
|
|
|
29
29
|
│ ├── data/
|
|
30
30
|
│ │ └── currencies.yaml # built-in ISO 4217 database
|
|
31
31
|
│ ├── mint/
|
|
32
|
-
│ │ ├── currency.rb #
|
|
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 `
|
|
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
|