minting 1.9.7 → 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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +298 -122
  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 +77 -94
  10. data/lib/minting/currency/registry.rb +134 -0
  11. data/lib/minting/currency/rounding.rb +47 -0
  12. data/lib/minting/data/crypto-currencies.yaml +126 -0
  13. data/lib/minting/mint/i18n.rb +79 -29
  14. data/lib/minting/mint/mint.rb +1 -26
  15. data/lib/minting/mint/registry/crypto.rb +59 -0
  16. data/lib/minting/mint/registry/registration.rb +1 -2
  17. data/lib/minting/mint/registry/registry.rb +9 -11
  18. data/lib/minting/mint/registry/symbols.rb +37 -30
  19. data/lib/minting/mint.rb +3 -2
  20. data/lib/minting/money/allocation/allocation.rb +2 -2
  21. data/lib/minting/money/allocation/split.rb +2 -2
  22. data/lib/minting/money/arithmetics/operators.rb +10 -13
  23. data/lib/minting/money/clamp.rb +6 -6
  24. data/lib/minting/money/coercion.rb +1 -1
  25. data/lib/minting/money/comparable.rb +3 -3
  26. data/lib/minting/money/constructors.rb +3 -42
  27. data/lib/minting/money/conversion.rb +22 -18
  28. data/lib/minting/money/format/format.rb +100 -0
  29. data/lib/minting/money/format/formatter.rb +110 -0
  30. data/lib/minting/money/format/to_s.rb +20 -102
  31. data/lib/minting/money/format/validator.rb +34 -0
  32. data/lib/minting/money/money.rb +25 -9
  33. data/lib/minting/money/parse.rb +127 -0
  34. data/lib/minting/money/rounding.rb +26 -0
  35. data/lib/minting/version.rb +1 -1
  36. data/lib/minting.rb +17 -8
  37. metadata +12 -31
  38. data/doc/Mint/Currency.html +0 -2032
  39. data/doc/Mint/Money.html +0 -5139
  40. data/doc/Mint/RangeStepPatch.html +0 -277
  41. data/doc/Mint/Registry.html +0 -863
  42. data/doc/Mint/Rounding.html +0 -506
  43. data/doc/Mint/UnknownCurrency.html +0 -138
  44. data/doc/Mint.html +0 -931
  45. data/doc/Minting.html +0 -142
  46. data/doc/Numeric.html +0 -479
  47. data/doc/String.html +0 -241
  48. data/doc/_index.html +0 -206
  49. data/doc/class_list.html +0 -54
  50. data/doc/css/common.css +0 -1
  51. data/doc/css/full_list.css +0 -206
  52. data/doc/css/style.css +0 -1089
  53. data/doc/file.README.html +0 -291
  54. data/doc/file_list.html +0 -59
  55. data/doc/frames.html +0 -22
  56. data/doc/index.html +0 -291
  57. data/doc/js/app.js +0 -801
  58. data/doc/js/full_list.js +0 -334
  59. data/doc/js/jquery.js +0 -4
  60. data/doc/method_list.html +0 -758
  61. data/doc/top-level-namespace.html +0 -135
  62. data/lib/minting/mint/aliases.rb +0 -16
  63. data/lib/minting/mint/parser/parser.rb +0 -97
  64. data/lib/minting/mint/parser/separators.rb +0 -41
  65. data/lib/minting/mint/rounding.rb +0 -65
  66. data/lib/minting/money/format/formatting.rb +0 -130
@@ -29,7 +29,7 @@ lib/
29
29
  │ ├── data/
30
30
  │ │ └── currencies.yaml # built-in ISO 4217 database
31
31
  │ ├── mint/
32
- │ │ ├── currency.rb # Mint::Currency model
32
+ │ │ ├── currency.rb # Money::Currency model
33
33
  │ │ ├── refinements.rb # Scoped Numeric/String refinements
34
34
  │ │ └── registry.rb # Global lookup & registration
35
35
  │ ├── money/
@@ -70,7 +70,7 @@ lib/
70
70
 
71
71
  The gem exhibits top-tier Ruby development practices:
72
72
 
73
- - **Immutability & Safety**: Both `Mint::Currency` and `Mint::Money` objects are frozen upon initialization:
73
+ - **Immutability & Safety**: Both `Money::Currency` and `Mint::Money` objects are frozen upon initialization:
74
74
  ```ruby
75
75
  def initialize(amount, currency)
76
76
  # ...
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @!parse
4
+ # # Top-level alias for {Money::Currency}, opt-in via +require 'minting/aliases'+.
5
+ # Currency = Money::Currency
6
+
7
+ # Top-level alias for {Money::Currency}, opt-in via +require 'minting/aliases'+.
8
+ #
9
+ # {::Currency} is **not** auto-bound by `require 'minting'` because
10
+ # application domain models are commonly named +Currency+ (e.g. a Rails
11
+ # model). Load this file explicitly to opt in.
12
+ #
13
+ # If {::Currency} is already defined, a warning is emitted and the existing
14
+ # constant is preserved — use {Money::Currency} explicitly in that case.
15
+ #
16
+ # @see Money::Currency
17
+
18
+ Currency = Money::Currency unless defined?(Currency)
19
+
20
+ if Currency != Money::Currency
21
+ warn "minting: top-level Currency was already defined (#{Currency}); skipping alias. Use Money::Currency"
22
+ end
@@ -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
- # @attr_reader code [String] ISO 4217 currency code (e.g., "USD", "EUR")
11
- # @attr_reader subunit [Integer] Number of decimal places (0 for JPY, 2 for USD, 3 for IQD)
12
- # @attr_reader symbol [String] Display symbol (e.g., "$", "€", "R$")
13
- # @attr_reader priority [Integer] Parser precedence for symbol detection
14
- # @attr_reader country [String, nil] Associated country code
15
- # @attr_reader name [String, nil] Currency name
16
- # @attr_reader fractional_multiplier [Integer] 10^subunit, used for fractional conversions
17
- # @attr_reader minimum_amount [Rational] Smallest representable amount (1/fractional_multiplier)
18
- # @attr_reader disambiguate_symbol [String, nil] A longer, code-prefixed variant to distinguish
19
- # currencies that share the same primary symbol (e.g. "US$" for USD, "C$" for CAD).
20
- Currency = Data.define(:code, :subunit, :symbol, :priority, :country, :name,
21
- :fractional_multiplier, :disambiguate_symbol) do
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,98 +48,60 @@ 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
- subunit = subunit.to_i
31
- priority = priority.to_i
32
- fractional_multiplier = 10**subunit
33
- super(code:, subunit:, symbol:, priority:, country:, name:,
34
- fractional_multiplier:, disambiguate_symbol:)
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
35
62
  end
36
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
+
37
78
  # @return [String] debug representation
38
79
  def inspect = "<Currency:(#{code} #{symbol} #{subunit} #{name})>"
39
80
 
40
- # @return [Rational] smallest representable amount (1/fractional_multiplier)
41
- def minimum_amount = Rational(1, fractional_multiplier)
42
-
43
- # Normalizes numeric amounts for this currency
44
- # 1. Converts to Rational
45
- # 2. Rounds to respect currency subunit
46
- def normalize_amount(amount) = amount.to_r.round(subunit)
81
+ # Normalizes a numeric amount for this currency.
82
+ #
83
+ # @param amount [Numeric] the monetary amount to normalize
84
+ # @return [Rational] the amount converted to +Rational+ and rounded to
85
+ # the currency's subunit precision (up by default)
86
+ # @example
87
+ # usd = Money::Currency.for_code('USD')
88
+ # usd.normalize_amount(10.567) #=> (10567/1000)
89
+ # usd.normalize_amount("5.25") #=> (21/4)
90
+ #
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
47
99
 
48
100
  # Returns the cached frozen zero-Money for this currency.
49
101
  #
50
102
  # @return [Money] a frozen zero-Money instance
51
103
  # @example
52
- # Mint::Currency.for_code('USD').zero #=> [USD 0.00]
104
+ # Money::Currency.for_code('USD').zero #=> [USD 0.00]
53
105
  def zero = Registry.zero_for(self)
54
106
  end
55
-
56
- # Registers a new currency, raising a KeyError if already registered.
57
- #
58
- # @param code [String] the unique currency code
59
- # @param subunit [Integer] the decimal subunit precision, defaults to 0
60
- # @param symbol [String] the display symbol
61
- # @param priority [Integer] parser precedence priority
62
- # @return [Currency] the newly registered Currency instance
63
- # @raise [ArgumentError] if the code contains invalid characters
64
- # @raise [KeyError] if the currency code is already registered
65
- def Currency.register(code:, subunit: 0, symbol: '', priority: 0)
66
- Registry.register(code:, subunit:, symbol:, priority:)
67
- end
68
-
69
- # Resolves an object into a {Currency}, returning +nil+ when it can't.
70
- #
71
- # Accepts +nil+, +String+, {Currency}, or {Money}.
72
- # Passing a {Money} extracts its currency
73
- #
74
- # @param object [String, Currency, Money, nil] a currency code, object, or +nil+
75
- # @return [Currency, nil] the resolved Currency, or +nil+ if +object+ is +nil+
76
- # or the code is not registered
77
- # @raise [ArgumentError] if +object+ is an unsupported type (e.g. +Integer+)
78
- def Currency.resolve(object)
79
- case object
80
- when NilClass then nil
81
- when Currency then object
82
- when Money then object.currency
83
- when String then Currency.for_code object
84
- else raise ArgumentError, "currency must be [Currency], [Money], [String] or nil (#{object})"
85
- end
86
- end
87
-
88
- # Resolves an object into a {Currency}, raising on failure.
89
- #
90
- # Like {.resolve} but raises when the result would be +nil+.
91
- #
92
- # @param object [String, Currency, Money, nil] a currency code, object, or +nil+
93
- # @return [Currency] the resolved Currency
94
- # @raise [Mint::UnknownCurrency] if +object+ cannot be resolved into a
95
- # registered currency. +Mint::UnknownCurrency+ inherits from +ArgumentError+,
96
- # so existing +rescue ArgumentError+ handlers continue to work.
97
- def Currency.resolve!(object)
98
- resolve(object) or raise Mint::UnknownCurrency, "Could not resolve (#{object}) into a currency"
99
- end
100
-
101
- # Looks up a registered currency by its alpha code.
102
- #
103
- # @param code [String] the currency code
104
- # @return [Currency, nil] the registered Currency, or +nil+ if not found
105
- def Currency.for_code(code)
106
- Registry.currencies[code]
107
- end
108
-
109
- # Looks up a currency by its display symbol.
110
- #
111
- # @param symbol [String] the display symbol (e.g. "$", "R$")
112
- # @return [Currency, nil] the highest-priority currency for the symbol
113
- def Currency.for_symbol(symbol)
114
- Registry.currency_for_symbol(symbol)
115
- end
116
-
117
- # Returns a zero {Money} in the given currency, useful as a default value
118
- # for discounts, totals, or placeholders.
119
- #
120
- # @param currency [String, Currency] a currency code or object
121
- # @return [Money] a frozen zero-Money
122
- # @raise [Mint::UnknownCurrency] if the currency can't be resolved
123
- def Currency.zero(currency) = Registry.zero_for(Currency.resolve!(currency))
124
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
@@ -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