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.
- checksums.yaml +4 -4
- data/README.md +298 -122
- 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 +77 -94
- data/lib/minting/currency/registry.rb +134 -0
- data/lib/minting/currency/rounding.rb +47 -0
- 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/registry.rb +9 -11
- data/lib/minting/mint/registry/symbols.rb +37 -30
- data/lib/minting/mint.rb +3 -2
- data/lib/minting/money/allocation/allocation.rb +2 -2
- data/lib/minting/money/allocation/split.rb +2 -2
- 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 +110 -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 +26 -0
- data/lib/minting/version.rb +1 -1
- data/lib/minting.rb +17 -8
- metadata +12 -31
- 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/mint/rounding.rb +0 -65
- 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 #
|
|
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 `
|
|
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
|
-
|
|
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,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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
#
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
|
|
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
|
-
#
|
|
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
|
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
|