money 6.9.0 → 6.16.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +131 -3
- data/LICENSE +17 -17
- data/README.md +181 -71
- data/config/currency_backwards_compatible.json +65 -0
- data/config/currency_iso.json +119 -56
- data/config/currency_non_iso.json +35 -2
- data/lib/money/bank/variable_exchange.rb +22 -12
- data/lib/money/currency/loader.rb +15 -13
- data/lib/money/currency.rb +38 -39
- data/lib/money/locale_backend/base.rb +7 -0
- data/lib/money/locale_backend/currency.rb +11 -0
- data/lib/money/locale_backend/errors.rb +6 -0
- data/lib/money/locale_backend/i18n.rb +25 -0
- data/lib/money/locale_backend/legacy.rb +28 -0
- data/lib/money/money/allocation.rb +46 -0
- data/lib/money/money/arithmetic.rb +33 -15
- data/lib/money/money/constructors.rb +1 -2
- data/lib/money/money/formatter.rb +399 -0
- data/lib/money/money/formatting_rules.rb +142 -0
- data/lib/money/money/locale_backend.rb +22 -0
- data/lib/money/money.rb +235 -187
- data/lib/money/rates_store/memory.rb +24 -24
- data/lib/money/version.rb +1 -1
- data/money.gemspec +14 -8
- metadata +36 -56
- data/.coveralls.yml +0 -1
- data/.gitignore +0 -23
- data/.rspec +0 -1
- data/.travis.yml +0 -26
- data/AUTHORS +0 -126
- data/CONTRIBUTING.md +0 -17
- data/Gemfile +0 -16
- data/Rakefile +0 -17
- data/lib/money/money/formatting.rb +0 -426
- data/spec/bank/base_spec.rb +0 -79
- data/spec/bank/single_currency_spec.rb +0 -13
- data/spec/bank/variable_exchange_spec.rb +0 -265
- data/spec/currency/heuristics_spec.rb +0 -11
- data/spec/currency/loader_spec.rb +0 -19
- data/spec/currency_spec.rb +0 -359
- data/spec/money/arithmetic_spec.rb +0 -693
- data/spec/money/constructors_spec.rb +0 -103
- data/spec/money/formatting_spec.rb +0 -757
- data/spec/money_spec.rb +0 -778
- data/spec/rates_store/memory_spec.rb +0 -69
- 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": "
|
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
|
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 = {:
|
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.
|
113
|
-
exchange(fractional, rate, &block),
|
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
|
123
|
-
BigDecimal
|
124
|
-
BigDecimal
|
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
|
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
|
-
|
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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
16
|
+
private
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
data/lib/money/currency.rb
CHANGED
@@ -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
|
-
|
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.
|
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 =
|
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 =
|
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
|
408
|
+
# Returns if a code currency is ISO.
|
396
409
|
#
|
397
|
-
#
|
398
|
-
# @see https://en.wikipedia.org/wiki/ISO_4217#Active_codes
|
410
|
+
# @return [Boolean]
|
399
411
|
#
|
400
|
-
# @
|
401
|
-
|
402
|
-
|
403
|
-
|
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
|
-
#
|
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
|
414
|
-
|
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,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
|
-
|
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]
|
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]
|
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]
|
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
|
-
|
127
|
-
|
128
|
-
|
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
|
-
|
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
|
-
|
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,
|
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
|
-
[
|
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 :
|
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
|
-
|
295
|
+
dup_with(fractional: fractional.abs)
|
278
296
|
end
|
279
297
|
|
280
298
|
# Test if the money amount is zero.
|