minting 2.0.0 → 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.
- checksums.yaml +4 -4
- data/README.md +86 -28
- data/lib/minting/currency/currency.rb +67 -156
- data/lib/minting/currency/registry.rb +134 -0
- data/lib/minting/currency/rounding.rb +47 -0
- data/lib/minting/mint/registry/registry.rb +9 -11
- data/lib/minting/mint.rb +2 -0
- data/lib/minting/money/allocation/split.rb +1 -1
- data/lib/minting/money/format/formatter.rb +20 -12
- data/lib/minting/money/rounding.rb +8 -9
- data/lib/minting/version.rb +1 -1
- metadata +4 -3
- data/lib/minting/mint/rounding.rb +0 -67
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 851c39a123584b8a782ef0c610c5e271239e0f20b948963bf926daf11defa269
|
|
4
|
+
data.tar.gz: 9d9c5aab7a6acc499308b2850157f6e8d4eef8ad01f4b0db4aa75e89067162fb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d6cca508a6ec6690436d67f70720db03bcebabeb7b561a4769aca306d9350f1d40ba07af63500316c236094dca13b7a6c181d49b324729e7439deff05f69513a
|
|
7
|
+
data.tar.gz: 419ce736af2fd00d90834dc6b82e5459ff83873922871a40d89d62dda69603ce31a5456918fa61ba21564489019eba6c80b1589cefce19841410a6e328ee45fa
|
data/README.md
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
[](https://badge.fury.io/rb/minting)
|
|
2
2
|
[](https://github.com/gferraz/minting/actions/workflows/ci.yml)
|
|
3
|
-
[](https://github.com/gferraz/minting)
|
|
4
4
|
[](https://www.rubydoc.info/gems/minting/frames)
|
|
5
|
+
[](https://github.com/gferraz/minting)
|
|
6
|
+
[](https://github.com/gferraz/minting/commits/main)
|
|
7
|
+
[](https://github.com/gferraz/minting/blob/main/LICENSE)
|
|
5
8
|
|
|
6
9
|
# Minting
|
|
7
10
|
|
|
@@ -9,7 +12,7 @@
|
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
|
|
12
|
-
> **Status:** Minting 2.
|
|
15
|
+
> **Status:** Minting 2.1 is released. The core API (`Money`, `Currency`, formatting, parsing) is stable.
|
|
13
16
|
|
|
14
17
|
```ruby
|
|
15
18
|
price = Money.from(19.99, 'USD') #=> [USD 19.99]
|
|
@@ -19,11 +22,84 @@ total = price + tax #=> [USD 21.59]
|
|
|
19
22
|
total.to_s #=> "$21.59"
|
|
20
23
|
```
|
|
21
24
|
|
|
25
|
+
## Quickstart
|
|
26
|
+
|
|
27
|
+
Get started in 60 seconds:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
# Add to your Gemfile
|
|
31
|
+
gem "minting"
|
|
32
|
+
|
|
33
|
+
# Or install directly
|
|
34
|
+
# gem install minting
|
|
35
|
+
|
|
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
|
+
|
|
22
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.
|
|
23
96
|
|
|
24
97
|
## Table of contents
|
|
25
98
|
|
|
99
|
+
- [Quickstart](#quickstart)
|
|
26
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)
|
|
27
103
|
- [How it compares](#how-it-compares)
|
|
28
104
|
- [Installation](#installation)
|
|
29
105
|
- [Usage](#usage)
|
|
@@ -198,7 +274,7 @@ Notes:
|
|
|
198
274
|
|
|
199
275
|
```ruby
|
|
200
276
|
# All registered currencies (150+ ISO 4217 + custom)
|
|
201
|
-
Money::Currency.registered_currencies.size #=>
|
|
277
|
+
Money::Currency.registered_currencies.size #=> 164
|
|
202
278
|
Money::Currency.registered_currencies.each { |code, c| puts "#{code}: #{c.name}" }
|
|
203
279
|
|
|
204
280
|
# Built-in ISO 4217 currencies (before custom registrations)
|
|
@@ -254,16 +330,6 @@ Currency.crypto_currencies.each { |c| puts "#{c.code}: #{c.name}" }
|
|
|
254
330
|
Currency.register_all_crypto # raises KeyError on any conflict
|
|
255
331
|
```
|
|
256
332
|
|
|
257
|
-
`Currency.crypto_currencies` lists all available definitions without registering:
|
|
258
|
-
|
|
259
|
-
```ruby
|
|
260
|
-
Currency.crypto_currencies.each { |c| puts "#{c.code}: #{c.name}" }
|
|
261
|
-
# BTC: Bitcoin
|
|
262
|
-
# ETH: Ethereum
|
|
263
|
-
# SOL: Solana
|
|
264
|
-
# ...
|
|
265
|
-
```
|
|
266
|
-
|
|
267
333
|
### Locale formatting
|
|
268
334
|
|
|
269
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:
|
|
@@ -315,14 +381,14 @@ Mint.money(9.99, 'BRL').format #=> "R$9,99"
|
|
|
315
381
|
**Rounding modes** — Wrap operations in `Money.with_rounding(mode)` to change how amounts are rounded to the subunit:
|
|
316
382
|
|
|
317
383
|
```ruby
|
|
318
|
-
Money.with_rounding(:
|
|
319
|
-
Money.with_rounding(:
|
|
320
|
-
Money.with_rounding(:
|
|
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]
|
|
321
387
|
```
|
|
322
388
|
|
|
323
|
-
Modes: `:
|
|
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.
|
|
324
390
|
|
|
325
|
-
> **Performance note:** Rounding-mode support is not loaded by default — `require 'minting'` uses the fastest possible rounding (equivalent to `:
|
|
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**.
|
|
326
392
|
|
|
327
393
|
**Division** — `money / 5` returns new `Money`; `money / other_money` returns a numeric ratio, not money.
|
|
328
394
|
|
|
@@ -332,12 +398,11 @@ Modes: `:half_up` (default), `:half_down`, `:floor`, `:ceil`, `:truncate`, `:dow
|
|
|
332
398
|
|
|
333
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.
|
|
334
400
|
|
|
335
|
-
**Built-in currencies** — 150+ ISO-4217 world currencies ship in `lib/minting/data/currencies.yaml` and
|
|
401
|
+
**Built-in currencies** — 150+ ISO-4217 world currencies ship in `lib/minting/data/world-currencies.yaml` and are preloaded at gem initialization.
|
|
336
402
|
|
|
337
|
-
## Optional top-level `Money` and `Currency`
|
|
403
|
+
## Optional top-level `Money` (opt-out) and `Currency` (opt-in)
|
|
338
404
|
|
|
339
405
|
By default, `require "minting"` exposes `Mint::Money` as the top-level `Money` constant, so you can write `Money.from(10, "USD")` directly:
|
|
340
|
-
|
|
341
406
|
```ruby
|
|
342
407
|
require "minting"
|
|
343
408
|
|
|
@@ -372,13 +437,6 @@ Minting itself has no Rails dependency. For `ActiveRecord` type casting, validat
|
|
|
372
437
|
|
|
373
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.
|
|
374
439
|
|
|
375
|
-
## Roadmap
|
|
376
|
-
|
|
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
|
|
381
|
-
|
|
382
440
|
## License
|
|
383
441
|
|
|
384
442
|
MIT
|
|
@@ -6,19 +6,40 @@ module Mint
|
|
|
6
6
|
# Currency objects are immutable and define the properties of a monetary unit
|
|
7
7
|
# including its subunit precision, display symbol, and formatting rules.
|
|
8
8
|
#
|
|
9
|
+
# Currency identity is defined by its ISO code — two Currency objects with
|
|
10
|
+
# the same +code+ are considered equal regardless of other attributes.
|
|
11
|
+
# The Registry guarantees that only one canonical Currency exists per code.
|
|
12
|
+
#
|
|
9
13
|
# @see https://www.iso.org/iso-4217-currency-codes.html
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
class Currency
|
|
15
|
+
# @return [String] ISO 4217 currency code (e.g., "USD", "EUR")
|
|
16
|
+
attr_reader :code
|
|
17
|
+
|
|
18
|
+
# @return [String, nil] Associated country code
|
|
19
|
+
attr_reader :country
|
|
20
|
+
|
|
21
|
+
# @return [String, nil] A longer, code-prefixed variant to distinguish
|
|
22
|
+
# currencies that share the same primary symbol (e.g. "US$" for USD, "C$" for CAD).
|
|
23
|
+
attr_reader :disambiguate_symbol
|
|
24
|
+
|
|
25
|
+
# @return [Integer] 10^subunit, used for fractional conversions
|
|
26
|
+
attr_reader :fractional_multiplier
|
|
27
|
+
|
|
28
|
+
# @return [Rational] Smallest representable amount (1/fractional_multiplier)
|
|
29
|
+
attr_reader :minimum_amount
|
|
30
|
+
|
|
31
|
+
# @return [String, nil] Currency name
|
|
32
|
+
attr_reader :name
|
|
33
|
+
|
|
34
|
+
# @return [Integer] Parser precedence for symbol detection
|
|
35
|
+
attr_reader :priority
|
|
36
|
+
|
|
37
|
+
# @return [Integer] Number of decimal places (0 for JPY, 2 for USD, 3 for IQD)
|
|
38
|
+
attr_reader :subunit
|
|
39
|
+
|
|
40
|
+
# @return [String, nil] Display symbol (e.g., "$", "€", "R$")
|
|
41
|
+
attr_reader :symbol
|
|
42
|
+
|
|
22
43
|
# @param code [String] ISO 4217 currency code
|
|
23
44
|
# @param symbol [String] Display symbol
|
|
24
45
|
# @param subunit [Integer] Number of decimal places (default 0)
|
|
@@ -27,33 +48,54 @@ module Mint
|
|
|
27
48
|
# @param name [String, nil] Currency name (default nil)
|
|
28
49
|
def initialize(code:, symbol:, subunit: 0, priority: 0, country: nil, name: nil,
|
|
29
50
|
disambiguate_symbol: nil)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
51
|
+
@code = code
|
|
52
|
+
@country = country
|
|
53
|
+
@name = name
|
|
54
|
+
@priority = priority.to_i
|
|
55
|
+
@subunit = subunit.to_i
|
|
56
|
+
@symbol = symbol.nil? || symbol.empty? ? nil : symbol
|
|
57
|
+
|
|
58
|
+
@fractional_multiplier = 10**@subunit
|
|
59
|
+
@minimum_amount = Rational(1, @fractional_multiplier)
|
|
60
|
+
@disambiguate_symbol = [code, @symbol].include?(disambiguate_symbol) ? nil : disambiguate_symbol
|
|
61
|
+
freeze
|
|
37
62
|
end
|
|
38
63
|
|
|
64
|
+
# Two Currency objects are equal if they share the same ISO code.
|
|
65
|
+
def ==(other) = other.is_a?(self.class) && code == other.code
|
|
66
|
+
|
|
67
|
+
# @return [String, nil] disambiguate_symbol or code/symbol fallback
|
|
68
|
+
def dsymbol = disambiguate_symbol || (Registry.symbol_shared?(symbol) ? code : symbol)
|
|
69
|
+
|
|
70
|
+
# Currency identity is by code — two objects with the same code are +eql?+
|
|
71
|
+
# regardless of other attributes. This makes Currency usable as a Hash key
|
|
72
|
+
# where lookup is by currency identity (ISO code).
|
|
73
|
+
def eql?(other) = other.is_a?(Currency) && code == other.code
|
|
74
|
+
|
|
75
|
+
# @return [Integer] stable hash based on currency code
|
|
76
|
+
def hash = code.hash
|
|
77
|
+
|
|
39
78
|
# @return [String] debug representation
|
|
40
79
|
def inspect = "<Currency:(#{code} #{symbol} #{subunit} #{name})>"
|
|
41
80
|
|
|
42
|
-
# @return [Rational] smallest representable amount (1/fractional_multiplier)
|
|
43
|
-
def minimum_amount = Rational(1, fractional_multiplier)
|
|
44
|
-
|
|
45
81
|
# Normalizes a numeric amount for this currency.
|
|
46
82
|
#
|
|
47
83
|
# @param amount [Numeric] the monetary amount to normalize
|
|
48
84
|
# @return [Rational] the amount converted to +Rational+ and rounded to
|
|
49
|
-
# the currency's subunit precision (
|
|
85
|
+
# the currency's subunit precision (up by default)
|
|
50
86
|
# @example
|
|
51
87
|
# usd = Money::Currency.for_code('USD')
|
|
52
88
|
# usd.normalize_amount(10.567) #=> (10567/1000)
|
|
53
89
|
# usd.normalize_amount("5.25") #=> (21/4)
|
|
54
90
|
#
|
|
55
|
-
# @see
|
|
56
|
-
def normalize_amount(amount)
|
|
91
|
+
# @see Money.with_rounding Custom rounding modes via {Money.with_rounding}
|
|
92
|
+
def normalize_amount(amount)
|
|
93
|
+
if Currency.custom_rounding_active?
|
|
94
|
+
amount.to_r.round(subunit, half: Thread.current[Currency::ROUNDING_THREAD_KEY])
|
|
95
|
+
else
|
|
96
|
+
amount.to_r.round(subunit)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
57
99
|
|
|
58
100
|
# Returns the cached frozen zero-Money for this currency.
|
|
59
101
|
#
|
|
@@ -61,136 +103,5 @@ module Mint
|
|
|
61
103
|
# @example
|
|
62
104
|
# Money::Currency.for_code('USD').zero #=> [USD 0.00]
|
|
63
105
|
def zero = Registry.zero_for(self)
|
|
64
|
-
|
|
65
|
-
def dsymbol
|
|
66
|
-
disambiguate_symbol || (Registry.symbol_shared?(symbol) ? code : symbol)
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Registers a new currency, raising a KeyError if already registered.
|
|
71
|
-
#
|
|
72
|
-
# @param code [String] the unique currency code
|
|
73
|
-
# @param subunit [Integer] the decimal subunit precision, defaults to 0
|
|
74
|
-
# @param symbol [String] the display symbol
|
|
75
|
-
# @param priority [Integer] parser precedence priority
|
|
76
|
-
# @return [Currency] the newly registered Currency instance
|
|
77
|
-
# @raise [ArgumentError] if the code contains invalid characters
|
|
78
|
-
# @raise [KeyError] if the currency code is already registered
|
|
79
|
-
def Currency.register(code:, subunit: 0, symbol: '', priority: 0)
|
|
80
|
-
Registry.register(code:, subunit:, symbol:, priority:)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Resolves an object into a {Currency}, returning +nil+ when it can't.
|
|
84
|
-
#
|
|
85
|
-
# Accepts +nil+, +String+, {Currency}, {Money}, or any object implementing
|
|
86
|
-
# +#to_currency+ (must return {Currency}) or +#currency_code+ (must return +String+).
|
|
87
|
-
#
|
|
88
|
-
# @param object [String, Currency, Money, nil, #to_currency, #currency_code]
|
|
89
|
-
# a currency code, object, or +nil+
|
|
90
|
-
# @return [Currency, nil] the resolved Currency, or +nil+ if +object+ is +nil+
|
|
91
|
-
# or the code is not registered
|
|
92
|
-
# @raise [ArgumentError] if +object+ is an unsupported type, or if the method
|
|
93
|
-
# used to resolve it returns a value of the wrong type
|
|
94
|
-
def Currency.resolve(object)
|
|
95
|
-
case object
|
|
96
|
-
when NilClass then nil
|
|
97
|
-
when Currency then object
|
|
98
|
-
when Money then object.currency
|
|
99
|
-
when String then Currency.for_code object
|
|
100
|
-
else
|
|
101
|
-
if object.respond_to?(:to_currency)
|
|
102
|
-
result = object.to_currency
|
|
103
|
-
unless result.is_a?(Currency)
|
|
104
|
-
raise ArgumentError, "#to_currency must return a [Money::Currency], got #{result.class}"
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
result
|
|
108
|
-
elsif object.respond_to?(:currency_code)
|
|
109
|
-
result = object.currency_code
|
|
110
|
-
raise ArgumentError, "#currency_code must return a [String], got #{result.class}" unless result.is_a?(String)
|
|
111
|
-
|
|
112
|
-
Currency.for_code result
|
|
113
|
-
else
|
|
114
|
-
raise ArgumentError, "currency must be [Money::Currency], [Money], [String] or nil (#{object})"
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
# Resolves an object into a {Currency}, raising on failure.
|
|
120
|
-
#
|
|
121
|
-
# Like {.resolve} but raises when the result would be +nil+.
|
|
122
|
-
#
|
|
123
|
-
# @param object [String, Currency, Money, nil] a currency code, object, or +nil+
|
|
124
|
-
# @return [Currency] the resolved Currency
|
|
125
|
-
# @raise [Mint::UnknownCurrency] if +object+ cannot be resolved into a
|
|
126
|
-
# registered currency. +Mint::UnknownCurrency+ inherits from +ArgumentError+,
|
|
127
|
-
# so existing +rescue ArgumentError+ handlers continue to work.
|
|
128
|
-
def Currency.resolve!(object)
|
|
129
|
-
resolve(object) or raise Mint::UnknownCurrency, "Could not resolve (#{object}) into a currency"
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# Returns all registered currencies as a frozen hash keyed by ISO code.
|
|
133
|
-
#
|
|
134
|
-
# @return [Hash{String => Currency}] frozen hash of all registered currencies
|
|
135
|
-
# @example Iterate over registered currencies
|
|
136
|
-
# Currency.registered_currencies.each { |code, currency| puts "#{code}: #{currency.name}" }
|
|
137
|
-
# @example Count of registered currencies
|
|
138
|
-
# Currency.registered_currencies.size #=> 154
|
|
139
|
-
def Currency.registered_currencies = Registry.currencies
|
|
140
|
-
|
|
141
|
-
# Looks up a registered currency by its alpha code.
|
|
142
|
-
#
|
|
143
|
-
# @param code [String] the currency code
|
|
144
|
-
# @return [Currency, nil] the registered Currency, or +nil+ if not found
|
|
145
|
-
def Currency.for_code(code)
|
|
146
|
-
Registry.currencies[code]
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# Looks up a currency by its display symbol.
|
|
150
|
-
#
|
|
151
|
-
# @param symbol [String] the display symbol (e.g. "$", "R$")
|
|
152
|
-
# @return [Currency, nil] the highest-priority currency for the symbol
|
|
153
|
-
def Currency.for_symbol(symbol)
|
|
154
|
-
Registry.currency_for_symbol(symbol)
|
|
155
106
|
end
|
|
156
|
-
|
|
157
|
-
# Returns a zero {Money} in the given currency, useful as a default value
|
|
158
|
-
# for discounts, totals, or placeholders.
|
|
159
|
-
#
|
|
160
|
-
# @param currency [String, Currency] a currency code or object
|
|
161
|
-
# @return [Money] a frozen zero-Money
|
|
162
|
-
# @raise [Mint::UnknownCurrency] if the currency can't be resolved
|
|
163
|
-
def Currency.zero(currency) = Registry.zero_for(Currency.resolve!(currency))
|
|
164
|
-
|
|
165
|
-
# Returns the frozen hash of all built-in ISO 4217 world currencies.
|
|
166
|
-
#
|
|
167
|
-
# @return [Hash{String => Currency}] ISO-4217 world currencies mapped by code
|
|
168
|
-
# @api private
|
|
169
|
-
def Currency.world_currencies = Registry.world_currencies
|
|
170
|
-
|
|
171
|
-
# Returns the list of built-in crypto currency definitions.
|
|
172
|
-
#
|
|
173
|
-
# These are not registered by default — call {.register_crypto} to opt in.
|
|
174
|
-
#
|
|
175
|
-
# @return [Array<Currency>] frozen array of crypto currency definitions
|
|
176
|
-
def Currency.crypto_currencies = Registry.crypto_currencies
|
|
177
|
-
|
|
178
|
-
# Registers one or more crypto currencies into the shared currency registry.
|
|
179
|
-
#
|
|
180
|
-
# Raises on duplicate registration or unknown code — use +rescue+ if
|
|
181
|
-
# idempotent bulk registration is needed.
|
|
182
|
-
#
|
|
183
|
-
# @param codes [Array<String>] one or more crypto currency codes
|
|
184
|
-
# @raise [ArgumentError] if a code is not a known crypto currency
|
|
185
|
-
# @raise [KeyError] if the currency code is already registered
|
|
186
|
-
# @return [Array<Currency>] the newly registered Currency objects
|
|
187
|
-
def Currency.register_crypto(...) = Registry.register_crypto(...)
|
|
188
|
-
|
|
189
|
-
# Registers all built-in crypto currencies at once.
|
|
190
|
-
#
|
|
191
|
-
# Raises on the first duplicate — call +rescue+ if idempotency is needed.
|
|
192
|
-
#
|
|
193
|
-
# @raise [KeyError] if any currency code is already registered
|
|
194
|
-
# @return [Array<Currency>] the newly registered Currency objects
|
|
195
|
-
def Currency.register_all_crypto = Registry.register_all_crypto
|
|
196
107
|
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# :nodoc:
|
|
4
|
+
module Mint
|
|
5
|
+
# Class-level methods on {Currency} that delegate to the {Registry}.
|
|
6
|
+
# Separated from the core class definition for readability.
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
9
|
+
|
|
10
|
+
# Returns the list of built-in crypto currency definitions.
|
|
11
|
+
#
|
|
12
|
+
# These are not registered by default — call {.register_crypto} to opt in.
|
|
13
|
+
#
|
|
14
|
+
# @return [Array<Currency>] frozen array of crypto currency definitions
|
|
15
|
+
def Currency.crypto_currencies = Registry.crypto_currencies
|
|
16
|
+
|
|
17
|
+
# Looks up a registered currency by its alpha code.
|
|
18
|
+
#
|
|
19
|
+
# @param code [String] the currency code
|
|
20
|
+
# @return [Currency, nil] the registered Currency, or +nil+ if not found
|
|
21
|
+
def Currency.for_code(code) = Registry.currencies[code]
|
|
22
|
+
|
|
23
|
+
# Looks up a currency by its display symbol.
|
|
24
|
+
#
|
|
25
|
+
# @param symbol [String] the display symbol (e.g. "$", "R$")
|
|
26
|
+
# @return [Currency, nil] the highest-priority currency for the symbol
|
|
27
|
+
def Currency.for_symbol(symbol) = Registry.currency_for_symbol(symbol)
|
|
28
|
+
|
|
29
|
+
# Registers a new currency, raising a KeyError if already registered.
|
|
30
|
+
#
|
|
31
|
+
# @param code [String] the unique currency code
|
|
32
|
+
# @param subunit [Integer] the decimal subunit precision, defaults to 0
|
|
33
|
+
# @param symbol [String] the display symbol
|
|
34
|
+
# @param priority [Integer] parser precedence priority
|
|
35
|
+
# @return [Currency] the newly registered Currency instance
|
|
36
|
+
# @raise [ArgumentError] if the code contains invalid characters
|
|
37
|
+
# @raise [KeyError] if the currency code is already registered
|
|
38
|
+
def Currency.register(code:, subunit: 0, symbol: '', priority: 0)
|
|
39
|
+
Registry.register(code:, subunit:, symbol:, priority:)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Registers all built-in crypto currencies at once.
|
|
43
|
+
#
|
|
44
|
+
# Raises on the first duplicate — call +rescue+ if idempotency is needed.
|
|
45
|
+
#
|
|
46
|
+
# @raise [KeyError] if any currency code is already registered
|
|
47
|
+
# @return [Array<Currency>] the newly registered Currency objects
|
|
48
|
+
def Currency.register_all_crypto = Registry.register_all_crypto
|
|
49
|
+
|
|
50
|
+
# Registers one or more crypto currencies into the shared currency registry.
|
|
51
|
+
#
|
|
52
|
+
# Raises on duplicate registration or unknown code — use +rescue+ if
|
|
53
|
+
# idempotent bulk registration is needed.
|
|
54
|
+
#
|
|
55
|
+
# @param codes [Array<String>] one or more crypto currency codes
|
|
56
|
+
# @raise [ArgumentError] if a code is not a known crypto currency
|
|
57
|
+
# @raise [KeyError] if the currency code is already registered
|
|
58
|
+
# @return [Array<Currency>] the newly registered Currency objects
|
|
59
|
+
def Currency.register_crypto(...) = Registry.register_crypto(...)
|
|
60
|
+
|
|
61
|
+
# Returns all registered currencies as a frozen hash keyed by ISO code.
|
|
62
|
+
#
|
|
63
|
+
# @return [Hash{String => Currency}] frozen hash of all registered currencies
|
|
64
|
+
# @example Iterate over registered currencies
|
|
65
|
+
# Currency.registered_currencies.each { |code, currency| puts "#{code}: #{currency.name}" }
|
|
66
|
+
# @example Count of registered currencies
|
|
67
|
+
# Currency.registered_currencies.size #=> 154
|
|
68
|
+
def Currency.registered_currencies = Registry.currencies
|
|
69
|
+
|
|
70
|
+
# Resolves an object into a {Currency}, returning +nil+ when it can't.
|
|
71
|
+
#
|
|
72
|
+
# Accepts +nil+, +String+, {Currency}, {Money}, or any object implementing
|
|
73
|
+
# +#to_currency+ (must return {Currency}) or +#currency_code+ (must return +String+).
|
|
74
|
+
#
|
|
75
|
+
# @param object [String, Currency, Money, nil, #to_currency, #currency_code]
|
|
76
|
+
# a currency code, object, or +nil+
|
|
77
|
+
# @return [Currency, nil] the resolved Currency, or +nil+ if +object+ is +nil+
|
|
78
|
+
# or the code is not registered
|
|
79
|
+
# @raise [ArgumentError] if +object+ is an unsupported type, or if the method
|
|
80
|
+
# used to resolve it returns a value of the wrong type
|
|
81
|
+
def Currency.resolve(object)
|
|
82
|
+
case object
|
|
83
|
+
when NilClass then nil
|
|
84
|
+
when Currency then object
|
|
85
|
+
when Money then object.currency
|
|
86
|
+
when String then Currency.for_code object
|
|
87
|
+
else
|
|
88
|
+
if object.respond_to?(:to_currency)
|
|
89
|
+
result = object.to_currency
|
|
90
|
+
unless result.is_a?(Currency)
|
|
91
|
+
raise ArgumentError, "#to_currency must return a [Money::Currency], got #{result.class}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
result
|
|
95
|
+
elsif object.respond_to?(:currency_code)
|
|
96
|
+
result = object.currency_code
|
|
97
|
+
raise ArgumentError, "#currency_code must return a [String], got #{result.class}" unless result.is_a?(String)
|
|
98
|
+
|
|
99
|
+
Currency.for_code result
|
|
100
|
+
else
|
|
101
|
+
raise ArgumentError, "currency must be [Money::Currency], [Money], [String] or nil (#{object})"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Resolves an object into a {Currency}, raising on failure.
|
|
107
|
+
#
|
|
108
|
+
# Like {.resolve} but raises when the result would be +nil+.
|
|
109
|
+
#
|
|
110
|
+
# @param object [String, Currency, Money, nil] a currency code, object, or +nil+
|
|
111
|
+
# @return [Currency] the resolved Currency
|
|
112
|
+
# @raise [Mint::UnknownCurrency] if +object+ cannot be resolved into a
|
|
113
|
+
# registered currency. +Mint::UnknownCurrency+ inherits from +ArgumentError+,
|
|
114
|
+
# so existing +rescue ArgumentError+ handlers continue to work.
|
|
115
|
+
def Currency.resolve!(object)
|
|
116
|
+
resolve(object) or raise Mint::UnknownCurrency, "Could not resolve (#{object}) into a currency"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Returns a zero {Money} in the given currency, useful as a default value
|
|
120
|
+
# for discounts, totals, or placeholders.
|
|
121
|
+
#
|
|
122
|
+
# @param currency [String, Currency] a currency code or object
|
|
123
|
+
# @return [Money] a frozen zero-Money
|
|
124
|
+
# @raise [Mint::UnknownCurrency] if the currency can't be resolved
|
|
125
|
+
def Currency.zero(currency) = Registry.zero_for(Currency.resolve!(currency))
|
|
126
|
+
|
|
127
|
+
# --- @api private ---
|
|
128
|
+
|
|
129
|
+
# Returns the frozen hash of all built-in ISO 4217 world currencies.
|
|
130
|
+
#
|
|
131
|
+
# @return [Hash{String => Currency}] ISO-4217 world currencies mapped by code
|
|
132
|
+
# @api private
|
|
133
|
+
def Currency.world_currencies = Registry.world_currencies
|
|
134
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# :nodoc:
|
|
4
|
+
module Mint
|
|
5
|
+
# :nodoc:
|
|
6
|
+
class Currency
|
|
7
|
+
# @api private
|
|
8
|
+
VALID_ROUNDING_MODES = %i[up down even].freeze
|
|
9
|
+
|
|
10
|
+
# @api private
|
|
11
|
+
ROUNDING_THREAD_KEY = :minting_rounding_mode
|
|
12
|
+
|
|
13
|
+
# @return [Boolean] whether a custom rounding mode has been activated
|
|
14
|
+
# @api private
|
|
15
|
+
def self.custom_rounding_active? = @custom_rounding_active
|
|
16
|
+
|
|
17
|
+
# Activates the custom rounding dispatch path in {#normalize_amount}.
|
|
18
|
+
# Once called, this cannot be reversed for the lifetime of the process.
|
|
19
|
+
# @api private
|
|
20
|
+
def self.activate_custom_rounding! = @custom_rounding_active = true
|
|
21
|
+
|
|
22
|
+
# Returns the currently active rounding mode, falling back to +:up+.
|
|
23
|
+
# @api private
|
|
24
|
+
# @return [Symbol] one of +:up+, +:down+, +:even+
|
|
25
|
+
def self.current_rounding_mode
|
|
26
|
+
Thread.current[ROUNDING_THREAD_KEY] || :up
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Sets a rounding mode for the duration of a block, restoring the
|
|
30
|
+
# previous mode on exit (even on exception).
|
|
31
|
+
# @api private
|
|
32
|
+
# @param mode [Symbol] one of +:up+, +:down+, +:even+
|
|
33
|
+
# @yield block to execute with the mode active
|
|
34
|
+
# @raise [ArgumentError] on unknown mode
|
|
35
|
+
def self.rounding_mode(mode)
|
|
36
|
+
unless VALID_ROUNDING_MODES.include?(mode)
|
|
37
|
+
raise ArgumentError, "Unknown rounding mode: #{mode} (expected :up, :down, or :even)"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
prev = Thread.current[ROUNDING_THREAD_KEY]
|
|
41
|
+
Thread.current[ROUNDING_THREAD_KEY] = mode
|
|
42
|
+
yield
|
|
43
|
+
ensure
|
|
44
|
+
Thread.current[ROUNDING_THREAD_KEY] = prev
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -14,25 +14,23 @@ module Mint
|
|
|
14
14
|
|
|
15
15
|
private_constant :MUTEX
|
|
16
16
|
|
|
17
|
+
# Preload world currencies from YAML file during module load.
|
|
18
|
+
path = File.join(File.expand_path('../../data', __dir__), 'world-currencies.yaml')
|
|
19
|
+
@world_currencies = YAML.load_file(path).to_h do |entry|
|
|
20
|
+
[entry['code'], Currency.new(**entry.transform_keys(&:to_sym))]
|
|
21
|
+
end
|
|
22
|
+
@currencies = @world_currencies.freeze.dup.freeze
|
|
23
|
+
|
|
17
24
|
# Loads ISO world currencies from YAML file.
|
|
18
25
|
#
|
|
19
26
|
# @return [Hash{String => Currency}] ISO-4217 world currencies mapped by code
|
|
20
27
|
# @api private
|
|
21
|
-
def self.world_currencies
|
|
22
|
-
@world_currencies || MUTEX.synchronize do
|
|
23
|
-
@world_currencies = begin
|
|
24
|
-
path = File.join(File.expand_path('../../data', __dir__), 'world-currencies.yaml')
|
|
25
|
-
YAML.load_file(path).to_h { |entry| [entry['code'], Currency.new(**entry.transform_keys(&:to_sym))] }
|
|
26
|
-
end.freeze
|
|
27
|
-
end
|
|
28
|
-
end
|
|
28
|
+
def self.world_currencies = @world_currencies
|
|
29
29
|
|
|
30
30
|
# Returns the frozen hash of all registered currencies (world + custom).
|
|
31
31
|
#
|
|
32
32
|
# @return [Hash{String => Currency}] registered currencies mapped by code
|
|
33
33
|
# @api private
|
|
34
|
-
def self.currencies
|
|
35
|
-
@currencies || MUTEX.synchronize { @currencies = world_currencies.dup.freeze }
|
|
36
|
-
end
|
|
34
|
+
def self.currencies = @currencies
|
|
37
35
|
end
|
|
38
36
|
end
|
data/lib/minting/mint.rb
CHANGED
|
@@ -15,7 +15,7 @@ module Mint
|
|
|
15
15
|
# money = Money.from(10.00, 'USD')
|
|
16
16
|
# money.split(3) #=> [[USD 3.34], [USD 3.33], [USD 3.33]]
|
|
17
17
|
def split(slices)
|
|
18
|
-
raise ArgumentError, 'Slices quantity must be
|
|
18
|
+
raise ArgumentError, 'Slices quantity must be a positive integer' unless slices.positive? && slices.integer?
|
|
19
19
|
|
|
20
20
|
fraction = currency.normalize_amount(amount / slices)
|
|
21
21
|
allocate_left_over(amounts: Array.new(slices, fraction),
|
|
@@ -46,11 +46,13 @@ module Mint
|
|
|
46
46
|
amount = money.amount
|
|
47
47
|
currency = money.currency
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
templates = @has_placeholder ? @templates_by_subunit[currency.subunit] : @templates
|
|
50
|
+
|
|
51
|
+
template = templates[amount <=> 0] || templates[1]
|
|
52
|
+
|
|
53
|
+
display_amount = @has_negative_template && amount < 0 ? -amount : amount
|
|
51
54
|
integral = display_amount.to_i
|
|
52
55
|
|
|
53
|
-
template = template.gsub(SUBUNIT_PLACEHOLDER, currency.subunit.to_s) if @has_placeholder
|
|
54
56
|
result = Kernel.format(template,
|
|
55
57
|
currency: currency.code,
|
|
56
58
|
dsymbol: @needs_dsymbol && currency.dsymbol,
|
|
@@ -86,16 +88,22 @@ module Mint
|
|
|
86
88
|
# The placeholder is later replaced with the actual subunit count at
|
|
87
89
|
# format time (e.g. "\uE000" → "2" for USD, "0" for JPY).
|
|
88
90
|
@templates.transform_values! { |f| f.gsub(/%<amount>(\s*\+?\d*)f/, "%<amount>\\1.#{SUBUNIT_PLACEHOLDER}f") }
|
|
89
|
-
@
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
@
|
|
94
|
-
|
|
95
|
-
@
|
|
96
|
-
|
|
97
|
-
joined_template.include?('%<integral>'))
|
|
91
|
+
@has_negative_template = @templates.key?(-1)
|
|
92
|
+
|
|
93
|
+
joined = @templates.values.join
|
|
94
|
+
@needs_fractional = joined.include?('%<fractional>')
|
|
95
|
+
@needs_dsymbol = joined.include?('%<dsymbol>')
|
|
96
|
+
|
|
97
|
+
@needs_thousand_substitution = @thousand && !@thousand.empty? &&
|
|
98
|
+
(joined.include?('%<amount>') || joined.include?('%<integral>'))
|
|
98
99
|
@thousand_replacement = "\\1#{@thousand}" if @needs_thousand_substitution
|
|
100
|
+
|
|
101
|
+
@has_placeholder = joined.include?(SUBUNIT_PLACEHOLDER)
|
|
102
|
+
return unless @has_placeholder
|
|
103
|
+
|
|
104
|
+
@templates_by_subunit = Hash.new do |h, subunit|
|
|
105
|
+
h[subunit] = @templates.transform_values { |f| f.gsub(SUBUNIT_PLACEHOLDER, subunit.to_s) }
|
|
106
|
+
end
|
|
99
107
|
end
|
|
100
108
|
end
|
|
101
109
|
end
|
|
@@ -9,19 +9,18 @@ module Mint
|
|
|
9
9
|
# Restores the previous mode (or default) when the block exits, even on
|
|
10
10
|
# exception.
|
|
11
11
|
#
|
|
12
|
-
# Rounding-mode support is
|
|
13
|
-
# +Currency#normalize_amount+
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
12
|
+
# Rounding-mode support is activated on first call. Once activated,
|
|
13
|
+
# +Currency#normalize_amount+ dispatches through +Currency.rounding_mode+,
|
|
14
|
+
# adding ~10–35&ns of overhead to every money creation or mutation.
|
|
15
|
+
# When rounding modes are never used (the common case), the fast path
|
|
16
|
+
# incurs zero overhead.
|
|
17
17
|
#
|
|
18
|
-
# @param mode [Symbol] one of: +:
|
|
19
|
-
# +:ceil+, +:truncate+, +:down+
|
|
18
|
+
# @param mode [Symbol] one of: +:up+, +:down+, +:even+
|
|
20
19
|
# @yield block to execute with the rounding mode active
|
|
21
20
|
# @raise [ArgumentError] if +mode+ is not a recognised rounding mode
|
|
22
21
|
def self.with_rounding(mode, &)
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
Currency.activate_custom_rounding!
|
|
23
|
+
Currency.rounding_mode(mode, &)
|
|
25
24
|
end
|
|
26
25
|
end
|
|
27
26
|
end
|
data/lib/minting/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: minting
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gilson Ferraz
|
|
@@ -46,6 +46,8 @@ files:
|
|
|
46
46
|
- lib/minting.rb
|
|
47
47
|
- lib/minting/aliases.rb
|
|
48
48
|
- lib/minting/currency/currency.rb
|
|
49
|
+
- lib/minting/currency/registry.rb
|
|
50
|
+
- lib/minting/currency/rounding.rb
|
|
49
51
|
- lib/minting/data/crypto-currencies.yaml
|
|
50
52
|
- lib/minting/data/world-currencies.yaml
|
|
51
53
|
- lib/minting/mint.rb
|
|
@@ -59,7 +61,6 @@ files:
|
|
|
59
61
|
- lib/minting/mint/registry/registry.rb
|
|
60
62
|
- lib/minting/mint/registry/symbols.rb
|
|
61
63
|
- lib/minting/mint/registry/zeros.rb
|
|
62
|
-
- lib/minting/mint/rounding.rb
|
|
63
64
|
- lib/minting/money/allocation/allocation.rb
|
|
64
65
|
- lib/minting/money/allocation/split.rb
|
|
65
66
|
- lib/minting/money/arithmetics/methods.rb
|
|
@@ -103,7 +104,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
103
104
|
- !ruby/object:Gem::Version
|
|
104
105
|
version: '0'
|
|
105
106
|
requirements: []
|
|
106
|
-
rubygems_version: 4.0.
|
|
107
|
+
rubygems_version: 4.0.17
|
|
107
108
|
specification_version: 4
|
|
108
109
|
summary: Library to manipulate currency values
|
|
109
110
|
test_files: []
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Mint
|
|
4
|
-
# Rounding-mode dispatch table and block-scoped context.
|
|
5
|
-
# @api private
|
|
6
|
-
module Rounding
|
|
7
|
-
# Maps mode symbols to their corresponding +Rational+ rounding lambdas.
|
|
8
|
-
# @return [Hash{Symbol => Proc}]
|
|
9
|
-
# @api private
|
|
10
|
-
MODES = {
|
|
11
|
-
half_up: ->(amount, ndigits) { amount.round(ndigits, half: :up) },
|
|
12
|
-
half_down: ->(amount, ndigits) { amount.round(ndigits, half: :down) },
|
|
13
|
-
half_even: ->(amount, ndigits) { amount.round(ndigits, half: :even) },
|
|
14
|
-
floor: ->(amount, ndigits) { amount.floor(ndigits) },
|
|
15
|
-
ceil: ->(amount, ndigits) { amount.ceil(ndigits) },
|
|
16
|
-
truncate: ->(amount, ndigits) { amount.truncate(ndigits) },
|
|
17
|
-
down: ->(amount, ndigits) { amount.truncate(ndigits) }
|
|
18
|
-
}.freeze
|
|
19
|
-
|
|
20
|
-
THREAD_KEY = :minting_rounding_mode
|
|
21
|
-
|
|
22
|
-
# Returns the currently active rounding mode, falling back to +:half_up+.
|
|
23
|
-
# @api private
|
|
24
|
-
# @return [Symbol]
|
|
25
|
-
def self.current_mode
|
|
26
|
-
Thread.current[THREAD_KEY] || :half_up
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Rounds +amount+ to +ndigits+ using the currently scoped rounding mode.
|
|
30
|
-
# Uses the fast path (+to_r.round+) when no custom mode is active.
|
|
31
|
-
# @api private
|
|
32
|
-
# @param amount [Numeric]
|
|
33
|
-
# @param ndigits [Integer]
|
|
34
|
-
# @return [Rational]
|
|
35
|
-
def self.apply(amount, ndigits)
|
|
36
|
-
mode = Thread.current[THREAD_KEY]
|
|
37
|
-
if mode
|
|
38
|
-
MODES.fetch(mode).call(amount.to_r, ndigits)
|
|
39
|
-
else
|
|
40
|
-
amount.to_r.round(ndigits)
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# Sets a rounding mode for the duration of a block, restoring the
|
|
45
|
-
# previous mode on exit (even on exception).
|
|
46
|
-
# @api private
|
|
47
|
-
# @param mode [Symbol]
|
|
48
|
-
# @yield block to execute with the mode active
|
|
49
|
-
# @raise [ArgumentError] on unknown mode
|
|
50
|
-
def self.with_mode(mode)
|
|
51
|
-
raise ArgumentError, "Unknown rounding mode: #{mode}" unless MODES.key?(mode)
|
|
52
|
-
|
|
53
|
-
prev = Thread.current[THREAD_KEY]
|
|
54
|
-
Thread.current[THREAD_KEY] = mode
|
|
55
|
-
yield
|
|
56
|
-
ensure
|
|
57
|
-
Thread.current[THREAD_KEY] = prev
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# When loaded, patch Currency to delegate rounding through this module.
|
|
63
|
-
# Remove the default fast-path method first to avoid redefinition warnings.
|
|
64
|
-
Money::Currency.remove_method(:normalize_amount)
|
|
65
|
-
Money::Currency.define_method(:normalize_amount) do |amount|
|
|
66
|
-
Mint::Rounding.apply(amount, subunit)
|
|
67
|
-
end
|