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
|
@@ -30,6 +30,8 @@ module Mint
|
|
|
30
30
|
subunit = subunit.to_i
|
|
31
31
|
priority = priority.to_i
|
|
32
32
|
fractional_multiplier = 10**subunit
|
|
33
|
+
symbol = nil if symbol && symbol.empty?
|
|
34
|
+
disambiguate_symbol = nil if [code, symbol].include? disambiguate_symbol
|
|
33
35
|
super(code:, subunit:, symbol:, priority:, country:, name:,
|
|
34
36
|
fractional_multiplier:, disambiguate_symbol:)
|
|
35
37
|
end
|
|
@@ -40,17 +42,29 @@ module Mint
|
|
|
40
42
|
# @return [Rational] smallest representable amount (1/fractional_multiplier)
|
|
41
43
|
def minimum_amount = Rational(1, fractional_multiplier)
|
|
42
44
|
|
|
43
|
-
# Normalizes numeric
|
|
44
|
-
#
|
|
45
|
-
#
|
|
45
|
+
# Normalizes a numeric amount for this currency.
|
|
46
|
+
#
|
|
47
|
+
# @param amount [Numeric] the monetary amount to normalize
|
|
48
|
+
# @return [Rational] the amount converted to +Rational+ and rounded to
|
|
49
|
+
# the currency's subunit precision (half-up by default)
|
|
50
|
+
# @example
|
|
51
|
+
# usd = Money::Currency.for_code('USD')
|
|
52
|
+
# usd.normalize_amount(10.567) #=> (10567/1000)
|
|
53
|
+
# usd.normalize_amount("5.25") #=> (21/4)
|
|
54
|
+
#
|
|
55
|
+
# @see Mint::Rounding.apply Custom rounding modes via {Money.with_rounding}
|
|
46
56
|
def normalize_amount(amount) = amount.to_r.round(subunit)
|
|
47
57
|
|
|
48
58
|
# Returns the cached frozen zero-Money for this currency.
|
|
49
59
|
#
|
|
50
60
|
# @return [Money] a frozen zero-Money instance
|
|
51
61
|
# @example
|
|
52
|
-
#
|
|
62
|
+
# Money::Currency.for_code('USD').zero #=> [USD 0.00]
|
|
53
63
|
def zero = Registry.zero_for(self)
|
|
64
|
+
|
|
65
|
+
def dsymbol
|
|
66
|
+
disambiguate_symbol || (Registry.symbol_shared?(symbol) ? code : symbol)
|
|
67
|
+
end
|
|
54
68
|
end
|
|
55
69
|
|
|
56
70
|
# Registers a new currency, raising a KeyError if already registered.
|
|
@@ -68,20 +82,37 @@ module Mint
|
|
|
68
82
|
|
|
69
83
|
# Resolves an object into a {Currency}, returning +nil+ when it can't.
|
|
70
84
|
#
|
|
71
|
-
# Accepts +nil+, +String+, {Currency},
|
|
72
|
-
#
|
|
85
|
+
# Accepts +nil+, +String+, {Currency}, {Money}, or any object implementing
|
|
86
|
+
# +#to_currency+ (must return {Currency}) or +#currency_code+ (must return +String+).
|
|
73
87
|
#
|
|
74
|
-
# @param object [String, Currency, Money, nil
|
|
88
|
+
# @param object [String, Currency, Money, nil, #to_currency, #currency_code]
|
|
89
|
+
# a currency code, object, or +nil+
|
|
75
90
|
# @return [Currency, nil] the resolved Currency, or +nil+ if +object+ is +nil+
|
|
76
91
|
# or the code is not registered
|
|
77
|
-
# @raise [ArgumentError] if +object+ is an unsupported type
|
|
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
|
|
78
94
|
def Currency.resolve(object)
|
|
79
95
|
case object
|
|
80
96
|
when NilClass then nil
|
|
81
97
|
when Currency then object
|
|
82
98
|
when Money then object.currency
|
|
83
99
|
when String then Currency.for_code object
|
|
84
|
-
else
|
|
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
|
|
85
116
|
end
|
|
86
117
|
end
|
|
87
118
|
|
|
@@ -98,6 +129,15 @@ module Mint
|
|
|
98
129
|
resolve(object) or raise Mint::UnknownCurrency, "Could not resolve (#{object}) into a currency"
|
|
99
130
|
end
|
|
100
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
|
+
|
|
101
141
|
# Looks up a registered currency by its alpha code.
|
|
102
142
|
#
|
|
103
143
|
# @param code [String] the currency code
|
|
@@ -121,4 +161,36 @@ module Mint
|
|
|
121
161
|
# @return [Money] a frozen zero-Money
|
|
122
162
|
# @raise [Mint::UnknownCurrency] if the currency can't be resolved
|
|
123
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
|
|
124
196
|
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
---
|
|
2
|
+
- code: BTC
|
|
3
|
+
subunit: 8
|
|
4
|
+
symbol: "₿"
|
|
5
|
+
priority: 1000
|
|
6
|
+
name: Bitcoin
|
|
7
|
+
- code: ETH
|
|
8
|
+
subunit: 18
|
|
9
|
+
symbol: "Ξ"
|
|
10
|
+
priority: 950
|
|
11
|
+
name: Ethereum
|
|
12
|
+
- code: USDT
|
|
13
|
+
subunit: 6
|
|
14
|
+
symbol: "$"
|
|
15
|
+
priority: 900
|
|
16
|
+
name: Tether
|
|
17
|
+
- code: SOL
|
|
18
|
+
subunit: 9
|
|
19
|
+
symbol: "◎"
|
|
20
|
+
priority: 850
|
|
21
|
+
name: Solana
|
|
22
|
+
- code: XRP
|
|
23
|
+
subunit: 6
|
|
24
|
+
symbol: "✕"
|
|
25
|
+
priority: 800
|
|
26
|
+
name: XRP
|
|
27
|
+
- code: BNB
|
|
28
|
+
subunit: 8
|
|
29
|
+
symbol: ""
|
|
30
|
+
priority: 750
|
|
31
|
+
name: BNB
|
|
32
|
+
- code: USDC
|
|
33
|
+
subunit: 6
|
|
34
|
+
symbol: "$"
|
|
35
|
+
priority: 740
|
|
36
|
+
name: USD Coin
|
|
37
|
+
- code: DOGE
|
|
38
|
+
subunit: 8
|
|
39
|
+
symbol: "Ð"
|
|
40
|
+
priority: 730
|
|
41
|
+
name: Dogecoin
|
|
42
|
+
- code: ADA
|
|
43
|
+
subunit: 6
|
|
44
|
+
symbol: "₳"
|
|
45
|
+
priority: 720
|
|
46
|
+
name: Cardano
|
|
47
|
+
- code: TRX
|
|
48
|
+
subunit: 6
|
|
49
|
+
symbol: ""
|
|
50
|
+
priority: 710
|
|
51
|
+
name: TRON
|
|
52
|
+
- code: AVAX
|
|
53
|
+
subunit: 9
|
|
54
|
+
symbol: ""
|
|
55
|
+
priority: 700
|
|
56
|
+
name: Avalanche
|
|
57
|
+
- code: DOT
|
|
58
|
+
subunit: 10
|
|
59
|
+
symbol: ""
|
|
60
|
+
priority: 680
|
|
61
|
+
name: Polkadot
|
|
62
|
+
- code: LINK
|
|
63
|
+
subunit: 8
|
|
64
|
+
symbol: ""
|
|
65
|
+
priority: 660
|
|
66
|
+
name: Chainlink
|
|
67
|
+
- code: MATIC
|
|
68
|
+
subunit: 6
|
|
69
|
+
symbol: "⧫"
|
|
70
|
+
priority: 640
|
|
71
|
+
name: Polygon
|
|
72
|
+
- code: SHIB
|
|
73
|
+
subunit: 8
|
|
74
|
+
symbol: ""
|
|
75
|
+
priority: 620
|
|
76
|
+
name: Shiba Inu
|
|
77
|
+
- code: UNI
|
|
78
|
+
subunit: 8
|
|
79
|
+
symbol: ""
|
|
80
|
+
priority: 600
|
|
81
|
+
name: Uniswap
|
|
82
|
+
- code: ATOM
|
|
83
|
+
subunit: 6
|
|
84
|
+
symbol: ""
|
|
85
|
+
priority: 580
|
|
86
|
+
name: Cosmos
|
|
87
|
+
- code: LTC
|
|
88
|
+
subunit: 8
|
|
89
|
+
symbol: "Ł"
|
|
90
|
+
priority: 560
|
|
91
|
+
name: Litecoin
|
|
92
|
+
- code: BCH
|
|
93
|
+
subunit: 8
|
|
94
|
+
symbol: ""
|
|
95
|
+
priority: 540
|
|
96
|
+
name: Bitcoin Cash
|
|
97
|
+
- code: FIL
|
|
98
|
+
subunit: 9
|
|
99
|
+
symbol: ""
|
|
100
|
+
priority: 520
|
|
101
|
+
name: Filecoin
|
|
102
|
+
- code: APT
|
|
103
|
+
subunit: 8
|
|
104
|
+
symbol: ""
|
|
105
|
+
priority: 500
|
|
106
|
+
name: Aptos
|
|
107
|
+
- code: SUI
|
|
108
|
+
subunit: 9
|
|
109
|
+
symbol: ""
|
|
110
|
+
priority: 480
|
|
111
|
+
name: Sui
|
|
112
|
+
- code: TON
|
|
113
|
+
subunit: 9
|
|
114
|
+
symbol: ""
|
|
115
|
+
priority: 460
|
|
116
|
+
name: Toncoin
|
|
117
|
+
- code: PEPE
|
|
118
|
+
subunit: 8
|
|
119
|
+
symbol: ""
|
|
120
|
+
priority: 440
|
|
121
|
+
name: Pepe
|
|
122
|
+
- code: STX
|
|
123
|
+
subunit: 6
|
|
124
|
+
symbol: ""
|
|
125
|
+
priority: 420
|
|
126
|
+
name: Stacks
|
data/lib/minting/mint/i18n.rb
CHANGED
|
@@ -5,52 +5,102 @@ module Mint
|
|
|
5
5
|
class << self
|
|
6
6
|
# Optional callable that returns a Hash with locale-aware formatting defaults.
|
|
7
7
|
#
|
|
8
|
-
# The callable receives
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# [
|
|
8
|
+
# The callable receives the locale (as passed to +#format+'s +locale:+ kwarg,
|
|
9
|
+
# or +nil+ when omitted) and returns a Hash with these keys (strings and
|
|
10
|
+
# symbols are treated interchangeably):
|
|
11
|
+
# [+decimal+] Decimal separator (e.g. +","+) — also accepts +:separator+
|
|
12
|
+
# [+thousand+] Thousands delimiter (e.g. +"."+) — also accepts +:delimiter+
|
|
13
|
+
# [+format+] Format template string (e.g. +"%<amount>f %<symbol>s"+)
|
|
12
14
|
#
|
|
13
15
|
# When set, +#to_formatted_s+ and +#format+ use these values as fallbacks when the
|
|
14
16
|
# corresponding parameter is not explicitly provided.
|
|
15
17
|
#
|
|
16
|
-
# @example
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# {
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
18
|
+
# @example Per-locale formatting with string keys
|
|
19
|
+
# LOCALE_DATA = {
|
|
20
|
+
# 'en' => { decimal: '.', thousand: ',', format: '%<symbol>s%<amount>f' },
|
|
21
|
+
# 'de' => { decimal: ',', thousand: '.', format: '%<amount>f %<currency>s' },
|
|
22
|
+
# }.freeze
|
|
23
|
+
# Mint.locale_backend = ->(locale) { LOCALE_DATA[locale.to_s] || {} }
|
|
24
|
+
#
|
|
25
|
+
# @example Rails I18n integration (direct pass-through, no mapping needed)
|
|
26
|
+
# Mint.locale_backend = ->(locale = nil) {
|
|
27
|
+
# I18n.with_locale(locale || I18n.default_locale) do
|
|
28
|
+
# I18n.t('number.currency.format', default: {})
|
|
29
|
+
# end
|
|
24
30
|
# }
|
|
25
31
|
#
|
|
26
32
|
# @return [Proc, #call, nil]
|
|
27
33
|
attr_accessor :locale_backend
|
|
28
34
|
end
|
|
29
35
|
|
|
36
|
+
# Resolves locale-aware formatting defaults from +Mint.locale_backend+.
|
|
37
|
+
#
|
|
38
|
+
# Returns a Hash with +:decimal+, +:thousand+, and +:format+ keys (any of
|
|
39
|
+
# which may be +nil+ if the backend doesn't provide them).
|
|
40
|
+
#
|
|
41
|
+
# @param locale [Symbol, String, nil] locale passed to the backend callable
|
|
42
|
+
# @return [Hash{Symbol => String, nil}]
|
|
30
43
|
# @api private
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
def self.resolve_locale_for(locale: nil)
|
|
45
|
+
return EMPTY_LOCALE if Mint.locale_backend.nil?
|
|
46
|
+
|
|
47
|
+
lc = resolve_locale_backend(locale)
|
|
48
|
+
return EMPTY_LOCALE if lc.empty?
|
|
49
|
+
|
|
50
|
+
{ decimal: fetch_locale_key(lc, :decimal),
|
|
51
|
+
thousand: fetch_locale_key(lc, :thousand),
|
|
52
|
+
format: fetch_locale_key(lc, :format) }.freeze
|
|
53
|
+
end
|
|
33
54
|
|
|
34
|
-
|
|
35
|
-
# @private
|
|
36
|
-
def resolve_locale_for(format, decimal, thousand)
|
|
37
|
-
locale = locale_backend
|
|
55
|
+
EMPTY_LOCALE = { decimal: nil, thousand: nil, format: nil }.freeze
|
|
38
56
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
# Looks up a locale key from a hash, trying both symbol and string forms.
|
|
58
|
+
#
|
|
59
|
+
# Supports aliases: +:decimal+ checks +:decimal+ and +:separator+,
|
|
60
|
+
# +:thousand+ checks +:thousand+ and +:delimiter+, +:format+ checks +:format+.
|
|
61
|
+
#
|
|
62
|
+
# @param hash [Hash] locale config hash
|
|
63
|
+
# @param key [Symbol] the primary key (+:decimal+, +:thousand+, or +:format+)
|
|
64
|
+
# @return [String, nil] the value found, or nil
|
|
65
|
+
# @api private
|
|
66
|
+
def self.fetch_locale_key(hash, key)
|
|
67
|
+
aliases = { decimal: %i[decimal separator], thousand: %i[thousand delimiter], format: [:format] }
|
|
68
|
+
aliases.fetch(key).each do |name|
|
|
69
|
+
val = hash[name] || hash[name.to_s]
|
|
70
|
+
return val unless val.nil?
|
|
42
71
|
end
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
43
74
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
75
|
+
# Resolves the locale backend configuration into a Hash.
|
|
76
|
+
#
|
|
77
|
+
# Handles three backend types:
|
|
78
|
+
# - +Hash+: returned as-is
|
|
79
|
+
# - +Proc+/callable: called with the locale (or no args for 0-arity),
|
|
80
|
+
# result must be a Hash or nil
|
|
81
|
+
# - +nil+: returns empty Hash
|
|
82
|
+
#
|
|
83
|
+
# Invalid return values or backends emit a warning and return +{}+.
|
|
84
|
+
#
|
|
85
|
+
# @param locale [Symbol, String, nil] locale passed to the backend callable
|
|
86
|
+
# @return [Hash] locale configuration (possibly empty)
|
|
87
|
+
# @api private
|
|
88
|
+
def self.resolve_locale_backend(locale = nil)
|
|
89
|
+
case bk = Mint.locale_backend
|
|
90
|
+
when Hash then bk
|
|
91
|
+
when NilClass then {}
|
|
92
|
+
else
|
|
93
|
+
if bk.respond_to?(:call)
|
|
94
|
+
args = bk.respond_to?(:arity) && bk.arity == 0 ? [] : [locale]
|
|
95
|
+
result = bk.call(*args)
|
|
96
|
+
return result if result.is_a?(Hash) || result.nil?
|
|
50
97
|
|
|
51
|
-
warn "ignoring invalid locale_backend: #{
|
|
52
|
-
{}
|
|
98
|
+
warn "ignoring invalid locale_backend result: #{result.inspect}"
|
|
99
|
+
return {}
|
|
53
100
|
end
|
|
101
|
+
|
|
102
|
+
warn "ignoring invalid locale_backend: #{bk.inspect}"
|
|
103
|
+
{}
|
|
54
104
|
end
|
|
55
105
|
end
|
|
56
106
|
end
|
data/lib/minting/mint/mint.rb
CHANGED
|
@@ -14,36 +14,11 @@ module Mint
|
|
|
14
14
|
# @param amount [Numeric] the financial value
|
|
15
15
|
# @param currency_code [String, Currency, Money, nil] Currency code, object,
|
|
16
16
|
# Money whose currency to reuse, or +nil+. Passed through
|
|
17
|
-
# {
|
|
17
|
+
# {Money::Currency.resolve!} so all accepted types resolve to a registered
|
|
18
18
|
# currency.
|
|
19
19
|
# @return [Money] the instantiated Money object
|
|
20
20
|
# @raise [ArgumentError] if the amount is not a Numeric
|
|
21
21
|
# @raise [Mint::UnknownCurrency] if the currency code is not registered.
|
|
22
22
|
# +Mint::UnknownCurrency+ inherits from +ArgumentError+.
|
|
23
23
|
def self.money(amount, currency_code) = Money.from(amount, currency_code)
|
|
24
|
-
|
|
25
|
-
# @return [Hash{String => Currency}] the frozen world-currencies hash
|
|
26
|
-
# @api private
|
|
27
|
-
def self.world_currencies = Registry.world_currencies
|
|
28
|
-
|
|
29
|
-
# Executes a block with a specific rounding mode applied to all money
|
|
30
|
-
# construction, parsing, change, allocation, and split operations.
|
|
31
|
-
#
|
|
32
|
-
# Restores the previous mode (or default) when the block exits, even on
|
|
33
|
-
# exception.
|
|
34
|
-
#
|
|
35
|
-
# Rounding-mode support is loaded lazily on first call. Once loaded,
|
|
36
|
-
# +Currency#normalize_amount+ is patched to dispatch through the
|
|
37
|
-
# rounding module, adding ~10–35&ns of overhead to every money creation
|
|
38
|
-
# or mutation. When rounding modes are never used (the common case),
|
|
39
|
-
# the fast path incurs zero overhead.
|
|
40
|
-
#
|
|
41
|
-
# @param mode [Symbol] one of: +:half_up+, +:half_down+, +:floor+,
|
|
42
|
-
# +:ceil+, +:truncate+, +:down+
|
|
43
|
-
# @yield block to execute with the rounding mode active
|
|
44
|
-
# @raise [ArgumentError] if +mode+ is not a recognised rounding mode
|
|
45
|
-
def self.with_rounding(mode, &)
|
|
46
|
-
require_relative 'rounding' unless defined?(Mint::Rounding)
|
|
47
|
-
Rounding.with_mode(mode, &)
|
|
48
|
-
end
|
|
49
24
|
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Crypto currency definitions (opt-in registration)
|
|
4
|
+
module Mint
|
|
5
|
+
# Internal registry for currencies, symbols, and zero-money cache.
|
|
6
|
+
module Registry
|
|
7
|
+
@crypto_currencies = nil
|
|
8
|
+
CRYPTO_MUTEX = Monitor.new
|
|
9
|
+
|
|
10
|
+
private_constant :CRYPTO_MUTEX
|
|
11
|
+
|
|
12
|
+
# Returns the list of built-in crypto currency definitions.
|
|
13
|
+
#
|
|
14
|
+
# These are not registered by default — call {.register_crypto} to opt in.
|
|
15
|
+
#
|
|
16
|
+
# @return [Array<Currency>] frozen array of crypto currency definitions
|
|
17
|
+
def self.crypto_currencies
|
|
18
|
+
@crypto_currencies || CRYPTO_MUTEX.synchronize do
|
|
19
|
+
@crypto_currencies ||= begin
|
|
20
|
+
path = File.join(File.expand_path('../../data', __dir__), 'crypto-currencies.yaml')
|
|
21
|
+
YAML.load_file(path).map { |entry| Currency.new(**entry.transform_keys(&:to_sym)) }.freeze
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Registers one or more crypto currencies into the shared currency registry.
|
|
27
|
+
#
|
|
28
|
+
# Raises on duplicate registration or unknown code — use +rescue+ if
|
|
29
|
+
# idempotent bulk registration is needed.
|
|
30
|
+
#
|
|
31
|
+
# @param codes [Array<String>] one or more crypto currency codes
|
|
32
|
+
# @raise [ArgumentError] if a code is not a known crypto currency
|
|
33
|
+
# @raise [KeyError] if the currency code is already registered
|
|
34
|
+
# @return [Array<Currency>] the newly registered Currency objects
|
|
35
|
+
def self.register_crypto(*codes)
|
|
36
|
+
entries = crypto_currencies
|
|
37
|
+
index = entries.each_with_index.to_h { |currency, index| [currency.code, index] }
|
|
38
|
+
missing = codes.reject { |code| index.key?(code) }
|
|
39
|
+
raise ArgumentError, "Unknown crypto code(s): #{missing.join(', ')}" unless missing.empty?
|
|
40
|
+
|
|
41
|
+
codes.map do |code|
|
|
42
|
+
c = entries[index[code]]
|
|
43
|
+
Currency.register(code:, subunit: c.subunit, symbol: c.symbol, priority: c.priority)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Registers all built-in crypto currencies at once.
|
|
48
|
+
#
|
|
49
|
+
# Raises on the first duplicate — call +rescue+ if idempotency is needed.
|
|
50
|
+
#
|
|
51
|
+
# @raise [KeyError] if any currency code is already registered
|
|
52
|
+
# @return [Array<Currency>] the newly registered Currency objects
|
|
53
|
+
def self.register_all_crypto
|
|
54
|
+
crypto_currencies.map do |c|
|
|
55
|
+
Currency.register(code: c.code, subunit: c.subunit, symbol: c.symbol, priority: c.priority)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -5,44 +5,51 @@ module Mint
|
|
|
5
5
|
module Registry
|
|
6
6
|
extend self
|
|
7
7
|
|
|
8
|
-
# Looks up a currency by its display symbol.
|
|
9
|
-
#
|
|
10
|
-
# @param symbol [String] the display symbol (e.g. "$", "R$")
|
|
11
|
-
# @return [Currency, nil] the highest-priority currency for the symbol
|
|
12
|
-
# @api private
|
|
13
8
|
def currency_for_symbol(symbol)
|
|
14
|
-
|
|
15
|
-
@
|
|
9
|
+
sync_symbols
|
|
10
|
+
@symbols_map[symbol]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def symbol_shared?(symbol)
|
|
14
|
+
sync_symbols
|
|
15
|
+
@shared_symbols.include?(symbol)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
# Scans +input+ for registered currency symbols and returns the first match.
|
|
19
|
-
#
|
|
20
|
-
# @param input [String] the string to scan
|
|
21
|
-
# @return [Currency, nil]
|
|
22
|
-
# @api private
|
|
23
18
|
def detect_currency(input)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
end
|
|
27
|
-
nil
|
|
19
|
+
sync_symbols
|
|
20
|
+
input.match(@symbols_regex) { |m| @symbols_map[m[0]] }
|
|
28
21
|
end
|
|
29
22
|
|
|
30
23
|
private
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
25
|
+
def build_symbol_regex(symbols)
|
|
26
|
+
/(?<![a-zA-Z])#{Regexp.union(symbols)}(?![a-zA-Z])/
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def sync_symbols
|
|
30
|
+
MUTEX.synchronize do
|
|
31
|
+
return if @symbols_list
|
|
32
|
+
|
|
33
|
+
symbols_list = []
|
|
34
|
+
currencies.each_value do |currency|
|
|
35
|
+
next unless currency.symbol
|
|
36
|
+
|
|
37
|
+
symbols_list << [currency.symbol, currency]
|
|
38
|
+
symbols_list << [currency.disambiguate_symbol, currency] if currency.disambiguate_symbol
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
@shared_symbols = symbols_list.map(&:first).tally
|
|
42
|
+
.select { |_, count| count > 1 }
|
|
43
|
+
.keys
|
|
44
|
+
.to_set
|
|
45
|
+
.freeze
|
|
46
|
+
|
|
47
|
+
symbols_list.sort_by! { |sym, cur| [-sym.length, -cur.priority] }
|
|
48
|
+
symbols_list.uniq! { |sym, _| sym }
|
|
49
|
+
|
|
50
|
+
@symbols_list = symbols_list.freeze
|
|
51
|
+
@symbols_map = symbols_list.to_h.freeze
|
|
52
|
+
@symbols_regex = build_symbol_regex(@symbols_map.keys)
|
|
46
53
|
end
|
|
47
54
|
end
|
|
48
55
|
end
|
|
@@ -17,11 +17,13 @@ module Mint
|
|
|
17
17
|
down: ->(amount, ndigits) { amount.truncate(ndigits) }
|
|
18
18
|
}.freeze
|
|
19
19
|
|
|
20
|
+
THREAD_KEY = :minting_rounding_mode
|
|
21
|
+
|
|
20
22
|
# Returns the currently active rounding mode, falling back to +:half_up+.
|
|
21
23
|
# @api private
|
|
22
24
|
# @return [Symbol]
|
|
23
25
|
def self.current_mode
|
|
24
|
-
Thread.current[
|
|
26
|
+
Thread.current[THREAD_KEY] || :half_up
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
# Rounds +amount+ to +ndigits+ using the currently scoped rounding mode.
|
|
@@ -31,7 +33,7 @@ module Mint
|
|
|
31
33
|
# @param ndigits [Integer]
|
|
32
34
|
# @return [Rational]
|
|
33
35
|
def self.apply(amount, ndigits)
|
|
34
|
-
mode = Thread.current[
|
|
36
|
+
mode = Thread.current[THREAD_KEY]
|
|
35
37
|
if mode
|
|
36
38
|
MODES.fetch(mode).call(amount.to_r, ndigits)
|
|
37
39
|
else
|
|
@@ -48,18 +50,18 @@ module Mint
|
|
|
48
50
|
def self.with_mode(mode)
|
|
49
51
|
raise ArgumentError, "Unknown rounding mode: #{mode}" unless MODES.key?(mode)
|
|
50
52
|
|
|
51
|
-
prev = Thread.current[
|
|
52
|
-
Thread.current[
|
|
53
|
+
prev = Thread.current[THREAD_KEY]
|
|
54
|
+
Thread.current[THREAD_KEY] = mode
|
|
53
55
|
yield
|
|
54
56
|
ensure
|
|
55
|
-
Thread.current[
|
|
57
|
+
Thread.current[THREAD_KEY] = prev
|
|
56
58
|
end
|
|
57
59
|
end
|
|
58
60
|
end
|
|
59
61
|
|
|
60
62
|
# When loaded, patch Currency to delegate rounding through this module.
|
|
61
63
|
# Remove the default fast-path method first to avoid redefinition warnings.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
Money::Currency.remove_method(:normalize_amount)
|
|
65
|
+
Money::Currency.define_method(:normalize_amount) do |amount|
|
|
64
66
|
Mint::Rounding.apply(amount, subunit)
|
|
65
67
|
end
|
data/lib/minting/mint.rb
CHANGED
|
@@ -7,8 +7,7 @@ require_relative 'mint/dsl/range'
|
|
|
7
7
|
require_relative 'mint/dsl/string'
|
|
8
8
|
require_relative 'mint/i18n'
|
|
9
9
|
require_relative 'mint/mint'
|
|
10
|
-
require_relative 'mint/parser/parser'
|
|
11
|
-
require_relative 'mint/parser/separators'
|
|
12
10
|
require_relative 'mint/registry/registry'
|
|
11
|
+
require_relative 'mint/registry/crypto'
|
|
13
12
|
|
|
14
13
|
require_relative 'money/money'
|
|
@@ -10,14 +10,14 @@ module Mint
|
|
|
10
10
|
# @raise [ArgumentError] if the proportions list is empty or sums to zero
|
|
11
11
|
#
|
|
12
12
|
# @example Proportional allocation
|
|
13
|
-
# money =
|
|
13
|
+
# money = Money.from(10.00, 'USD')
|
|
14
14
|
# money.allocate([1, 2, 3]) #=> [[USD 1.67], [USD 3.33], [USD 5.00]]
|
|
15
15
|
def allocate(proportions)
|
|
16
16
|
whole = proportions.sum.to_r
|
|
17
17
|
raise ArgumentError, 'Need at least 1 proportion element' if proportions.empty?
|
|
18
18
|
raise ArgumentError, 'Proportions total must not be zero' if whole.zero?
|
|
19
19
|
|
|
20
|
-
amounts = proportions.map { |rate| currency.normalize_amount(
|
|
20
|
+
amounts = proportions.map { |rate| currency.normalize_amount(amount * rate / whole) }
|
|
21
21
|
allocate_left_over(amounts: amounts, left_over: amount - amounts.sum)
|
|
22
22
|
end
|
|
23
23
|
end
|
|
@@ -12,7 +12,7 @@ module Mint
|
|
|
12
12
|
# @raise [ArgumentError] if quantity is not a positive integer
|
|
13
13
|
#
|
|
14
14
|
# @example Even split
|
|
15
|
-
# money =
|
|
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
18
|
raise ArgumentError, 'Slices quantity must be an poitive integer' unless slices.positive? && slices.integer?
|