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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +234 -116
  3. data/Rakefile +2 -7
  4. data/doc/agents/api_review-2026-06-15.md +1 -1
  5. data/doc/agents/copilot-instructions.md +2 -2
  6. data/doc/agents/expired/copilot-instructions.md +2 -2
  7. data/doc/agents/expired/gemini_gem_evaluation.md +2 -2
  8. data/lib/minting/aliases.rb +22 -0
  9. data/lib/minting/currency/currency.rb +81 -9
  10. data/lib/minting/data/crypto-currencies.yaml +126 -0
  11. data/lib/minting/mint/i18n.rb +79 -29
  12. data/lib/minting/mint/mint.rb +1 -26
  13. data/lib/minting/mint/registry/crypto.rb +59 -0
  14. data/lib/minting/mint/registry/registration.rb +1 -2
  15. data/lib/minting/mint/registry/symbols.rb +37 -30
  16. data/lib/minting/mint/rounding.rb +9 -7
  17. data/lib/minting/mint.rb +1 -2
  18. data/lib/minting/money/allocation/allocation.rb +2 -2
  19. data/lib/minting/money/allocation/split.rb +1 -1
  20. data/lib/minting/money/arithmetics/operators.rb +10 -13
  21. data/lib/minting/money/clamp.rb +6 -6
  22. data/lib/minting/money/coercion.rb +1 -1
  23. data/lib/minting/money/comparable.rb +3 -3
  24. data/lib/minting/money/constructors.rb +3 -42
  25. data/lib/minting/money/conversion.rb +22 -18
  26. data/lib/minting/money/format/format.rb +100 -0
  27. data/lib/minting/money/format/formatter.rb +102 -0
  28. data/lib/minting/money/format/to_s.rb +20 -102
  29. data/lib/minting/money/format/validator.rb +34 -0
  30. data/lib/minting/money/money.rb +25 -9
  31. data/lib/minting/money/parse.rb +127 -0
  32. data/lib/minting/money/rounding.rb +27 -0
  33. data/lib/minting/version.rb +1 -1
  34. data/lib/minting.rb +17 -8
  35. metadata +9 -29
  36. data/doc/Mint/Currency.html +0 -2032
  37. data/doc/Mint/Money.html +0 -5139
  38. data/doc/Mint/RangeStepPatch.html +0 -277
  39. data/doc/Mint/Registry.html +0 -863
  40. data/doc/Mint/Rounding.html +0 -506
  41. data/doc/Mint/UnknownCurrency.html +0 -138
  42. data/doc/Mint.html +0 -931
  43. data/doc/Minting.html +0 -142
  44. data/doc/Numeric.html +0 -479
  45. data/doc/String.html +0 -241
  46. data/doc/_index.html +0 -206
  47. data/doc/class_list.html +0 -54
  48. data/doc/css/common.css +0 -1
  49. data/doc/css/full_list.css +0 -206
  50. data/doc/css/style.css +0 -1089
  51. data/doc/file.README.html +0 -291
  52. data/doc/file_list.html +0 -59
  53. data/doc/frames.html +0 -22
  54. data/doc/index.html +0 -291
  55. data/doc/js/app.js +0 -801
  56. data/doc/js/full_list.js +0 -334
  57. data/doc/js/jquery.js +0 -4
  58. data/doc/method_list.html +0 -758
  59. data/doc/top-level-namespace.html +0 -135
  60. data/lib/minting/mint/aliases.rb +0 -16
  61. data/lib/minting/mint/parser/parser.rb +0 -97
  62. data/lib/minting/mint/parser/separators.rb +0 -41
  63. 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 amounts for this currency
44
- # 1. Converts to Rational
45
- # 2. Rounds to respect currency subunit
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
- # Mint::Currency.for_code('USD').zero #=> [USD 0.00]
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}, or {Money}.
72
- # Passing a {Money} extracts its currency
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] a currency code, object, or +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 (e.g. +Integer+)
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 raise ArgumentError, "currency must be [Currency], [Money], [String] or nil (#{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
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
@@ -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 no arguments and returns a Hash with these keys:
9
- # [+:decimal+] Decimal separator (e.g. +","+)
10
- # [+:thousand+] Thousands delimiter (e.g. +"."+)
11
- # [+:format+] Format template string (e.g. +"%<amount>f %<symbol>s"+)
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 Rails I18n integration (in attribute-money railtie)
17
- # Mint.locale_backend = -> {
18
- # fmt = I18n.t('number.currency.format')
19
- # {
20
- # decimal: fmt[:separator],
21
- # thousand: fmt[:delimiter],
22
- # format: fmt[:format] == '%n %u' ? '%<amount>f %<symbol>s' : '%<symbol>s%<amount>f'
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
- class Money
32
- private
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
- # Resolves format/decimal/thousand from locale_backend when not explicitly given.
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
- [format || locale[:format] || DEFAULT_FORMAT,
40
- decimal || locale[:decimal] || '.',
41
- thousand.nil? ? (locale[:thousand] || ',') : thousand]
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
- def locale_backend
45
- case bk = Mint.locale_backend
46
- when Hash then bk
47
- when NilClass then {}
48
- else
49
- return bk.call if bk.respond_to?(:call)
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: #{bk.inspect}"
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
@@ -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
- # {Mint::Currency.resolve!} so all accepted types resolve to a registered
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
@@ -24,8 +24,7 @@ module Mint
24
24
 
25
25
  currency = Currency.new(code:, subunit:, symbol:, priority:)
26
26
  @currencies = @currencies.merge(code => currency).freeze
27
- @currency_symbols = nil
28
- @currency_symbol_map = nil
27
+ @symbols_list = nil
29
28
  currency
30
29
  end
31
30
  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
- @currency_symbol_map || MUTEX.synchronize { @currency_symbol_map = currency_symbols.to_h.freeze }
15
- @currency_symbol_map[symbol]
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
- currency_symbols.each do |symbol, currency|
25
- return currency if input.include?(symbol)
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
- # Registered symbols sorted for detection: longest match wins, then parser priority.
33
- # Duplicate symbols are deduplicated — the highest-priority currency wins.
34
- #
35
- # @return [Array<Array<String, Currency>>] sorted symbol-to-currency mappings
36
- # @api private
37
- def currency_symbols
38
- @currency_symbols || MUTEX.synchronize do
39
- @currency_symbols =
40
- currencies.values
41
- .reject { |currency| currency.symbol.empty? }
42
- .map { |currency| [currency.symbol, currency] }
43
- .sort_by { |symbol, currency| [-symbol.length, -currency.priority] }
44
- .uniq { |symbol, _| symbol }
45
- .freeze
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[:minting_rounding_mode] || :half_up
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[:minting_rounding_mode]
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[:minting_rounding_mode]
52
- Thread.current[:minting_rounding_mode] = mode
53
+ prev = Thread.current[THREAD_KEY]
54
+ Thread.current[THREAD_KEY] = mode
53
55
  yield
54
56
  ensure
55
- Thread.current[:minting_rounding_mode] = prev
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
- Mint::Currency.remove_method(:normalize_amount)
63
- Mint::Currency.define_method(:normalize_amount) do |amount|
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 = Mint.money(10.00, 'USD')
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(Rational(amount * rate, whole)) }
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 = Mint.money(10.00, 'USD')
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?