minting 2.1.0 → 2.2.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 +114 -39
- data/Rakefile +2 -7
- data/doc/agents/AGENTS.md +376 -0
- data/doc/api_review-2026-08-13.md +144 -0
- data/doc/security-report.md +137 -0
- data/lib/minting/aliases.rb +8 -8
- data/lib/minting/currency/registry.rb +0 -3
- data/lib/minting/mint/dsl/numeric.rb +7 -2
- data/lib/minting/mint/dsl/string.rb +1 -1
- data/lib/minting/mint/mint.rb +6 -1
- data/lib/minting/mint/registry/crypto.rb +3 -1
- data/lib/minting/mint/registry/registry.rb +1 -1
- data/lib/minting/money/clamp.rb +6 -16
- data/lib/minting/money/constructors.rb +1 -1
- data/lib/minting/money/conversion.rb +1 -1
- data/lib/minting/money/format/formatter.rb +27 -5
- data/lib/minting/money/format/to_s.rb +4 -3
- data/lib/minting/money/parse.rb +34 -9
- data/lib/minting/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4599b829514f80d36867c1ddd7afa3a00a6ea1a718883ce46ea250fc58f4deff
|
|
4
|
+
data.tar.gz: cabdea47ab90088ae53582ccacdf9695a6153230aff0f2e986af00191d8eeb05
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a1bf6f69bb7513c6ec7b3a0b1e3838e0a87add33c57b82b652d8c4e8951539abfccdbd0cf3fa42220a9dc0efce6b64afb426aebd63c2c4ae3e10614d5245e4c5
|
|
7
|
+
data.tar.gz: 5e896ab547e87ed89c6d2e30252840b43bf5a4808ba49f82ce2b9dcb55a7d9ccaffe75d0f7ec6c228d7d1bad3c93b9bde3112553704a6d1d4e1dd43f636450ed
|
data/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
> **Status:** Minting 2.
|
|
15
|
+
> **Status:** Minting 2.2 is released. The core API (`Money`, `Currency`, formatting, parsing) is stable.
|
|
16
16
|
|
|
17
17
|
```ruby
|
|
18
18
|
price = Money.from(19.99, 'USD') #=> [USD 19.99]
|
|
@@ -57,25 +57,10 @@ shares = total.allocate([1, 2, 3]) # => [16.67, 33.33, 50.00]
|
|
|
57
57
|
parts = total.split(3) # => [33.34, 33.33, 33.33]
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
## What's
|
|
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
|
|
60
|
+
## What's new since 2.0
|
|
76
61
|
|
|
77
62
|
### 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.
|
|
63
|
+
- **Crypto currency support**: Opt-in YAML-backed definitions for ~25 popular coins (BTC, ETH, SOL, ...). Use `Money::Currency.register_crypto('BTC', 'ETH')` to register, or `Money::Currency.crypto_currencies` to inspect available definitions.
|
|
79
64
|
- `Currency.registered_currencies` — public access to all registered currencies (frozen hash)
|
|
80
65
|
- `Money.from_hash(hash)` — deserializer symmetric with `to_hash`, accepts `{ currency:, amount: }`
|
|
81
66
|
- `Money#integral` — returns the whole-unit part of the amount (complement to `#fractional`). `#to_i` is now an alias of `#integral`.
|
|
@@ -88,6 +73,17 @@ parts = total.split(3) # => [33.34, 33.33, 33.33]
|
|
|
88
73
|
- `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
74
|
- `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
75
|
|
|
76
|
+
### Breaking Changes
|
|
77
|
+
- `Money#clamp` no longer accepts `Numeric` bounds — only `Money` or `nil`. Previously `price.clamp(0, 100)` was silently accepted; now it raises `ArgumentError`. Use `price.clamp(0.dollars, 100.dollars)` instead. This prevents accidental misuse where a bare number is passed as a variable.
|
|
78
|
+
- Rounding mode symbols renamed to match `Rational#round` `half:` parameter: `:half_up` → `:up`, `:half_down` → `:down`, `:half_even` → `:even`
|
|
79
|
+
- `Mint.parse` and `Mint.parse!` removed — use `Money.parse` and `Money.parse!`
|
|
80
|
+
- `Mint.with_rounding` removed — use `Money.with_rounding`
|
|
81
|
+
- `Mint.world_currencies` removed — use `Currency.world_currencies`
|
|
82
|
+
- `Money#mint` removed — use `Money#copy_with`
|
|
83
|
+
- `Money::Currency` is the canonical name to access the `Currency` class
|
|
84
|
+
- `Money#format` `formatter_class:` kwarg removed — `Formatter` is now the sole formatter implementation
|
|
85
|
+
- `Money#to_json` and `Money.from_json` — moved to `attribute-money` companion gem
|
|
86
|
+
|
|
91
87
|
### Removed
|
|
92
88
|
- `Money::Formatter2` — removed; `Formatter` is the sole implementation
|
|
93
89
|
- `Money#to_json` and `Money.from_json` — moved to `attribute-money` companion gem
|
|
@@ -98,8 +94,7 @@ Amounts are stored as `Rational`, so there's no floating-point drift — `0.1 +
|
|
|
98
94
|
|
|
99
95
|
- [Quickstart](#quickstart)
|
|
100
96
|
- [Why Minting](#why-minting)
|
|
101
|
-
- [What's
|
|
102
|
-
- [What's New in 2.0](#whats-new-in-20)
|
|
97
|
+
- [What's new since 2.0](#whats-new-since-20)
|
|
103
98
|
- [How it compares](#how-it-compares)
|
|
104
99
|
- [Installation](#installation)
|
|
105
100
|
- [Usage](#usage)
|
|
@@ -156,6 +151,14 @@ gem 'minting'
|
|
|
156
151
|
|
|
157
152
|
### Creating & comparing money
|
|
158
153
|
|
|
154
|
+
`Money.from` is the canonical constructor. The older `Mint.money` factory and
|
|
155
|
+
`Numeric#mint` shortcut are deprecated; use `Money.from` and `Numeric#to_money`
|
|
156
|
+
instead. Named shortcuts such as `dollars`, `euros`, and `reais` remain
|
|
157
|
+
available.
|
|
158
|
+
|
|
159
|
+
Currency arguments accept currency code strings or `Currency` objects. Symbols
|
|
160
|
+
such as `:USD` are not supported.
|
|
161
|
+
|
|
159
162
|
```ruby
|
|
160
163
|
require 'minting'
|
|
161
164
|
|
|
@@ -187,9 +190,10 @@ Money.from(10, 'USD') == 10 #=> false
|
|
|
187
190
|
price = Money.from(50, 'USD')
|
|
188
191
|
min_price = Money.from(75, 'USD')
|
|
189
192
|
|
|
190
|
-
price.clamp(
|
|
191
|
-
price.clamp(0, 25) #=> [USD 25.00] (clamped to max)
|
|
192
|
-
price.clamp(min_price, 100)
|
|
193
|
+
price.clamp(price, Money.from(100, 'USD')) #=> [USD 50.00] (returns self, no new object)
|
|
194
|
+
price.clamp(0.dollars, 25.dollars) #=> [USD 25.00] (clamped to max)
|
|
195
|
+
price.clamp(min_price, 100.dollars) #=> [USD 75.00] (clamped to min)
|
|
196
|
+
|
|
193
197
|
```
|
|
194
198
|
|
|
195
199
|
### Formatting
|
|
@@ -218,6 +222,7 @@ Money.from(0.99, 'USD').format('%<integral>d dollars and %<fractional>02d cents'
|
|
|
218
222
|
#=> "0 dollars and 99 cents"
|
|
219
223
|
|
|
220
224
|
# Per-sign Hash format (e.g. accounting parentheses for losses)
|
|
225
|
+
loss = Money.from(-1234.56, 'USD')
|
|
221
226
|
loss.format( { negative: '(%<symbol>s%<amount>f)' }) #=> "($1,234.56)"
|
|
222
227
|
Money.from(0, 'BRL').format( { zero: '--' }) #=> "--"
|
|
223
228
|
fmt = { positive: '%<symbol>s%<amount>f', negative: '(%<symbol>s%<amount>f)', zero: '--' }
|
|
@@ -264,8 +269,14 @@ Money.parse('USD 1,234.56') #=> [USD 1234.56]
|
|
|
264
269
|
|
|
265
270
|
Notes:
|
|
266
271
|
- Pass a currency code when the string has no symbol or code.
|
|
272
|
+
- The optional currency argument is a default, not an override: a code or symbol embedded in the string takes precedence (`Money.parse('10 EUR', 'USD')` returns EUR).
|
|
267
273
|
- `1,234` means 1234, not 1.234, and `1,23` means 1.23, not 123.
|
|
268
274
|
- `1,234.00` is unambiguous (thousands + decimal).
|
|
275
|
+
- Parsing is separator-positional rather than locale-aware. For currencies
|
|
276
|
+
with three decimal places, a single comma followed by three digits is read
|
|
277
|
+
as a thousands separator: `Money.parse('KWD 861,949')` means 861949 KWD.
|
|
278
|
+
Use a period decimal (`KWD 861.949`) or both separators (`KWD 1.234,567`)
|
|
279
|
+
when the intended value has three fractional digits.
|
|
269
280
|
- Accounting negatives like `($1.23)` or `(USD 10.00)` are supported — the parser detects parentheses and negates the amount.
|
|
270
281
|
- Ambiguous symbols like `$` resolve by currency priority (currently USD).
|
|
271
282
|
- 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]`.
|
|
@@ -273,12 +284,12 @@ Notes:
|
|
|
273
284
|
### Currency lookup
|
|
274
285
|
|
|
275
286
|
```ruby
|
|
276
|
-
# All registered currencies (
|
|
287
|
+
# All registered currencies (currently 164 built-in ISO 4217 + custom)
|
|
277
288
|
Money::Currency.registered_currencies.size #=> 164
|
|
278
289
|
Money::Currency.registered_currencies.each { |code, c| puts "#{code}: #{c.name}" }
|
|
279
290
|
|
|
280
291
|
# Built-in ISO 4217 currencies (before custom registrations)
|
|
281
|
-
Money::Currency.world_currencies.size #=>
|
|
292
|
+
Money::Currency.world_currencies.size #=> 164
|
|
282
293
|
|
|
283
294
|
# By ISO code (direct hash lookup, string only)
|
|
284
295
|
Money::Currency.for_code('USD') #=> #<Currency code="USD" ...>
|
|
@@ -307,16 +318,16 @@ Money::Currency.resolve!(Product.new) # raises Mint::UnknownCurrency if code is
|
|
|
307
318
|
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
319
|
|
|
309
320
|
```ruby
|
|
310
|
-
Currency.register_crypto('BTC', 'ETH', 'SOL')
|
|
321
|
+
Money::Currency.register_crypto('BTC', 'ETH', 'SOL')
|
|
311
322
|
|
|
312
323
|
Money.parse("0.01 BTC") #=> [BTC 0.01000000]
|
|
313
|
-
|
|
324
|
+
Money.from(1, 'ETH') #=> [ETH 1.000000000000000000]
|
|
314
325
|
```
|
|
315
326
|
|
|
316
|
-
`Currency.crypto_currencies` lists all available definitions without registering:
|
|
327
|
+
`Money::Currency.crypto_currencies` lists all available definitions without registering:
|
|
317
328
|
|
|
318
329
|
```ruby
|
|
319
|
-
Currency.crypto_currencies.each { |c| puts "#{c.code}: #{c.name}" }
|
|
330
|
+
Money::Currency.crypto_currencies.each { |c| puts "#{c.code}: #{c.name}" }
|
|
320
331
|
# BTC: Bitcoin
|
|
321
332
|
# ETH: Ethereum
|
|
322
333
|
# SOL: Solana
|
|
@@ -346,11 +357,11 @@ LOCALE_DATA = {
|
|
|
346
357
|
|
|
347
358
|
Mint.locale_backend = ->(locale) { LOCALE_DATA[locale.to_s] || {} }
|
|
348
359
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
360
|
+
Money.from(1234.56, 'USD').format(locale: :en) #=> "$1,234.56"
|
|
361
|
+
Money.from(9.99, 'BRL').format(locale: 'pt') #=> "R$9,99"
|
|
362
|
+
Money.from(9.99, 'EUR').format(locale: :de) #=> "9,99 EUR"
|
|
363
|
+
Money.from(9.99, 'EUR').format(locale: 'fr') #=> "9,99 €"
|
|
364
|
+
Money.from(9.99, 'USD').format(locale: :ja) #=> "$9.99"
|
|
354
365
|
```
|
|
355
366
|
|
|
356
367
|
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.
|
|
@@ -371,7 +382,7 @@ Arity-0 callables (`-> { ... }`) are called without arguments and work unchanged
|
|
|
371
382
|
|
|
372
383
|
```ruby
|
|
373
384
|
Mint.locale_backend = -> { { decimal: ',', thousand: '.' } }
|
|
374
|
-
|
|
385
|
+
Money.from(9.99, 'BRL').format #=> "R$9,99"
|
|
375
386
|
```
|
|
376
387
|
|
|
377
388
|
## API notes
|
|
@@ -386,21 +397,85 @@ Money.with_rounding(:even) { Money.from(1.015, 'USD') } #=> [USD 1.02]
|
|
|
386
397
|
Money.with_rounding(:up) { Money.from(1.005, 'USD') } #=> [USD 1.01]
|
|
387
398
|
```
|
|
388
399
|
|
|
389
|
-
Modes: `:up` (default), `:down`, `:even`. Applies to construction, parsing,
|
|
400
|
+
Modes: `:up` (default), `:down`, `:even`. Applies to construction, parsing,
|
|
401
|
+
`copy_with`, `split`, and `allocate`. Restores the previous mode when the block
|
|
402
|
+
exits, even on exception.
|
|
390
403
|
|
|
391
404
|
> **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**.
|
|
392
405
|
|
|
393
|
-
|
|
406
|
+
### Common API workflows
|
|
407
|
+
|
|
408
|
+
**Parsing failures** — `Money.parse` returns `nil` for invalid input or an
|
|
409
|
+
unresolved currency. `Money.parse!` raises `ArgumentError` instead:
|
|
410
|
+
|
|
411
|
+
```ruby
|
|
412
|
+
Money.parse('bad', 'USD') #=> nil
|
|
413
|
+
Money.parse!('bad', 'USD') #=> raises ArgumentError
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
The optional currency argument is a default. Embedded currency information wins:
|
|
417
|
+
|
|
418
|
+
```ruby
|
|
419
|
+
Money.parse('10', 'USD') #=> [USD 10.00]
|
|
420
|
+
Money.parse('10 EUR', 'USD') #=> [EUR 10.00]
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
**Arithmetic and division** — Money can be added or subtracted only with the
|
|
424
|
+
same currency. Multiplication and scalar division return Money; dividing by
|
|
425
|
+
same-currency Money returns a numeric ratio. Non-zero cross-currency operations
|
|
426
|
+
and reverse numeric division raise `TypeError`.
|
|
427
|
+
|
|
428
|
+
```ruby
|
|
429
|
+
price = Money.from(10, 'USD')
|
|
430
|
+
price + Money.from(2, 'USD') #=> [USD 12.00]
|
|
431
|
+
price * 1.5 #=> [USD 15.00]
|
|
432
|
+
price / 2 #=> [USD 5.00]
|
|
433
|
+
price / Money.from(5, 'USD') #=> (2/1)
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
**Clamping** — Bounds must be Money objects in the same currency or `nil`.
|
|
437
|
+
An inclusive Range can be passed as the only argument:
|
|
438
|
+
|
|
439
|
+
```ruby
|
|
440
|
+
price.clamp(Money.from(0, 'USD'), Money.from(8, 'USD')) #=> [USD 8.00]
|
|
441
|
+
price.clamp(Money.from(8, 'USD')..Money.from(12, 'USD')) #=> [USD 10.00]
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
**Allocation** — `split(n)` requires a positive integer. `allocate(ratios)`
|
|
445
|
+
requires a non-empty list whose total is non-zero. Both preserve the original
|
|
446
|
+
amount after subunit rounding; any leftover smallest units go to the first
|
|
447
|
+
slots. Ratios may be negative, but use them only when that distribution is
|
|
448
|
+
intentional.
|
|
449
|
+
|
|
450
|
+
```ruby
|
|
451
|
+
Money.from(10, 'USD').split(3) #=> [[USD 3.34], [USD 3.33], [USD 3.33]]
|
|
452
|
+
Money.from(10, 'USD').allocate([1, 2, 3]) #=> [[USD 1.67], [USD 3.33], [USD 5.00]]
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
**Conversions and serialization** — `to_r` is exact, `to_d` returns a
|
|
456
|
+
BigDecimal, and `to_f` can lose precision. `to_hash` and `from_hash` provide a
|
|
457
|
+
string-safe round trip:
|
|
458
|
+
|
|
459
|
+
```ruby
|
|
460
|
+
money = Money.from(9.99, 'USD')
|
|
461
|
+
money.to_r #=> (999/100)
|
|
462
|
+
money.to_f #=> 9.99
|
|
463
|
+
Money.from_hash(money.to_hash) #=> [USD 9.99]
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
**Money ranges** — Money ranges use `succ` and a Money step. Ruby 4.0 supports
|
|
467
|
+
non-numeric steps natively; Minting patches `Range#step` for Money only on
|
|
468
|
+
older supported Rubies.
|
|
394
469
|
|
|
395
470
|
**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.
|
|
396
471
|
|
|
397
472
|
|
|
398
473
|
|
|
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.
|
|
474
|
+
**Registered currencies** — `Money::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.
|
|
400
475
|
|
|
401
476
|
**Built-in currencies** — 150+ ISO-4217 world currencies ship in `lib/minting/data/world-currencies.yaml` and are preloaded at gem initialization.
|
|
402
477
|
|
|
403
|
-
##
|
|
478
|
+
## Top-level aliases
|
|
404
479
|
|
|
405
480
|
By default, `require "minting"` exposes `Mint::Money` as the top-level `Money` constant, so you can write `Money.from(10, "USD")` directly:
|
|
406
481
|
```ruby
|
data/Rakefile
CHANGED
|
@@ -13,12 +13,7 @@ Rake::TestTask.new(:test) do |t|
|
|
|
13
13
|
t.ruby_opts << '-rtest_helper.rb'
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
Rake::TestTask.new('bench:
|
|
17
|
-
t.libs = %w[lib bench]
|
|
18
|
-
t.pattern = 'bench/{core,memory,regression}/*_benchmark.rb'
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
Rake::TestTask.new('bench:core') do |t|
|
|
16
|
+
Rake::TestTask.new('bench:parse') do |t|
|
|
22
17
|
t.libs = %w[lib bench]
|
|
23
18
|
t.pattern = 'bench/core/parse_benchmark.rb'
|
|
24
19
|
end
|
|
@@ -49,7 +44,7 @@ desc 'Run core benchmarks and update the baseline'
|
|
|
49
44
|
task 'bench:baseline' do
|
|
50
45
|
platform = RUBY_PLATFORM
|
|
51
46
|
baseline = "bench/check/results/baseline-#{platform}.json"
|
|
52
|
-
|
|
47
|
+
ruby "bench/check/runner.rb #{baseline}"
|
|
53
48
|
puts "Baseline updated for #{platform}."
|
|
54
49
|
end
|
|
55
50
|
|