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.
@@ -1,13 +1,24 @@
1
- # :nodoc
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).freeze if currency
12
+ return Money.new(amount, currency) if currency
6
13
 
7
- available = currencies.keys.join(', ')
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
- def self.register_currency(code, subunit: 2, symbol: '$')
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, subunit: subunit, symbol: symbol)
43
+ currencies[code] || register_currency!(code:, subunit:, symbol:, priority:)
25
44
  end
26
45
 
27
- def self.register_currency!(code, subunit:, symbol: '')
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
- Currency.new(code, subunit: subunit.to_i, symbol: symbol.to_s).freeze
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
- # Major Global Currencies
45
- 'USD' => Currency.new('USD', subunit: 2, symbol: '$'),
46
- 'EUR' => Currency.new('EUR', subunit: 2, symbol: '€'),
47
- 'GBP' => Currency.new('GBP', subunit: 2, symbol: '£'),
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
- # Africa
114
- 'ZAR' => Currency.new('ZAR', subunit: 2, symbol: 'R'),
115
- 'EGP' => Currency.new('EGP', subunit: 2, symbol: '£'),
116
- 'NGN' => Currency.new('NGN', subunit: 2, symbol: '₦'),
117
- 'KES' => Currency.new('KES', subunit: 2, symbol: 'KSh'),
118
- 'GHS' => Currency.new('GHS', subunit: 2, symbol: '¢'),
119
- 'UGX' => Currency.new('UGX', subunit: 0, symbol: 'USh'),
120
- 'TZS' => Currency.new('TZS', subunit: 2, symbol: 'TSh'),
121
- 'ETB' => Currency.new('ETB', subunit: 2, symbol: 'Br'),
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
- # Americas
138
- 'MXN' => Currency.new('MXN', subunit: 2, symbol: '$'),
139
- 'BRL' => Currency.new('BRL', subunit: 2, symbol: 'R$'),
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
- # Pacific & Others
164
- 'FJD' => Currency.new('FJD', subunit: 2, symbol: 'FJ$'),
165
- 'PGK' => Currency.new('PGK', subunit: 2, symbol: 'K'),
166
- 'SBD' => Currency.new('SBD', subunit: 2, symbol: 'SI$'),
167
- 'VUV' => Currency.new('VUV', subunit: 0, symbol: 'VT'),
168
- 'TOP' => Currency.new('TOP', subunit: 2, symbol: 'T$'),
169
- 'WST' => Currency.new('WST', subunit: 2, symbol: 'WS$'),
170
- 'XCD' => Currency.new('XCD', subunit: 2, symbol: 'EC$'),
171
- 'XOF' => Currency.new('XOF', subunit: 0, symbol: 'CFA'),
172
- 'XAF' => Currency.new('XAF', subunit: 0, symbol: 'FCFA'),
173
- 'XPF' => Currency.new('XPF', subunit: 0, symbol: '₣')
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
- whole = proportions.sum.to_r
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(it) }
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
- # :nodoc
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
- amount == other.amount && currency == other.currency
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']) #=> [$ 2.00]
17
- # two_usd > 0 #=> true
18
- # two_usd > Mint.money(2, 'USD']) #=> true
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 eql?(other)
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
- %(<data class='money' title='#{title}'>#{to_s(format: format)}</data>)
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 #=> "$1234.56"
14
- # money.to_s(thousand: ',') #=> "$1,234.56"
15
- # money.to_s(decimal: ',') #=> "$1234,56"
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
 
@@ -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
- def currency_code
24
- currency.code
25
- end
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
- def hash
28
- zero? ? 0.hash : [amount, currency.code].hash
29
- end
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).freeze
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
- def same_currency?(other)
43
- other.respond_to?(:currency) && other.currency == currency
44
- end
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