minting 1.0.1 → 1.1.1
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/LICENSE +21 -0
- data/README.md +41 -1
- data/Rakefile +8 -1
- data/lib/minting/data/currencies.yaml +711 -0
- data/lib/minting/mint/currency.rb +9 -8
- data/lib/minting/mint/registry.rb +73 -138
- data/lib/minting/money/allocation.rb +24 -5
- data/lib/minting/money/arithmetics.rb +37 -2
- data/lib/minting/money/coercion.rb +14 -3
- data/lib/minting/money/comparable.rb +13 -16
- data/lib/minting/money/conversion.rb +30 -2
- data/lib/minting/money/formatting.rb +5 -4
- data/lib/minting/money/money.rb +24 -15
- data/lib/minting/money/parse.rb +82 -0
- data/lib/minting/money.rb +1 -0
- data/lib/minting/version.rb +3 -1
- data/minting.gemspec +1 -1
- metadata +5 -2
|
@@ -1,13 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
require 'yaml'
|
|
2
|
+
|
|
2
3
|
module Mint
|
|
4
|
+
# Creates a new {Money} instance with the given amount and currency code.
|
|
5
|
+
#
|
|
6
|
+
# @param amount [Numeric] the financial value
|
|
7
|
+
# @param currency_code [String, Symbol] the ISO currency code or symbol
|
|
8
|
+
# @return [Money] the instantiated Money object
|
|
9
|
+
# @raise [ArgumentError] if the currency code is not registered
|
|
3
10
|
def self.money(amount, currency_code)
|
|
4
11
|
currency = currency(currency_code)
|
|
5
|
-
return Money.new(amount, currency)
|
|
12
|
+
return Money.new(amount, currency) if currency
|
|
6
13
|
|
|
7
|
-
|
|
8
|
-
raise ArgumentError, "Currency [#{currency_code}] not registered. Available: #{available}"
|
|
14
|
+
raise ArgumentError, "Currency [#{currency_code}] not registered. Check Mint.currencies"
|
|
9
15
|
end
|
|
10
16
|
|
|
17
|
+
# Finds a registered currency by its code, symbol,
|
|
18
|
+
# or retrieves it directly if already a Currency object.
|
|
19
|
+
#
|
|
20
|
+
# @param currency [String, Symbol, Currency] the currency identifier or object
|
|
21
|
+
# @return [Currency, nil] the registered Currency instance or nil if not found
|
|
11
22
|
def self.currency(currency)
|
|
12
23
|
case currency
|
|
13
24
|
when Currency
|
|
@@ -19,12 +30,29 @@ module Mint
|
|
|
19
30
|
end
|
|
20
31
|
end
|
|
21
32
|
|
|
22
|
-
|
|
33
|
+
# Registers a new currency if not already registered.
|
|
34
|
+
#
|
|
35
|
+
# @param code [String, Symbol] the unique currency code (e.g. 'USD', :EUR)
|
|
36
|
+
# @param subunit [Integer] the decimal subunit precision (defaults to 2)
|
|
37
|
+
# @param symbol [String] the display symbol (defaults to '$')
|
|
38
|
+
# @param priority [Integer] parser precedence priority (defaults to 0)
|
|
39
|
+
# @return [Currency] the registered or existing Currency instance
|
|
40
|
+
# @raise [ArgumentError] if the code layout is invalid or register throws an error
|
|
41
|
+
def self.register_currency(code:, subunit: 2, symbol: '$', priority: 0)
|
|
23
42
|
code = code.to_s
|
|
24
|
-
currencies[code] || register_currency!(code
|
|
43
|
+
currencies[code] || register_currency!(code:, subunit:, symbol:, priority:)
|
|
25
44
|
end
|
|
26
45
|
|
|
27
|
-
|
|
46
|
+
# Strictly registers a new currency, raising a KeyError if already registered.
|
|
47
|
+
#
|
|
48
|
+
# @param code [String, Symbol] the unique currency code
|
|
49
|
+
# @param subunit [Integer] the decimal subunit precision
|
|
50
|
+
# @param symbol [String] the display symbol
|
|
51
|
+
# @param priority [Integer] parser precedence priority
|
|
52
|
+
# @return [Currency] the newly registered Currency instance
|
|
53
|
+
# @raise [ArgumentError] if the code contains invalid characters
|
|
54
|
+
# @raise [KeyError] if the currency code is already registered
|
|
55
|
+
def self.register_currency!(code:, subunit:, symbol: '', priority: 0)
|
|
28
56
|
code = code.to_s
|
|
29
57
|
unless code.match?(/^[A-Z_]+$/)
|
|
30
58
|
raise ArgumentError,
|
|
@@ -35,142 +63,49 @@ module Mint
|
|
|
35
63
|
"Currency: #{code} already registered"
|
|
36
64
|
end
|
|
37
65
|
|
|
38
|
-
currencies[code] =
|
|
39
|
-
|
|
66
|
+
currencies[code] = Currency.new(code:, subunit:, symbol:, priority:)
|
|
67
|
+
@currency_symbols = nil
|
|
68
|
+
currencies[code]
|
|
40
69
|
end
|
|
41
70
|
|
|
71
|
+
# Returns the hash of all registered currencies.
|
|
72
|
+
#
|
|
73
|
+
# @return [Hash{String => Currency}] registered currencies mapped by code
|
|
42
74
|
def self.currencies
|
|
43
|
-
@currencies ||=
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
'JPY' => Currency.new('JPY', subunit: 0, symbol: '¥'),
|
|
49
|
-
'CHF' => Currency.new('CHF', subunit: 2, symbol: 'Fr'),
|
|
50
|
-
'CAD' => Currency.new('CAD', subunit: 2, symbol: '$'),
|
|
51
|
-
'AUD' => Currency.new('AUD', subunit: 2, symbol: '$'),
|
|
52
|
-
'CNY' => Currency.new('CNY', subunit: 2, symbol: '¥'),
|
|
53
|
-
'SEK' => Currency.new('SEK', subunit: 2, symbol: 'kr'),
|
|
54
|
-
'NZD' => Currency.new('NZD', subunit: 2, symbol: '$'),
|
|
55
|
-
|
|
56
|
-
# Asia-Pacific
|
|
57
|
-
'HKD' => Currency.new('HKD', subunit: 2, symbol: 'HK$'),
|
|
58
|
-
'SGD' => Currency.new('SGD', subunit: 2, symbol: 'S$'),
|
|
59
|
-
'KRW' => Currency.new('KRW', subunit: 0, symbol: '₩'),
|
|
60
|
-
'INR' => Currency.new('INR', subunit: 2, symbol: '₹'),
|
|
61
|
-
'THB' => Currency.new('THB', subunit: 2, symbol: '฿'),
|
|
62
|
-
'MYR' => Currency.new('MYR', subunit: 2, symbol: 'RM'),
|
|
63
|
-
'IDR' => Currency.new('IDR', subunit: 2, symbol: 'Rp'),
|
|
64
|
-
'PHP' => Currency.new('PHP', subunit: 2, symbol: '₱'),
|
|
65
|
-
'VND' => Currency.new('VND', subunit: 0, symbol: '₫'),
|
|
66
|
-
'TWD' => Currency.new('TWD', subunit: 2, symbol: 'NT$'),
|
|
67
|
-
'PKR' => Currency.new('PKR', subunit: 2, symbol: '₨'),
|
|
68
|
-
'BDT' => Currency.new('BDT', subunit: 2, symbol: '৳'),
|
|
69
|
-
'LKR' => Currency.new('LKR', subunit: 2, symbol: '₨'),
|
|
70
|
-
'NPR' => Currency.new('NPR', subunit: 2, symbol: '₨'),
|
|
71
|
-
'MMK' => Currency.new('MMK', subunit: 2, symbol: 'K'),
|
|
72
|
-
'KHR' => Currency.new('KHR', subunit: 2, symbol: '៛'),
|
|
73
|
-
'LAK' => Currency.new('LAK', subunit: 2, symbol: '₭'),
|
|
74
|
-
'BND' => Currency.new('BND', subunit: 2, symbol: 'B$'),
|
|
75
|
-
|
|
76
|
-
# Middle East & Central Asia
|
|
77
|
-
'AED' => Currency.new('AED', subunit: 2, symbol: 'د.إ'),
|
|
78
|
-
'SAR' => Currency.new('SAR', subunit: 2, symbol: '﷼'),
|
|
79
|
-
'QAR' => Currency.new('QAR', subunit: 2, symbol: '﷼'),
|
|
80
|
-
'KWD' => Currency.new('KWD', subunit: 3, symbol: 'د.ك'),
|
|
81
|
-
'BHD' => Currency.new('BHD', subunit: 3, symbol: '.د.ب'),
|
|
82
|
-
'OMR' => Currency.new('OMR', subunit: 3, symbol: '﷼'),
|
|
83
|
-
'JOD' => Currency.new('JOD', subunit: 3, symbol: 'د.ا'),
|
|
84
|
-
'ILS' => Currency.new('ILS', subunit: 2, symbol: '₪'),
|
|
85
|
-
'TRY' => Currency.new('TRY', subunit: 2, symbol: '₺'),
|
|
86
|
-
'IRR' => Currency.new('IRR', subunit: 2, symbol: '﷼'),
|
|
87
|
-
'IQD' => Currency.new('IQD', subunit: 3, symbol: 'د.ع'),
|
|
88
|
-
'AFN' => Currency.new('AFN', subunit: 2, symbol: '؋'),
|
|
89
|
-
'KZT' => Currency.new('KZT', subunit: 2, symbol: '₸'),
|
|
90
|
-
'UZS' => Currency.new('UZS', subunit: 2, symbol: 'лв'),
|
|
91
|
-
'KGS' => Currency.new('KGS', subunit: 2, symbol: 'лв'),
|
|
92
|
-
'TJS' => Currency.new('TJS', subunit: 2, symbol: 'SM'),
|
|
93
|
-
|
|
94
|
-
# Europe
|
|
95
|
-
'NOK' => Currency.new('NOK', subunit: 2, symbol: 'kr'),
|
|
96
|
-
'DKK' => Currency.new('DKK', subunit: 2, symbol: 'kr'),
|
|
97
|
-
'ISK' => Currency.new('ISK', subunit: 0, symbol: 'kr'),
|
|
98
|
-
'PLN' => Currency.new('PLN', subunit: 2, symbol: 'zł'),
|
|
99
|
-
'CZK' => Currency.new('CZK', subunit: 2, symbol: 'Kč'),
|
|
100
|
-
'HUF' => Currency.new('HUF', subunit: 2, symbol: 'Ft'),
|
|
101
|
-
'RON' => Currency.new('RON', subunit: 2, symbol: 'lei'),
|
|
102
|
-
'BGN' => Currency.new('BGN', subunit: 2, symbol: 'лв'),
|
|
103
|
-
'HRK' => Currency.new('HRK', subunit: 2, symbol: 'kn'),
|
|
104
|
-
'RSD' => Currency.new('RSD', subunit: 2, symbol: 'Дин.'),
|
|
105
|
-
'RUB' => Currency.new('RUB', subunit: 2, symbol: '₽'),
|
|
106
|
-
'UAH' => Currency.new('UAH', subunit: 2, symbol: '₴'),
|
|
107
|
-
'BYN' => Currency.new('BYN', subunit: 2, symbol: 'Br'),
|
|
108
|
-
'MDL' => Currency.new('MDL', subunit: 2, symbol: 'L'),
|
|
109
|
-
'GEL' => Currency.new('GEL', subunit: 2, symbol: '₾'),
|
|
110
|
-
'AMD' => Currency.new('AMD', subunit: 2, symbol: '֏'),
|
|
111
|
-
'AZN' => Currency.new('AZN', subunit: 2, symbol: '₼'),
|
|
75
|
+
@currencies ||= begin
|
|
76
|
+
registry = { 'XXX' => Currency.new(code: 'XXX', name: 'No currency', symbol: '¤') }
|
|
77
|
+
load_currencies(registry)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
112
80
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
'MAD' => Currency.new('MAD', subunit: 2, symbol: 'د.م.'),
|
|
123
|
-
'TND' => Currency.new('TND', subunit: 3, symbol: 'د.ت'),
|
|
124
|
-
'DZD' => Currency.new('DZD', subunit: 2, symbol: 'د.ج'),
|
|
125
|
-
'LYD' => Currency.new('LYD', subunit: 3, symbol: 'ل.د'),
|
|
126
|
-
'AOA' => Currency.new('AOA', subunit: 2, symbol: 'Kz'),
|
|
127
|
-
'BWP' => Currency.new('BWP', subunit: 2, symbol: 'P'),
|
|
128
|
-
'NAD' => Currency.new('NAD', subunit: 2, symbol: 'N$'),
|
|
129
|
-
'SZL' => Currency.new('SZL', subunit: 2, symbol: 'L'),
|
|
130
|
-
'LSL' => Currency.new('LSL', subunit: 2, symbol: 'L'),
|
|
131
|
-
'MZN' => Currency.new('MZN', subunit: 2, symbol: 'MT'),
|
|
132
|
-
'ZMW' => Currency.new('ZMW', subunit: 2, symbol: 'ZK'),
|
|
133
|
-
'MWK' => Currency.new('MWK', subunit: 2, symbol: 'MK'),
|
|
134
|
-
'RWF' => Currency.new('RWF', subunit: 0, symbol: 'R₣'),
|
|
135
|
-
'BIF' => Currency.new('BIF', subunit: 0, symbol: 'FBu'),
|
|
81
|
+
# Registered symbols sorted for detection: longest match wins, then parser priority.
|
|
82
|
+
def self.currency_symbols
|
|
83
|
+
@currency_symbols ||= begin
|
|
84
|
+
currencies.values
|
|
85
|
+
.map { |currency| [currency.symbol, currency] }
|
|
86
|
+
.reject { |symbol, _| symbol.empty? }
|
|
87
|
+
.sort_by { |symbol, currency| [-symbol.length, -currency.priority] }
|
|
88
|
+
end.freeze
|
|
89
|
+
end
|
|
136
90
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
'ARS' => Currency.new('ARS', subunit: 2, symbol: '$'),
|
|
141
|
-
'CLP' => Currency.new('CLP', subunit: 0, symbol: '$'),
|
|
142
|
-
'PEN' => Currency.new('PEN', subunit: 2, symbol: 'S/.'),
|
|
143
|
-
'COP' => Currency.new('COP', subunit: 2, symbol: '$'),
|
|
144
|
-
'VES' => Currency.new('VES', subunit: 2, symbol: 'Bs.'),
|
|
145
|
-
'UYU' => Currency.new('UYU', subunit: 2, symbol: '$U'),
|
|
146
|
-
'PYG' => Currency.new('PYG', subunit: 0, symbol: 'Gs'),
|
|
147
|
-
'BOB' => Currency.new('BOB', subunit: 2, symbol: '$b'),
|
|
148
|
-
'CRC' => Currency.new('CRC', subunit: 2, symbol: '₡'),
|
|
149
|
-
'GTQ' => Currency.new('GTQ', subunit: 2, symbol: 'Q'),
|
|
150
|
-
'HNL' => Currency.new('HNL', subunit: 2, symbol: 'L'),
|
|
151
|
-
'NIO' => Currency.new('NIO', subunit: 2, symbol: 'C$'),
|
|
152
|
-
'PAB' => Currency.new('PAB', subunit: 2, symbol: 'B/.'),
|
|
153
|
-
'DOP' => Currency.new('DOP', subunit: 2, symbol: 'RD$'),
|
|
154
|
-
'HTG' => Currency.new('HTG', subunit: 2, symbol: 'G'),
|
|
155
|
-
'JMD' => Currency.new('JMD', subunit: 2, symbol: 'J$'),
|
|
156
|
-
'TTD' => Currency.new('TTD', subunit: 2, symbol: 'TT$'),
|
|
157
|
-
'BBD' => Currency.new('BBD', subunit: 2, symbol: 'Bds$'),
|
|
158
|
-
'BSD' => Currency.new('BSD', subunit: 2, symbol: 'B$'),
|
|
159
|
-
'BZD' => Currency.new('BZD', subunit: 2, symbol: 'BZ$'),
|
|
160
|
-
'GYD' => Currency.new('GYD', subunit: 2, symbol: 'G$'),
|
|
161
|
-
'SRD' => Currency.new('SRD', subunit: 2, symbol: 'Sr$'),
|
|
91
|
+
def self.load_currencies(registry)
|
|
92
|
+
base = File.expand_path('../data', __dir__)
|
|
93
|
+
path = File.join(base, 'currencies.yaml')
|
|
162
94
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
95
|
+
data = YAML.load_file(path)
|
|
96
|
+
data.each do |entry|
|
|
97
|
+
code = entry['code']
|
|
98
|
+
registry[code] = Currency.new(
|
|
99
|
+
code: code,
|
|
100
|
+
subunit: entry['subunit'],
|
|
101
|
+
symbol: entry['symbol'],
|
|
102
|
+
priority: entry['priority'],
|
|
103
|
+
country: entry['country'],
|
|
104
|
+
name: entry['name']
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
registry
|
|
175
108
|
end
|
|
109
|
+
|
|
110
|
+
private_class_method :load_currencies
|
|
176
111
|
end
|
|
@@ -1,15 +1,34 @@
|
|
|
1
1
|
module Mint
|
|
2
|
-
# :nodoc
|
|
3
|
-
# split and allocation methods
|
|
4
2
|
class Money
|
|
3
|
+
# Proportionally allocates the monetary amount among a list of ratios.
|
|
4
|
+
# Disperses any subunit rounding amounts across the initial slots
|
|
5
|
+
# @param proportions [Array<Numeric>] a list of numeric proportions/ratios to allocate by
|
|
6
|
+
# @return [Array<Money>] the list of newly allocated Money objects
|
|
7
|
+
# @raise [ArgumentError] if the proportions list is empty or sums to zero
|
|
8
|
+
#
|
|
9
|
+
# @example Proportional allocation
|
|
10
|
+
# money = Mint.money(10.00, 'USD')
|
|
11
|
+
# money.allocate([1, 2, 3]) #=> [[USD 1.67], [USD 3.33], [USD 5.00]]
|
|
5
12
|
def allocate(proportions)
|
|
13
|
+
whole = proportions.sum.to_r
|
|
6
14
|
raise ArgumentError, 'Need at least 1 proportion element' if proportions.empty?
|
|
15
|
+
raise ArgumentError, 'Proportions total must not be zero' if whole.zero?
|
|
7
16
|
|
|
8
|
-
|
|
9
|
-
amounts = proportions.map! { |rate| (amount * rate.to_r / whole).round(currency.subunit) }
|
|
17
|
+
amounts = proportions.map { |rate| (amount * rate.to_r / whole).round(currency.subunit) }
|
|
10
18
|
allocate_left_over!(amounts: amounts, left_over: amount - amounts.sum)
|
|
11
19
|
end
|
|
12
20
|
|
|
21
|
+
# Splits the monetary amount into a given quantity of equal parts.
|
|
22
|
+
# Disperses any fractional subunit rounding differences across the initial slots
|
|
23
|
+
# so that the sum is preserved.
|
|
24
|
+
#
|
|
25
|
+
# @param quantity [Integer] the number of equal parts to divide the money into (must be > 0)
|
|
26
|
+
# @return [Array<Money>] the list of newly split Money objects
|
|
27
|
+
# @raise [ArgumentError] if quantity is not a positive integer
|
|
28
|
+
#
|
|
29
|
+
# @example Even split
|
|
30
|
+
# money = Mint.money(10.00, 'USD')
|
|
31
|
+
# money.split(3) #=> [[USD 3.34], [USD 3.33], [USD 3.33]]
|
|
13
32
|
def split(quantity)
|
|
14
33
|
unless quantity.positive? && quantity.integer?
|
|
15
34
|
raise ArgumentError,
|
|
@@ -29,7 +48,7 @@ module Mint
|
|
|
29
48
|
last_slot = (left_over / minimum).to_i - 1
|
|
30
49
|
(0..last_slot).each { |slot| amounts[slot] += minimum }
|
|
31
50
|
end
|
|
32
|
-
amounts.map { mint
|
|
51
|
+
amounts.map { mint it }
|
|
33
52
|
end
|
|
34
53
|
end
|
|
35
54
|
end
|
|
@@ -1,15 +1,31 @@
|
|
|
1
1
|
module Mint
|
|
2
|
-
# :nodoc
|
|
3
|
-
# Arithmetic functions for money objects
|
|
4
2
|
class Money
|
|
3
|
+
# Returns the absolute value of the monetary amount as a new {Money} instance.
|
|
4
|
+
#
|
|
5
|
+
# @return [Money] the absolute value
|
|
5
6
|
def abs = mint(amount.abs)
|
|
6
7
|
|
|
8
|
+
# Returns true if the monetary amount is less than zero.
|
|
9
|
+
#
|
|
10
|
+
# @return [Boolean] true if negative, false otherwise
|
|
7
11
|
def negative? = amount.negative?
|
|
8
12
|
|
|
13
|
+
# Returns true if the monetary amount is greater than zero.
|
|
14
|
+
#
|
|
15
|
+
# @return [Boolean] true if positive, false otherwise
|
|
9
16
|
def positive? = amount.positive?
|
|
10
17
|
|
|
18
|
+
# Returns the successor of the Money instance by adding the minimum possible subunit amount.
|
|
19
|
+
# Enables standard ranges and stepping (e.g. `1.dollar..10.dollars`).
|
|
20
|
+
#
|
|
21
|
+
# @return [Money] successor Money instance
|
|
11
22
|
def succ = mint(amount + currency.minimum_amount)
|
|
12
23
|
|
|
24
|
+
# Performs addition with another {Money} instance or standard zero Numeric.
|
|
25
|
+
#
|
|
26
|
+
# @param addend [Money, Numeric] the value to add
|
|
27
|
+
# @return [Money] the sum of the addition
|
|
28
|
+
# @raise [TypeError] if addition involves a different currency or incompatible types
|
|
13
29
|
def +(addend)
|
|
14
30
|
return self if addend.respond_to?(:zero?) && addend.zero?
|
|
15
31
|
return mint(amount + addend.amount) if addend.is_a?(Money) && same_currency?(addend)
|
|
@@ -17,6 +33,11 @@ module Mint
|
|
|
17
33
|
raise TypeError, "#{addend} can't be added to #{self}"
|
|
18
34
|
end
|
|
19
35
|
|
|
36
|
+
# Performs subtraction with another {Money} instance or standard zero Numeric.
|
|
37
|
+
#
|
|
38
|
+
# @param subtrahend [Money, Numeric] the value to subtract
|
|
39
|
+
# @return [Money] the difference of the subtraction
|
|
40
|
+
# @raise [TypeError] if subtraction involves a different currency or incompatible types
|
|
20
41
|
def -(subtrahend)
|
|
21
42
|
return self if subtrahend.respond_to?(:zero?) && subtrahend.zero?
|
|
22
43
|
if subtrahend.is_a?(Money) && same_currency?(subtrahend)
|
|
@@ -26,16 +47,30 @@ module Mint
|
|
|
26
47
|
raise TypeError, "#{subtrahend} can't be subtracted from #{self}"
|
|
27
48
|
end
|
|
28
49
|
|
|
50
|
+
# Unary negation operator. Returns a new {Money} instance with the inverted sign.
|
|
51
|
+
#
|
|
52
|
+
# @return [Money] negated Money instance
|
|
29
53
|
def -@
|
|
30
54
|
mint(-amount)
|
|
31
55
|
end
|
|
32
56
|
|
|
57
|
+
# Performs multiplication of the monetary value by a standard scalar Numeric.
|
|
58
|
+
#
|
|
59
|
+
# @param multiplicand [Numeric] the scalar multiplier
|
|
60
|
+
# @return [Money] the multiplied Money instance
|
|
61
|
+
# @raise [TypeError] if multiplier is not Numeric or is a Money object
|
|
33
62
|
def *(multiplicand)
|
|
34
63
|
return mint(amount * multiplicand.to_r) if multiplicand.is_a?(Numeric)
|
|
35
64
|
|
|
36
65
|
raise TypeError, "#{self} can't be multiplied by #{multiplicand}"
|
|
37
66
|
end
|
|
38
67
|
|
|
68
|
+
# Performs division of the monetary value by a scalar Numeric or identical currency {Money}.
|
|
69
|
+
#
|
|
70
|
+
# @param divisor [Numeric, Money] the divisor
|
|
71
|
+
# @return [Money, Numeric] a new Money (scalar division) or a numeric ratio (Money division)
|
|
72
|
+
# @raise [TypeError] if divisor is of incompatible type or different currency
|
|
73
|
+
# @raise [ZeroDivisionError] if division by zero is attempted
|
|
39
74
|
def /(divisor)
|
|
40
75
|
return mint(amount / divisor) if divisor.is_a?(Numeric)
|
|
41
76
|
return amount / divisor.amount if same_currency? divisor
|
|
@@ -1,40 +1,50 @@
|
|
|
1
1
|
module Mint
|
|
2
|
-
# :nodoc
|
|
3
|
-
# Coercion logic
|
|
4
2
|
class Money
|
|
3
|
+
# Implements the standard Ruby coercion protocol.
|
|
4
|
+
# Allows {Money} to interact seamlessly as the right-hand operand in Numeric arithmetic.
|
|
5
|
+
#
|
|
6
|
+
# @param other [Numeric] the left-hand operand to coerce
|
|
7
|
+
# @return [Array(CoercedNumber, Money)] coerced operand array
|
|
5
8
|
def coerce(other)
|
|
6
9
|
[CoercedNumber.new(other), self]
|
|
7
10
|
end
|
|
8
11
|
|
|
9
|
-
#
|
|
12
|
+
# @private
|
|
10
13
|
# Coerced Number contains the arithmetic logic for numeric compatible ops.
|
|
14
|
+
# @private
|
|
11
15
|
class CoercedNumber
|
|
12
16
|
include Comparable
|
|
13
17
|
|
|
18
|
+
# @private
|
|
14
19
|
def initialize(value)
|
|
15
20
|
@value = value
|
|
16
21
|
end
|
|
17
22
|
|
|
23
|
+
# @private
|
|
18
24
|
def +(other)
|
|
19
25
|
return other if @value.zero?
|
|
20
26
|
|
|
21
27
|
raise_coercion_error(:+, other)
|
|
22
28
|
end
|
|
23
29
|
|
|
30
|
+
# @private
|
|
24
31
|
def -(other)
|
|
25
32
|
return -other if @value.zero?
|
|
26
33
|
|
|
27
34
|
raise_coercion_error(:-, other)
|
|
28
35
|
end
|
|
29
36
|
|
|
37
|
+
# @private
|
|
30
38
|
def *(other)
|
|
31
39
|
other.mint(@value * other.amount)
|
|
32
40
|
end
|
|
33
41
|
|
|
42
|
+
# @private
|
|
34
43
|
def /(other)
|
|
35
44
|
raise_coercion_error(:/, other)
|
|
36
45
|
end
|
|
37
46
|
|
|
47
|
+
# @private
|
|
38
48
|
def <=>(other)
|
|
39
49
|
return nil if @value.nil? || other.nil?
|
|
40
50
|
return @value <=> other.amount if @value.zero? || other.zero?
|
|
@@ -42,6 +52,7 @@ module Mint
|
|
|
42
52
|
raise_coercion_error(:<=>, other)
|
|
43
53
|
end
|
|
44
54
|
|
|
55
|
+
# @private
|
|
45
56
|
def raise_coercion_error(operation, operand)
|
|
46
57
|
raise TypeError,
|
|
47
58
|
"#{self} #{operation} #{operand} : incompatible operands"
|
|
@@ -7,18 +7,23 @@ module Mint
|
|
|
7
7
|
# @return true if both are zero, or both have same amount and same currency
|
|
8
8
|
def ==(other)
|
|
9
9
|
return true if zero? && other.respond_to?(:zero?) && other.zero?
|
|
10
|
-
return false unless other.is_a?(Mint::Money)
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
eql?(other)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def eql?(other)
|
|
15
|
+
other.is_a?(Mint::Money) &&
|
|
16
|
+
amount == other.amount &&
|
|
17
|
+
currency == other.currency
|
|
13
18
|
end
|
|
14
19
|
|
|
15
20
|
# @example
|
|
16
|
-
# two_usd == Mint.money(2r, 'USD'
|
|
17
|
-
# two_usd > 0
|
|
18
|
-
# two_usd > Mint.money(2, 'USD'
|
|
21
|
+
# two_usd == Mint.money(2r, 'USD') #=> [$ 2.00]
|
|
22
|
+
# two_usd > 0 #=> true
|
|
23
|
+
# two_usd > Mint.money(2, 'USD') #=> false
|
|
19
24
|
# two_usd > 1
|
|
20
25
|
# => TypeError: [$ 2.00] can't be compared to 1
|
|
21
|
-
# two_usd > Mint.money(2, 'BRL'
|
|
26
|
+
# two_usd > Mint.money(2, 'BRL')
|
|
22
27
|
# => TypeError: [$ 2.00] can't be compared to [R$ 2.00]
|
|
23
28
|
#
|
|
24
29
|
def <=>(other)
|
|
@@ -31,16 +36,8 @@ module Mint
|
|
|
31
36
|
raise TypeError, "#{inspect} can't be compared to #{other.inspect}"
|
|
32
37
|
end
|
|
33
38
|
|
|
34
|
-
def
|
|
35
|
-
self == other
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def nonzero?
|
|
39
|
-
amount.nonzero?
|
|
40
|
-
end
|
|
39
|
+
def nonzero? = amount.nonzero?
|
|
41
40
|
|
|
42
|
-
def zero?
|
|
43
|
-
amount.zero?
|
|
44
|
-
end
|
|
41
|
+
def zero? = amount.zero?
|
|
45
42
|
end
|
|
46
43
|
end
|
|
@@ -1,23 +1,48 @@
|
|
|
1
|
+
require 'erb'
|
|
2
|
+
|
|
1
3
|
module Mint
|
|
2
|
-
# Conversion logic
|
|
4
|
+
# Conversion and serialization logic for {Money} instances.
|
|
3
5
|
class Money
|
|
6
|
+
# Converts the monetary amount to a {BigDecimal} object.
|
|
7
|
+
#
|
|
8
|
+
# @return [BigDecimal] the decimal representation of the money amount
|
|
9
|
+
# @raise [NoMethodError] if the bigdecimal gem is not loaded/available
|
|
4
10
|
def to_d
|
|
11
|
+
raise NoMethodError, 'decimal gem required' unless defined?(BigDecimal)
|
|
12
|
+
|
|
5
13
|
amount.to_d 0
|
|
6
14
|
end
|
|
7
15
|
|
|
16
|
+
# Converts the monetary amount to a standard float.
|
|
17
|
+
# Note: Using float conversion loses precision guarantees.
|
|
18
|
+
#
|
|
19
|
+
# @return [Float] the floating-point representation of the money amount
|
|
8
20
|
def to_f
|
|
9
21
|
amount.to_f
|
|
10
22
|
end
|
|
11
23
|
|
|
24
|
+
# Renders a safe HTML5 `<data>` element containing the formatted currency.
|
|
25
|
+
# Embeds the ISO currency description and raw value as the metadata `title` attribute.
|
|
26
|
+
#
|
|
27
|
+
# @param format [String] the display format to apply to the visible HTML text
|
|
28
|
+
# @return [String] HTML5 `<data>` representation
|
|
12
29
|
def to_html(format = DEFAULT_FORMAT)
|
|
13
30
|
title = Kernel.format("#{currency_code} %0.#{currency.subunit}f", amount)
|
|
14
|
-
|
|
31
|
+
body = to_s(format: format)
|
|
32
|
+
%(<data class='money' title='#{ERB::Util.html_escape(title)}'>#{ERB::Util.html_escape(body)}</data>)
|
|
15
33
|
end
|
|
16
34
|
|
|
35
|
+
# Truncates and converts the monetary amount to an Integer.
|
|
36
|
+
#
|
|
37
|
+
# @return [Integer] the integer representation of the money amount
|
|
17
38
|
def to_i
|
|
18
39
|
amount.to_i
|
|
19
40
|
end
|
|
20
41
|
|
|
42
|
+
# Serializes the money instance to a standard JSON object containing the amount and currency.
|
|
43
|
+
# Highly optimized to run without external dependencies.
|
|
44
|
+
#
|
|
45
|
+
# @return [String] the JSON serialized string representation
|
|
21
46
|
def to_json(*_args)
|
|
22
47
|
subunit = currency.subunit
|
|
23
48
|
Kernel.format(
|
|
@@ -26,6 +51,9 @@ module Mint
|
|
|
26
51
|
)
|
|
27
52
|
end
|
|
28
53
|
|
|
54
|
+
# Returns the exact internal Rational representation of the monetary amount.
|
|
55
|
+
#
|
|
56
|
+
# @return [Rational] the rational representation of the money amount
|
|
29
57
|
def to_r
|
|
30
58
|
amount
|
|
31
59
|
end
|
|
@@ -10,9 +10,9 @@ module Mint
|
|
|
10
10
|
#
|
|
11
11
|
# @example Basic formatting
|
|
12
12
|
# money = Mint.money(1234.56, 'USD')
|
|
13
|
-
# money.to_s
|
|
14
|
-
# money.to_s(thousand: ',')
|
|
15
|
-
# money.to_s(decimal: ',')
|
|
13
|
+
# money.to_s #=> "$1,234.56"
|
|
14
|
+
# money.to_s(thousand: '.', decimal: ',') #=> "$1.234,56"
|
|
15
|
+
# money.to_s(decimal: ',', thousand: '') #=> "$1234,56"
|
|
16
16
|
#
|
|
17
17
|
# @example Custom formats
|
|
18
18
|
# money.to_s(format: '%<amount>f') #=> "1234.56"
|
|
@@ -41,8 +41,9 @@ module Mint
|
|
|
41
41
|
formatted
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
private
|
|
45
|
+
|
|
44
46
|
def format_amount(format)
|
|
45
|
-
# binding.irb if format.is_a? Hash
|
|
46
47
|
format = { positive: format } if format.is_a?(String)
|
|
47
48
|
value = amount
|
|
48
49
|
|
data/lib/minting/money/money.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
module Mint
|
|
2
2
|
class Money
|
|
3
|
+
# The default display format pattern for formatting monetary values.
|
|
4
|
+
# Uses `%<symbol>s` for the currency symbol and `%<amount>f` for the rounded amount.
|
|
3
5
|
DEFAULT_FORMAT = '%<symbol>s%<amount>f'.freeze
|
|
4
6
|
|
|
5
7
|
attr_reader :amount, :currency
|
|
@@ -10,37 +12,44 @@ module Mint
|
|
|
10
12
|
# @raise [ArgumentError] If amount is not numeric or currency is invalid
|
|
11
13
|
def initialize(amount, currency)
|
|
12
14
|
raise ArgumentError, 'amount must be Numeric' unless amount.is_a?(Numeric)
|
|
13
|
-
|
|
14
|
-
unless currency.is_a?(Currency)
|
|
15
|
-
raise ArgumentError,
|
|
16
|
-
'currency must be a Currency object'
|
|
17
|
-
end
|
|
15
|
+
raise ArgumentError, 'currency must be a Currency object' unless currency.is_a?(Currency)
|
|
18
16
|
|
|
19
17
|
@amount = amount.to_r.round(currency.subunit)
|
|
20
18
|
@currency = currency
|
|
19
|
+
freeze
|
|
21
20
|
end
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
# Returns the ISO 3-letter currency code string.
|
|
23
|
+
#
|
|
24
|
+
# @return [String] the ISO currency code
|
|
25
|
+
def currency_code = currency.code
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
# Generates a stable hash key for Money instances.
|
|
28
|
+
#
|
|
29
|
+
# @return [Integer] the calculated hash value
|
|
30
|
+
def hash = [amount, currency_code].hash
|
|
30
31
|
|
|
31
32
|
# Returns a new Money object with the specified amount, or self if unchanged
|
|
32
33
|
# @param new_amount [Numeric] The new amount
|
|
33
34
|
# @return [Money] A new Money object or self
|
|
34
35
|
def mint(new_amount)
|
|
35
|
-
new_amount.to_r == amount ? self : Money.new(new_amount, currency)
|
|
36
|
+
new_amount.to_r == amount ? self : Money.new(new_amount, currency)
|
|
36
37
|
end
|
|
37
38
|
|
|
39
|
+
# Returns a standard developer-oriented string inspection of the Money object.
|
|
40
|
+
#
|
|
41
|
+
# @return [String] the formatted inspect representation
|
|
38
42
|
def inspect
|
|
39
43
|
Kernel.format "[#{currency_code} %0.#{currency.subunit}f]", amount
|
|
40
44
|
end
|
|
41
45
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
# Helper method to verify if another object has the identical currency.
|
|
47
|
+
#
|
|
48
|
+
# @param other [Object] the target object to compare
|
|
49
|
+
# @return [Boolean] true if currencies match, false otherwise
|
|
50
|
+
def same_currency?(other) = other.respond_to?(:currency) && other.currency == currency
|
|
51
|
+
|
|
52
|
+
# Returns default zero no currency money
|
|
53
|
+
def self.zero = @zero ||= new(0, Mint.currencies('XXX'))
|
|
45
54
|
end
|
|
46
55
|
end
|