minting 2.1.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1cb21299dcb5f2a07ce7ace30463640372647eca9fbbfdf8d667e5a8c12d86b
4
- data.tar.gz: 90838c1838f18a3892cedbe2ea3583d1103356479eb04fe3d51b9214fa05ffa3
3
+ metadata.gz: 4599b829514f80d36867c1ddd7afa3a00a6ea1a718883ce46ea250fc58f4deff
4
+ data.tar.gz: cabdea47ab90088ae53582ccacdf9695a6153230aff0f2e986af00191d8eeb05
5
5
  SHA512:
6
- metadata.gz: db903188b6e59832572d8d7161672f38b293d34bdca45c3d1bfff6bd8906e4f2bfa471530067f31196e619ae0ef9465026f23e3d7e852d9fb1661c742d40c070
7
- data.tar.gz: a27d66b6395e6e980ed22965dbe94abdf9b9ce23f3bfe186c03257e62721b274ebe50703173faa8c1b91ef87aaa8c8f7d35c71ca84f78a4e9c766de6a2614bd1
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.1 is released. The core API (`Money`, `Currency`, formatting, parsing) is stable.
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 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
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 New in 2.1](#whats-new-in-21)
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(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
+ 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 (150+ ISO 4217 + custom)
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 #=> 154
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
- Mint.money(1, 'ETH') #=> [ETH 1.000000000000000000]
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
- 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"
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
- Mint.money(9.99, 'BRL').format #=> "R$9,99"
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, `change`, `split`, and `allocate`. Restores the previous mode when the block exits, even on exception.
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
- **Division** `money / 5` returns new `Money`; `money / other_money` returns a numeric ratio, not money.
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
- ## Optional top-level `Money` (opt-out) and `Currency` (opt-in)
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:all') do |t|
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
- sh "bundle exec ruby bench/check/runner.rb #{baseline}"
47
+ ruby "bench/check/runner.rb #{baseline}"
53
48
  puts "Baseline updated for #{platform}."
54
49
  end
55
50