money 6.9.0 → 6.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +131 -3
  3. data/LICENSE +17 -17
  4. data/README.md +181 -71
  5. data/config/currency_backwards_compatible.json +65 -0
  6. data/config/currency_iso.json +119 -56
  7. data/config/currency_non_iso.json +35 -2
  8. data/lib/money/bank/variable_exchange.rb +22 -12
  9. data/lib/money/currency/loader.rb +15 -13
  10. data/lib/money/currency.rb +38 -39
  11. data/lib/money/locale_backend/base.rb +7 -0
  12. data/lib/money/locale_backend/currency.rb +11 -0
  13. data/lib/money/locale_backend/errors.rb +6 -0
  14. data/lib/money/locale_backend/i18n.rb +25 -0
  15. data/lib/money/locale_backend/legacy.rb +28 -0
  16. data/lib/money/money/allocation.rb +46 -0
  17. data/lib/money/money/arithmetic.rb +33 -15
  18. data/lib/money/money/constructors.rb +1 -2
  19. data/lib/money/money/formatter.rb +399 -0
  20. data/lib/money/money/formatting_rules.rb +142 -0
  21. data/lib/money/money/locale_backend.rb +22 -0
  22. data/lib/money/money.rb +235 -187
  23. data/lib/money/rates_store/memory.rb +24 -24
  24. data/lib/money/version.rb +1 -1
  25. data/money.gemspec +14 -8
  26. metadata +36 -56
  27. data/.coveralls.yml +0 -1
  28. data/.gitignore +0 -23
  29. data/.rspec +0 -1
  30. data/.travis.yml +0 -26
  31. data/AUTHORS +0 -126
  32. data/CONTRIBUTING.md +0 -17
  33. data/Gemfile +0 -16
  34. data/Rakefile +0 -17
  35. data/lib/money/money/formatting.rb +0 -426
  36. data/spec/bank/base_spec.rb +0 -79
  37. data/spec/bank/single_currency_spec.rb +0 -13
  38. data/spec/bank/variable_exchange_spec.rb +0 -265
  39. data/spec/currency/heuristics_spec.rb +0 -11
  40. data/spec/currency/loader_spec.rb +0 -19
  41. data/spec/currency_spec.rb +0 -359
  42. data/spec/money/arithmetic_spec.rb +0 -693
  43. data/spec/money/constructors_spec.rb +0 -103
  44. data/spec/money/formatting_spec.rb +0 -757
  45. data/spec/money_spec.rb +0 -778
  46. data/spec/rates_store/memory_spec.rb +0 -69
  47. data/spec/spec_helper.rb +0 -28
@@ -1,14 +1,31 @@
1
1
  {
2
+ "bch": {
3
+ "priority": 100,
4
+ "iso_code": "BCH",
5
+ "name": "Bitcoin Cash",
6
+ "symbol": "₿",
7
+ "disambiguate_symbol": "₿CH",
8
+ "alternate_symbols": ["BCH"],
9
+ "subunit": "Satoshi",
10
+ "subunit_to_unit": 100000000,
11
+ "symbol_first": false,
12
+ "format": "%n %u",
13
+ "html_entity": "₿",
14
+ "decimal_mark": ".",
15
+ "thousands_separator": ",",
16
+ "iso_numeric": "",
17
+ "smallest_denomination": 1
18
+ },
2
19
  "btc": {
3
20
  "priority": 100,
4
21
  "iso_code": "BTC",
5
22
  "name": "Bitcoin",
6
- "symbol": "B⃦",
23
+ "symbol": "",
7
24
  "alternate_symbols": [],
8
25
  "subunit": "Satoshi",
9
26
  "subunit_to_unit": 100000000,
10
27
  "symbol_first": true,
11
- "html_entity": "",
28
+ "html_entity": "₿",
12
29
  "decimal_mark": ".",
13
30
  "thousands_separator": ",",
14
31
  "iso_numeric": "",
@@ -93,5 +110,21 @@
93
110
  "thousands_separator": ",",
94
111
  "iso_numeric": "",
95
112
  "smallest_denomination": 1
113
+ },
114
+ "cnh": {
115
+ "priority": 100,
116
+ "iso_code": "CNH",
117
+ "name": "Chinese Renminbi Yuan Offshore",
118
+ "symbol": "¥",
119
+ "disambiguate_symbol": "CNH",
120
+ "alternate_symbols": ["CN¥", "元", "CN元"],
121
+ "subunit": "Fen",
122
+ "subunit_to_unit": 100,
123
+ "symbol_first": true,
124
+ "html_entity": "¥",
125
+ "decimal_mark": ".",
126
+ "thousands_separator": ",",
127
+ "iso_numeric": "",
128
+ "smallest_denomination": 1
96
129
  }
97
130
  }
@@ -42,12 +42,12 @@ class Money
42
42
  # bank.get_rate 'USD', 'CAD'
43
43
  class VariableExchange < Base
44
44
 
45
- attr_reader :mutex, :store
45
+ attr_reader :mutex
46
46
 
47
47
  # Available formats for importing/exporting rates.
48
48
  RATE_FORMATS = [:json, :ruby, :yaml].freeze
49
49
  SERIALIZER_SEPARATOR = '_TO_'.freeze
50
- FORMAT_SERIALIZERS = {:json => JSON, :ruby => Marshal, :yaml => YAML}.freeze
50
+ FORMAT_SERIALIZERS = {json: JSON, ruby: Marshal, yaml: YAML}.freeze
51
51
 
52
52
  # Initializes a new +Money::Bank::VariableExchange+ object.
53
53
  # It defaults to using an in-memory, thread safe store instance for
@@ -61,6 +61,10 @@ class Money
61
61
  super(&block)
62
62
  end
63
63
 
64
+ def store
65
+ @store.is_a?(String) ? Object.const_get(@store) : @store
66
+ end
67
+
64
68
  def marshal_dump
65
69
  [store.marshal_dump, @rounding_method]
66
70
  end
@@ -109,8 +113,10 @@ class Money
109
113
  else
110
114
  if rate = get_rate(from.currency, to_currency)
111
115
  fractional = calculate_fractional(from, to_currency)
112
- from.class.new(
113
- exchange(fractional, rate, &block), to_currency
116
+ from.dup_with(
117
+ fractional: exchange(fractional, rate, &block),
118
+ currency: to_currency,
119
+ bank: self
114
120
  )
115
121
  else
116
122
  raise UnknownRate, "No conversion rate known for '#{from.currency.iso_code}' -> '#{to_currency}'"
@@ -119,14 +125,14 @@ class Money
119
125
  end
120
126
 
121
127
  def calculate_fractional(from, to_currency)
122
- BigDecimal.new(from.fractional.to_s) / (
123
- BigDecimal.new(from.currency.subunit_to_unit.to_s) /
124
- BigDecimal.new(to_currency.subunit_to_unit.to_s)
128
+ BigDecimal(from.fractional.to_s) / (
129
+ BigDecimal(from.currency.subunit_to_unit.to_s) /
130
+ BigDecimal(to_currency.subunit_to_unit.to_s)
125
131
  )
126
132
  end
127
133
 
128
134
  def exchange(fractional, rate, &block)
129
- ex = fractional * BigDecimal.new(rate.to_s)
135
+ ex = fractional * BigDecimal(rate.to_s)
130
136
  if block_given?
131
137
  yield ex
132
138
  elsif @rounding_method
@@ -213,8 +219,7 @@ class Money
213
219
  # s = bank.export_rates(:json)
214
220
  # s #=> "{\"USD_TO_CAD\":1.24515,\"CAD_TO_USD\":0.803115}"
215
221
  def export_rates(format, file = nil, opts = {})
216
- raise Money::Bank::UnknownRateFormat unless
217
- RATE_FORMATS.include? format
222
+ raise Money::Bank::UnknownRateFormat unless RATE_FORMATS.include?(format)
218
223
 
219
224
  store.transaction do
220
225
  s = FORMAT_SERIALIZERS[format].dump(rates)
@@ -254,8 +259,13 @@ class Money
254
259
  # bank.get_rate("USD", "CAD") #=> 1.24515
255
260
  # bank.get_rate("CAD", "USD") #=> 0.803115
256
261
  def import_rates(format, s, opts = {})
257
- raise Money::Bank::UnknownRateFormat unless
258
- RATE_FORMATS.include? format
262
+ raise Money::Bank::UnknownRateFormat unless RATE_FORMATS.include?(format)
263
+
264
+ if format == :ruby
265
+ warn '[WARNING] Using :ruby format when importing rates is potentially unsafe and ' \
266
+ 'might lead to remote code execution via Marshal.load deserializer. Consider using ' \
267
+ 'safe alternatives such as :json and :yaml.'
268
+ end
259
269
 
260
270
  store.transaction do
261
271
  data = FORMAT_SERIALIZERS[format].load(s)
@@ -3,21 +3,23 @@ class Money
3
3
  module Loader
4
4
  DATA_PATH = File.expand_path("../../../../config", __FILE__)
5
5
 
6
- # Loads and returns the currencies stored in JSON files in the config directory.
7
- #
8
- # @return [Hash]
9
- def load_currencies
10
- currencies = parse_currency_file("currency_iso.json")
11
- currencies.merge! parse_currency_file("currency_non_iso.json")
12
- currencies.merge! parse_currency_file("currency_backwards_compatible.json")
13
- end
6
+ class << self
7
+ # Loads and returns the currencies stored in JSON files in the config directory.
8
+ #
9
+ # @return [Hash]
10
+ def load_currencies
11
+ currencies = parse_currency_file("currency_iso.json")
12
+ currencies.merge! parse_currency_file("currency_non_iso.json")
13
+ currencies.merge! parse_currency_file("currency_backwards_compatible.json")
14
+ end
14
15
 
15
- private
16
+ private
16
17
 
17
- def parse_currency_file(filename)
18
- json = File.read("#{DATA_PATH}/#{filename}")
19
- json.force_encoding(::Encoding::UTF_8) if defined?(::Encoding)
20
- JSON.parse(json, :symbolize_names => true)
18
+ def parse_currency_file(filename)
19
+ json = File.read("#{DATA_PATH}/#{filename}")
20
+ json.force_encoding(::Encoding::UTF_8) if defined?(::Encoding)
21
+ JSON.parse(json, symbolize_names: true)
22
+ end
21
23
  end
22
24
  end
23
25
  end
@@ -13,7 +13,6 @@ class Money
13
13
  class Currency
14
14
  include Comparable
15
15
  extend Enumerable
16
- extend Money::Currency::Loader
17
16
  extend Money::Currency::Heuristics
18
17
 
19
18
  # Keeping cached instances in sync between threads
@@ -76,10 +75,12 @@ class Money
76
75
  #
77
76
  # @example
78
77
  # Money::Currency.find_by_iso_numeric(978) #=> #<Money::Currency id: eur ...>
78
+ # Money::Currency.find_by_iso_numeric(51) #=> #<Money::Currency id: amd ...>
79
79
  # Money::Currency.find_by_iso_numeric('001') #=> nil
80
80
  def find_by_iso_numeric(num)
81
- num = num.to_s
82
- id, _ = self.table.find{|key, currency| currency[:iso_numeric] == num}
81
+ num = num.to_s.rjust(3, '0')
82
+ return if num.empty?
83
+ id, _ = self.table.find { |key, currency| currency[:iso_numeric] == num }
83
84
  new(id)
84
85
  rescue UnknownCurrency
85
86
  nil
@@ -120,14 +121,14 @@ class Money
120
121
  # See https://en.wikipedia.org/wiki/List_of_circulating_currencies and
121
122
  # http://search.cpan.org/~tnguyen/Locale-Currency-Format-1.28/Format.pm
122
123
  def table
123
- @table ||= load_currencies
124
+ @table ||= Loader.load_currencies
124
125
  end
125
126
 
126
127
  # List the currencies imported and registered
127
128
  # @return [Array]
128
129
  #
129
130
  # @example
130
- # Money::Currency.iso_codes()
131
+ # Money::Currency.all()
131
132
  # [#<Currency ..USD>, 'CAD', 'EUR']...
132
133
  def all
133
134
  table.keys.map do |curr|
@@ -170,9 +171,18 @@ class Money
170
171
  key = curr.fetch(:iso_code).downcase.to_sym
171
172
  @@mutex.synchronize { _instances.delete(key.to_s) }
172
173
  @table[key] = curr
173
- @stringified_keys = stringify_keys
174
+ @stringified_keys = nil
174
175
  end
175
176
 
177
+ # Inherit a new currency from existing one
178
+ #
179
+ # @param parent_iso_code [String] the international 3-letter code as defined
180
+ # @param curr [Hash] See {register} method for hash structure
181
+ def inherit(parent_iso_code, curr)
182
+ parent_iso_code = parent_iso_code.downcase.to_sym
183
+ curr = @table.fetch(parent_iso_code, {}).merge(curr)
184
+ register(curr)
185
+ end
176
186
 
177
187
  # Unregister a currency.
178
188
  #
@@ -188,15 +198,18 @@ class Money
188
198
  key = curr.downcase.to_sym
189
199
  end
190
200
  existed = @table.delete(key)
191
- @stringified_keys = stringify_keys if existed
201
+ @stringified_keys = nil if existed
192
202
  existed ? true : false
193
203
  end
194
204
 
195
-
196
205
  def each
197
206
  all.each { |c| yield(c) }
198
207
  end
199
208
 
209
+ def reset!
210
+ @@instances = {}
211
+ @table = Loader.load_currencies
212
+ end
200
213
 
201
214
  private
202
215
 
@@ -244,7 +257,7 @@ class Money
244
257
 
245
258
  attr_reader :id, :priority, :iso_code, :iso_numeric, :name, :symbol,
246
259
  :disambiguate_symbol, :html_entity, :subunit, :subunit_to_unit, :decimal_mark,
247
- :thousands_separator, :symbol_first, :smallest_denomination
260
+ :thousands_separator, :symbol_first, :smallest_denomination, :format
248
261
 
249
262
  alias_method :separator, :decimal_mark
250
263
  alias_method :delimiter, :thousands_separator
@@ -332,7 +345,7 @@ class Money
332
345
  # @example
333
346
  # Money::Currency.new(:usd) #=> #<Currency id: usd ...>
334
347
  def inspect
335
- "#<#{self.class.name} id: #{id}, priority: #{priority}, symbol_first: #{symbol_first}, thousands_separator: #{thousands_separator}, html_entity: #{html_entity}, decimal_mark: #{decimal_mark}, name: #{name}, symbol: #{symbol}, subunit_to_unit: #{subunit_to_unit}, exponent: #{exponent}, iso_code: #{iso_code}, iso_numeric: #{iso_numeric}, subunit: #{subunit}, smallest_denomination: #{smallest_denomination}>"
348
+ "#<#{self.class.name} id: #{id}, priority: #{priority}, symbol_first: #{symbol_first}, thousands_separator: #{thousands_separator}, html_entity: #{html_entity}, decimal_mark: #{decimal_mark}, name: #{name}, symbol: #{symbol}, subunit_to_unit: #{subunit_to_unit}, exponent: #{exponent}, iso_code: #{iso_code}, iso_numeric: #{iso_numeric}, subunit: #{subunit}, smallest_denomination: #{smallest_denomination}, format: #{format}>"
336
349
  end
337
350
 
338
351
  # Returns a string representation corresponding to the upcase +id+
@@ -392,45 +405,30 @@ class Money
392
405
  !!@symbol_first
393
406
  end
394
407
 
395
- # Returns the relation between subunit and unit as a base 10 exponent.
408
+ # Returns if a code currency is ISO.
396
409
  #
397
- # Note that MGA and MRO are exceptions and are rounded to 1
398
- # @see https://en.wikipedia.org/wiki/ISO_4217#Active_codes
410
+ # @return [Boolean]
399
411
  #
400
- # @return [Integer]
401
- def exponent
402
- Math.log10(@subunit_to_unit).round
403
- end
404
-
405
- # Cache decimal places for subunit_to_unit values. Common ones pre-cached.
406
- def self.decimal_places_cache
407
- @decimal_places_cache ||= {1 => 0, 10 => 1, 100 => 2, 1000 => 3}
412
+ # @example
413
+ # Money::Currency.new(:usd).iso?
414
+ #
415
+ def iso?
416
+ iso_numeric && iso_numeric != ''
408
417
  end
409
418
 
410
- # The number of decimal places needed.
419
+ # Returns the relation between subunit and unit as a base 10 exponent.
420
+ #
421
+ # Note that MGA and MRU are exceptions and are rounded to 1
422
+ # @see https://en.wikipedia.org/wiki/ISO_4217#Active_codes
411
423
  #
412
424
  # @return [Integer]
413
- def decimal_places
414
- cache[subunit_to_unit] ||= calculate_decimal_places(subunit_to_unit)
425
+ def exponent
426
+ Math.log10(subunit_to_unit).round
415
427
  end
428
+ alias decimal_places exponent
416
429
 
417
430
  private
418
431
 
419
- def cache
420
- self.class.decimal_places_cache
421
- end
422
-
423
- # If we need to figure out how many decimal places we need we
424
- # use repeated integer division.
425
- def calculate_decimal_places(num)
426
- i = 1
427
- while num >= 10
428
- num /= 10
429
- i += 1 if num >= 10
430
- end
431
- i
432
- end
433
-
434
432
  def initialize_data!
435
433
  data = self.class.table[@id]
436
434
  @alternate_symbols = data[:alternate_symbols]
@@ -447,6 +445,7 @@ class Money
447
445
  @symbol = data[:symbol]
448
446
  @symbol_first = data[:symbol_first]
449
447
  @thousands_separator = data[:thousands_separator]
448
+ @format = data[:format]
450
449
  end
451
450
  end
452
451
  end
@@ -0,0 +1,7 @@
1
+ require 'money/locale_backend/errors'
2
+
3
+ class Money
4
+ module LocaleBackend
5
+ class Base; end
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ require 'money/locale_backend/base'
2
+
3
+ class Money
4
+ module LocaleBackend
5
+ class Currency < Base
6
+ def lookup(key, currency)
7
+ currency.public_send(key) if currency.respond_to?(key)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ class Money
2
+ module LocaleBackend
3
+ class NotSupported < StandardError; end
4
+ class Unknown < ArgumentError; end
5
+ end
6
+ end
@@ -0,0 +1,25 @@
1
+ require 'money/locale_backend/base'
2
+
3
+ class Money
4
+ module LocaleBackend
5
+ class I18n < Base
6
+ KEY_MAP = {
7
+ thousands_separator: :delimiter,
8
+ decimal_mark: :separator,
9
+ symbol: :unit
10
+ }.freeze
11
+
12
+ def initialize
13
+ raise NotSupported, 'I18n not found' unless defined?(::I18n)
14
+ end
15
+
16
+ def lookup(key, _)
17
+ i18n_key = KEY_MAP[key]
18
+
19
+ ::I18n.t i18n_key, scope: 'number.currency.format', raise: true
20
+ rescue ::I18n::MissingTranslationData
21
+ ::I18n.t i18n_key, scope: 'number.format', default: nil
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ require 'money/locale_backend/base'
2
+ require 'money/locale_backend/i18n'
3
+
4
+ class Money
5
+ module LocaleBackend
6
+ class Legacy < Base
7
+ def initialize
8
+ raise NotSupported, 'I18n not found' if Money.use_i18n && !defined?(::I18n)
9
+ end
10
+
11
+ def lookup(key, currency)
12
+ warn '[DEPRECATION] You are using the default localization behaviour that will change in the next major release. Find out more - https://github.com/RubyMoney/money#deprecation'
13
+
14
+ if Money.use_i18n
15
+ i18n_backend.lookup(key, nil) || currency.public_send(key)
16
+ else
17
+ currency.public_send(key)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def i18n_backend
24
+ @i18n_backend ||= Money::LocaleBackend::I18n.new
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,46 @@
1
+ # encoding: utf-8
2
+
3
+ class Money
4
+ class Allocation
5
+ # Splits a given amount in parts without losing pennies.
6
+ # The left-over pennies will be distributed round-robin amongst the parts. This means that
7
+ # parts listed first will likely receive more pennies than the ones listed later.
8
+ #
9
+ # The results should always add up to the original amount.
10
+ #
11
+ # The parts can be specified as:
12
+ # Numeric — performs the split between a given number of parties evenely
13
+ # Array<Numeric> — allocates the amounts proportionally to the given array
14
+ #
15
+ def self.generate(amount, parts, whole_amounts = true)
16
+ parts = if parts.is_a?(Numeric)
17
+ Array.new(parts, 1)
18
+ elsif parts.all?(&:zero?)
19
+ Array.new(parts.count, 1)
20
+ else
21
+ parts.dup
22
+ end
23
+
24
+ raise ArgumentError, 'need at least one party' if parts.empty?
25
+
26
+ result = []
27
+ remaining_amount = amount
28
+
29
+ until parts.empty? do
30
+ parts_sum = parts.inject(0, :+)
31
+ part = parts.pop
32
+
33
+ current_split = 0
34
+ if parts_sum > 0
35
+ current_split = remaining_amount * part / parts_sum
36
+ current_split = current_split.truncate if whole_amounts
37
+ end
38
+
39
+ result.unshift current_split
40
+ remaining_amount -= current_split
41
+ end
42
+
43
+ result
44
+ end
45
+ end
46
+ end
@@ -16,7 +16,7 @@ class Money
16
16
  # @example
17
17
  # - Money.new(100) #=> #<Money @fractional=-100>
18
18
  def -@
19
- self.class.new(-fractional, currency)
19
+ dup_with(fractional: -fractional)
20
20
  end
21
21
 
22
22
  # Checks whether two Money objects have the same currency and the same
@@ -46,7 +46,7 @@ class Money
46
46
  # Compares two Money objects. If money objects have a different currency it
47
47
  # will attempt to convert the currency.
48
48
  #
49
- # @param [Money] other_money Value to compare with.
49
+ # @param [Money] other Value to compare with.
50
50
  #
51
51
  # @return [Integer]
52
52
  #
@@ -57,6 +57,12 @@ class Money
57
57
  return unless other.respond_to?(:zero?) && other.zero?
58
58
  return other.is_a?(CoercedNumeric) ? 0 <=> fractional : fractional <=> 0
59
59
  end
60
+
61
+ # Always allow comparison with zero
62
+ if zero? || other.zero?
63
+ return fractional <=> other.fractional
64
+ end
65
+
60
66
  other = other.exchange_to(currency)
61
67
  fractional <=> other.fractional
62
68
  rescue Money::Bank::UnknownRate
@@ -102,7 +108,7 @@ class Money
102
108
  # values. If +other_money+ has a different currency then its monetary value
103
109
  # is automatically exchanged to this object's currency using +exchange_to+.
104
110
  #
105
- # @param [Money] other_money Other +Money+ object to add.
111
+ # @param [Money] other Other +Money+ object to add.
106
112
  #
107
113
  # @return [Money]
108
114
  #
@@ -115,20 +121,32 @@ class Money
115
121
  # its monetary value is automatically exchanged to this object's currency
116
122
  # using +exchange_to+.
117
123
  #
118
- # @param [Money] other_money Other +Money+ object to subtract.
124
+ # @param [Money] other Other +Money+ object to subtract.
119
125
  #
120
126
  # @return [Money]
121
127
  #
122
128
  # @example
123
129
  # Money.new(100) - Money.new(99) #=> #<Money @fractional=1>
124
130
  [:+, :-].each do |op|
131
+ non_zero_message = lambda do |value|
132
+ "Can't add or subtract a non-zero #{value.class.name} value"
133
+ end
134
+
125
135
  define_method(op) do |other|
126
- unless other.is_a?(Money)
127
- return self if other.zero?
128
- raise TypeError
136
+ case other
137
+ when Money
138
+ other = other.exchange_to(currency)
139
+ new_fractional = fractional.public_send(op, other.fractional)
140
+ dup_with(fractional: new_fractional)
141
+ when CoercedNumeric
142
+ raise TypeError, non_zero_message.call(other.value) unless other.zero?
143
+ dup_with(fractional: other.value.public_send(op, fractional))
144
+ when Numeric
145
+ raise TypeError, non_zero_message.call(other) unless other.zero?
146
+ self
147
+ else
148
+ raise TypeError, "Unsupported argument type: #{other.class.name}"
129
149
  end
130
- other = other.exchange_to(currency)
131
- self.class.new(fractional.public_send(op, other.fractional), currency)
132
150
  end
133
151
  end
134
152
 
@@ -149,7 +167,7 @@ class Money
149
167
  def *(value)
150
168
  value = value.value if value.is_a?(CoercedNumeric)
151
169
  if value.is_a? Numeric
152
- self.class.new(fractional * value, currency)
170
+ dup_with(fractional: fractional * value)
153
171
  else
154
172
  raise TypeError, "Can't multiply a #{self.class.name} by a #{value.class.name}'s value"
155
173
  end
@@ -175,7 +193,7 @@ class Money
175
193
  fractional / as_d(value.exchange_to(currency).fractional).to_f
176
194
  else
177
195
  raise TypeError, 'Can not divide by Money' if value.is_a?(CoercedNumeric)
178
- self.class.new(fractional / as_d(value), currency)
196
+ dup_with(fractional: fractional / as_d(value))
179
197
  end
180
198
  end
181
199
 
@@ -213,13 +231,13 @@ class Money
213
231
  def divmod_money(val)
214
232
  cents = val.exchange_to(currency).cents
215
233
  quotient, remainder = fractional.divmod(cents)
216
- [quotient, self.class.new(remainder, currency)]
234
+ [quotient, dup_with(fractional: remainder)]
217
235
  end
218
236
  private :divmod_money
219
237
 
220
238
  def divmod_other(val)
221
239
  quotient, remainder = fractional.divmod(as_d(val))
222
- [self.class.new(quotient, currency), self.class.new(remainder, currency)]
240
+ [dup_with(fractional: quotient), dup_with(fractional: remainder)]
223
241
  end
224
242
  private :divmod_other
225
243
 
@@ -263,7 +281,7 @@ class Money
263
281
  if (fractional < 0 && val < 0) || (fractional > 0 && val > 0)
264
282
  self.modulo(val)
265
283
  else
266
- self.modulo(val) - (val.is_a?(Money) ? val : self.class.new(val, currency))
284
+ self.modulo(val) - (val.is_a?(Money) ? val : dup_with(fractional: val))
267
285
  end
268
286
  end
269
287
 
@@ -274,7 +292,7 @@ class Money
274
292
  # @example
275
293
  # Money.new(-100).abs #=> #<Money @fractional=100>
276
294
  def abs
277
- self.class.new(fractional.abs, currency)
295
+ dup_with(fractional: fractional.abs)
278
296
  end
279
297
 
280
298
  # Test if the money amount is zero.
@@ -10,8 +10,7 @@ class Money
10
10
  # @example
11
11
  # Money.empty #=> #<Money @fractional=0>
12
12
  def empty(currency = default_currency)
13
- @empty ||= {}
14
- @empty[currency] ||= new(0, currency).freeze
13
+ new(0, currency)
15
14
  end
16
15
  alias_method :zero, :empty
17
16