money 6.7.0 → 6.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +2 -1
- data/.travis.yml +22 -5
- data/AUTHORS +5 -0
- data/CHANGELOG.md +109 -3
- data/Gemfile +13 -4
- data/LICENSE +2 -0
- data/README.md +69 -49
- data/config/currency_backwards_compatible.json +30 -0
- data/config/currency_iso.json +139 -62
- data/config/currency_non_iso.json +66 -2
- data/lib/money.rb +0 -13
- data/lib/money/bank/variable_exchange.rb +9 -22
- data/lib/money/currency.rb +35 -38
- data/lib/money/currency/heuristics.rb +1 -144
- data/lib/money/currency/loader.rb +1 -1
- data/lib/money/locale_backend/base.rb +7 -0
- data/lib/money/locale_backend/errors.rb +6 -0
- data/lib/money/locale_backend/i18n.rb +24 -0
- data/lib/money/locale_backend/legacy.rb +28 -0
- data/lib/money/money.rb +120 -151
- data/lib/money/money/allocation.rb +37 -0
- data/lib/money/money/arithmetic.rb +57 -52
- data/lib/money/money/constructors.rb +1 -2
- data/lib/money/money/formatter.rb +397 -0
- data/lib/money/money/formatting_rules.rb +120 -0
- data/lib/money/money/locale_backend.rb +20 -0
- data/lib/money/rates_store/memory.rb +1 -2
- data/lib/money/version.rb +1 -1
- data/money.gemspec +10 -16
- data/spec/bank/variable_exchange_spec.rb +7 -3
- data/spec/currency/heuristics_spec.rb +2 -153
- data/spec/currency_spec.rb +45 -4
- data/spec/locale_backend/i18n_spec.rb +62 -0
- data/spec/locale_backend/legacy_spec.rb +74 -0
- data/spec/money/allocation_spec.rb +130 -0
- data/spec/money/arithmetic_spec.rb +217 -104
- data/spec/money/constructors_spec.rb +0 -12
- data/spec/money/formatting_spec.rb +320 -179
- data/spec/money/locale_backend_spec.rb +14 -0
- data/spec/money_spec.rb +159 -26
- data/spec/rates_store/memory_spec.rb +13 -2
- data/spec/spec_helper.rb +2 -0
- data/spec/support/shared_examples/money_examples.rb +14 -0
- metadata +32 -41
- data/lib/money/money/formatting.rb +0 -417
data/lib/money/money.rb
CHANGED
@@ -3,7 +3,9 @@ require "money/bank/variable_exchange"
|
|
3
3
|
require "money/bank/single_currency"
|
4
4
|
require "money/money/arithmetic"
|
5
5
|
require "money/money/constructors"
|
6
|
-
require "money/money/
|
6
|
+
require "money/money/formatter"
|
7
|
+
require "money/money/allocation"
|
8
|
+
require "money/money/locale_backend"
|
7
9
|
|
8
10
|
# "Money is any object or record that is generally accepted as payment for
|
9
11
|
# goods and services and repayment of debts in a given socio-economic context
|
@@ -15,7 +17,8 @@ require "money/money/formatting"
|
|
15
17
|
#
|
16
18
|
# @see http://en.wikipedia.org/wiki/Money
|
17
19
|
class Money
|
18
|
-
include Comparable
|
20
|
+
include Comparable
|
21
|
+
include Money::Arithmetic
|
19
22
|
extend Constructors
|
20
23
|
|
21
24
|
# Raised when smallest denomination of a currency is not defined
|
@@ -34,17 +37,17 @@ class Money
|
|
34
37
|
# The value of the monetary amount represented in the fractional or subunit
|
35
38
|
# of the currency.
|
36
39
|
#
|
37
|
-
# For example, in the US
|
38
|
-
# there are 100 cents in one US
|
40
|
+
# For example, in the US dollar currency the fractional unit is cents, and
|
41
|
+
# there are 100 cents in one US dollar. So given the Money representation of
|
39
42
|
# one US dollar, the fractional interpretation is 100.
|
40
43
|
#
|
41
|
-
# Another example is that of the Kuwaiti
|
42
|
-
# unit is the
|
43
|
-
# Money representation of one Kuwaiti
|
44
|
+
# Another example is that of the Kuwaiti dinar. In this case the fractional
|
45
|
+
# unit is the fils and there 1000 fils to one Kuwaiti dinar. So given the
|
46
|
+
# Money representation of one Kuwaiti dinar, the fractional interpretation is
|
44
47
|
# 1000.
|
45
48
|
#
|
46
49
|
# @return [Integer] when infinite_precision is false
|
47
|
-
# @return [BigDecimal] when
|
50
|
+
# @return [BigDecimal] when infinite_precision is true
|
48
51
|
#
|
49
52
|
# @see infinite_precision
|
50
53
|
def fractional
|
@@ -56,7 +59,7 @@ class Money
|
|
56
59
|
end
|
57
60
|
|
58
61
|
# Round a given amount of money to the nearest possible amount in cash value. For
|
59
|
-
# example, in Swiss
|
62
|
+
# example, in Swiss franc (CHF), the smallest possible amount of cash value is
|
60
63
|
# CHF 0.05. Therefore, this method rounds CHF 0.07 to CHF 0.05, and CHF 0.08 to
|
61
64
|
# CHF 0.10.
|
62
65
|
#
|
@@ -78,7 +81,7 @@ class Money
|
|
78
81
|
|
79
82
|
# @!attribute [r] currency
|
80
83
|
# @return [Currency] The money's currency.
|
81
|
-
# @!attribute [r] bank
|
84
|
+
# @!attribute [r] bank
|
82
85
|
# @return [Money::Bank::Base] The +Money::Bank+-based object which currency
|
83
86
|
# exchanges are performed with.
|
84
87
|
|
@@ -95,7 +98,7 @@ class Money
|
|
95
98
|
# one to specify custom exchange rates.
|
96
99
|
#
|
97
100
|
# @!attribute default_formatting_rules
|
98
|
-
# @return [Hash] Use this to define a default hash of rules for
|
101
|
+
# @return [Hash] Use this to define a default hash of rules for every time
|
99
102
|
# +Money#format+ is called. Rules provided on method call will be
|
100
103
|
# merged with the default ones. To overwrite a rule, just provide the
|
101
104
|
# intended value while calling +format+.
|
@@ -103,9 +106,9 @@ class Money
|
|
103
106
|
# @see +Money::Formatting#format+ for more details.
|
104
107
|
#
|
105
108
|
# @example
|
106
|
-
# Money.default_formatting_rules = { :
|
109
|
+
# Money.default_formatting_rules = { display_free: true }
|
107
110
|
# Money.new(0, "USD").format # => "free"
|
108
|
-
# Money.new(0, "USD").format(:
|
111
|
+
# Money.new(0, "USD").format(display_free: false) # => "$0.00"
|
109
112
|
#
|
110
113
|
# @!attribute [rw] use_i18n
|
111
114
|
# @return [Boolean] Use this to disable i18n even if it's used by other
|
@@ -115,10 +118,11 @@ class Money
|
|
115
118
|
# @return [Boolean] Use this to enable infinite precision cents
|
116
119
|
#
|
117
120
|
# @!attribute [rw] conversion_precision
|
118
|
-
# @return [
|
121
|
+
# @return [Integer] Use this to specify precision for converting Rational
|
119
122
|
# to BigDecimal
|
120
123
|
attr_accessor :default_bank, :default_formatting_rules,
|
121
|
-
:use_i18n, :infinite_precision, :conversion_precision
|
124
|
+
:use_i18n, :infinite_precision, :conversion_precision,
|
125
|
+
:locale_backend
|
122
126
|
|
123
127
|
# @attr_writer rounding_mode Use this to specify the rounding mode
|
124
128
|
#
|
@@ -139,6 +143,18 @@ class Money
|
|
139
143
|
end
|
140
144
|
end
|
141
145
|
|
146
|
+
def self.locale_backend=(value)
|
147
|
+
@locale_backend = value ? LocaleBackend.find(value) : nil
|
148
|
+
end
|
149
|
+
|
150
|
+
def self.use_i18n=(value)
|
151
|
+
if value
|
152
|
+
warn '[DEPRECATION] `use_i18n` is deprecated - use `Money.locale_backend = :i18n` instead'
|
153
|
+
end
|
154
|
+
|
155
|
+
@use_i18n = value
|
156
|
+
end
|
157
|
+
|
142
158
|
def self.setup_defaults
|
143
159
|
# Set the default bank for creating new +Money+ objects.
|
144
160
|
self.default_bank = Bank::VariableExchange.instance
|
@@ -147,7 +163,10 @@ class Money
|
|
147
163
|
self.default_currency = Currency.new("USD")
|
148
164
|
|
149
165
|
# Default to using i18n
|
150
|
-
|
166
|
+
@use_i18n = true
|
167
|
+
|
168
|
+
# Default to using legacy locale backend
|
169
|
+
self.locale_backend = :legacy
|
151
170
|
|
152
171
|
# Default to not using infinite precision cents
|
153
172
|
self.infinite_precision = false
|
@@ -166,7 +185,7 @@ class Money
|
|
166
185
|
setup_defaults
|
167
186
|
|
168
187
|
# Use this to return the rounding mode. You may also pass a
|
169
|
-
# rounding mode and a block to
|
188
|
+
# rounding mode and a block to temporarily change it. It will
|
170
189
|
# then return the results of the block instead.
|
171
190
|
#
|
172
191
|
# @param [BigDecimal::ROUND_MODE] mode
|
@@ -175,9 +194,9 @@ class Money
|
|
175
194
|
#
|
176
195
|
# @example
|
177
196
|
# fee = Money.rounding_mode(BigDecimal::ROUND_HALF_UP) do
|
178
|
-
# Money.new(1200) * BigDecimal
|
197
|
+
# Money.new(1200) * BigDecimal('0.029')
|
179
198
|
# end
|
180
|
-
def self.rounding_mode(mode=nil)
|
199
|
+
def self.rounding_mode(mode = nil)
|
181
200
|
if mode.nil?
|
182
201
|
Thread.current[:money_rounding_mode] || @rounding_mode
|
183
202
|
else
|
@@ -226,8 +245,9 @@ class Money
|
|
226
245
|
#
|
227
246
|
# @see #initialize
|
228
247
|
def self.from_amount(amount, currency = default_currency, bank = default_bank)
|
229
|
-
|
230
|
-
|
248
|
+
raise ArgumentError, "'amount' must be numeric" unless Numeric === amount
|
249
|
+
|
250
|
+
currency = Currency.wrap(currency) || Money.default_currency
|
231
251
|
value = amount.to_d * currency.subunit_to_unit
|
232
252
|
value = value.round(0, rounding_mode) unless infinite_precision
|
233
253
|
new(value, currency, bank)
|
@@ -269,7 +289,7 @@ class Money
|
|
269
289
|
# @return [BigDecimal]
|
270
290
|
#
|
271
291
|
# @example
|
272
|
-
# Money.new(1_00, "USD").dollars # => BigDecimal
|
292
|
+
# Money.new(1_00, "USD").dollars # => BigDecimal("1.00")
|
273
293
|
#
|
274
294
|
# @see #amount
|
275
295
|
# @see #to_d
|
@@ -284,7 +304,7 @@ class Money
|
|
284
304
|
# @return [BigDecimal]
|
285
305
|
#
|
286
306
|
# @example
|
287
|
-
# Money.new(1_00, "USD").amount # => BigDecimal
|
307
|
+
# Money.new(1_00, "USD").amount # => BigDecimal("1.00")
|
288
308
|
#
|
289
309
|
# @see #to_d
|
290
310
|
# @see #fractional
|
@@ -300,6 +320,7 @@ class Money
|
|
300
320
|
# @example
|
301
321
|
# Money.new(100, :USD).currency_as_string #=> "USD"
|
302
322
|
def currency_as_string
|
323
|
+
warn "[DEPRECATION] `currency_as_string` is deprecated. Please use `.currency.to_s` instead."
|
303
324
|
currency.to_s
|
304
325
|
end
|
305
326
|
|
@@ -312,13 +333,15 @@ class Money
|
|
312
333
|
# @example
|
313
334
|
# Money.new(100).currency_as_string("CAD") #=> #<Money::Currency id: cad>
|
314
335
|
def currency_as_string=(val)
|
336
|
+
warn "[DEPRECATION] `currency_as_string=` is deprecated - Money instances are immutable." \
|
337
|
+
" Please use `with_currency` instead."
|
315
338
|
@currency = Currency.wrap(val)
|
316
339
|
end
|
317
340
|
|
318
|
-
# Returns a
|
341
|
+
# Returns a Integer hash value based on the +fractional+ and +currency+ attributes
|
319
342
|
# in order to use functions like & (intersection), group_by, etc.
|
320
343
|
#
|
321
|
-
# @return [
|
344
|
+
# @return [Integer]
|
322
345
|
#
|
323
346
|
# @example
|
324
347
|
# Money.new(100).hash #=> 908351
|
@@ -350,19 +373,10 @@ class Money
|
|
350
373
|
# @example
|
351
374
|
# Money.ca_dollar(100).to_s #=> "1.00"
|
352
375
|
def to_s
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
unit
|
358
|
-
else
|
359
|
-
"#{unit}#{decimal_mark}#{fraction}"
|
360
|
-
end
|
361
|
-
else
|
362
|
-
"#{unit}#{decimal_mark}#{pad_subunit(subunit)}#{fraction}"
|
363
|
-
end
|
364
|
-
|
365
|
-
fractional < 0 ? "-#{str}" : str
|
376
|
+
format thousands_separator: '',
|
377
|
+
no_cents_if_whole: currency.decimal_places == 0,
|
378
|
+
symbol: false,
|
379
|
+
ignore_defaults: true
|
366
380
|
end
|
367
381
|
|
368
382
|
# Return the amount of money as a BigDecimal.
|
@@ -370,7 +384,7 @@ class Money
|
|
370
384
|
# @return [BigDecimal]
|
371
385
|
#
|
372
386
|
# @example
|
373
|
-
# Money.us_dollar(1_00).to_d #=> BigDecimal
|
387
|
+
# Money.us_dollar(1_00).to_d #=> BigDecimal("1.00")
|
374
388
|
def to_d
|
375
389
|
as_d(fractional) / as_d(currency.subunit_to_unit)
|
376
390
|
end
|
@@ -398,7 +412,22 @@ class Money
|
|
398
412
|
to_d.to_f
|
399
413
|
end
|
400
414
|
|
401
|
-
#
|
415
|
+
# Returns a new Money instance in a given currency leaving the amount intact
|
416
|
+
# and not performing currency conversion.
|
417
|
+
#
|
418
|
+
# @param [Currency, String, Symbol] new_currency Currency of the new object.
|
419
|
+
#
|
420
|
+
# @return [self]
|
421
|
+
def with_currency(new_currency)
|
422
|
+
new_currency = Currency.wrap(new_currency)
|
423
|
+
if !new_currency || currency == new_currency
|
424
|
+
self
|
425
|
+
else
|
426
|
+
self.class.new(fractional, new_currency, bank)
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
# Conversion to +self+.
|
402
431
|
#
|
403
432
|
# @return [self]
|
404
433
|
def to_money(given_currency = nil)
|
@@ -436,7 +465,7 @@ class Money
|
|
436
465
|
end
|
437
466
|
|
438
467
|
# Receive a money object with the same amount as the current Money object
|
439
|
-
# in
|
468
|
+
# in United States dollar.
|
440
469
|
#
|
441
470
|
# @return [Money]
|
442
471
|
#
|
@@ -448,7 +477,7 @@ class Money
|
|
448
477
|
end
|
449
478
|
|
450
479
|
# Receive a money object with the same amount as the current Money object
|
451
|
-
# in
|
480
|
+
# in Canadian dollar.
|
452
481
|
#
|
453
482
|
# @return [Money]
|
454
483
|
#
|
@@ -471,52 +500,27 @@ class Money
|
|
471
500
|
exchange_to("EUR")
|
472
501
|
end
|
473
502
|
|
474
|
-
#
|
475
|
-
#
|
476
|
-
#
|
477
|
-
# listed first will likely receive more pennies than ones that are listed later
|
503
|
+
# Splits a given amount in parts without loosing pennies. The left-over pennies will be
|
504
|
+
# distributed round-robin amongst the parties. This means that parties listed first will likely
|
505
|
+
# receive more pennies than ones that are listed later.
|
478
506
|
#
|
479
|
-
# @param [Array<Numeric
|
507
|
+
# @param [Array<Numeric>, Numeric] pass [2, 1, 1] to give twice as much to party1 as party2 or
|
508
|
+
# party3 which results in 50% of the cash to party1, 25% to party2, and 25% to party3. Passing a
|
509
|
+
# number instead of an array will split the amount evenly (without loosing pennies when rounding).
|
480
510
|
#
|
481
511
|
# @return [Array<Money>]
|
482
512
|
#
|
483
513
|
# @example
|
484
|
-
# Money.new(5, "USD").allocate([
|
485
|
-
# Money.new(100, "USD").allocate([
|
514
|
+
# Money.new(5, "USD").allocate([3, 7]) #=> [Money.new(2), Money.new(3)]
|
515
|
+
# Money.new(100, "USD").allocate([1, 1, 1]) #=> [Money.new(34), Money.new(33), Money.new(33)]
|
516
|
+
# Money.new(100, "USD").allocate(2) #=> [Money.new(50), Money.new(50)]
|
517
|
+
# Money.new(100, "USD").allocate(3) #=> [Money.new(34), Money.new(33), Money.new(33)]
|
486
518
|
#
|
487
|
-
def allocate(
|
488
|
-
|
489
|
-
|
490
|
-
if (allocations - BigDecimal("1")) > Float::EPSILON
|
491
|
-
raise ArgumentError, "splits add to more then 100%"
|
492
|
-
end
|
493
|
-
|
494
|
-
amounts, left_over = amounts_from_splits(allocations, splits)
|
495
|
-
|
496
|
-
unless self.class.infinite_precision
|
497
|
-
left_over.to_i.times { |i| amounts[i % amounts.length] += 1 }
|
498
|
-
end
|
499
|
-
|
500
|
-
amounts.collect { |fractional| self.class.new(fractional, currency) }
|
501
|
-
end
|
502
|
-
|
503
|
-
# Split money amongst parties evenly without losing pennies.
|
504
|
-
#
|
505
|
-
# @param [Numeric] num number of parties.
|
506
|
-
#
|
507
|
-
# @return [Array<Money>]
|
508
|
-
#
|
509
|
-
# @example
|
510
|
-
# Money.new(100, "USD").split(3) #=> [Money.new(34), Money.new(33), Money.new(33)]
|
511
|
-
def split(num)
|
512
|
-
raise ArgumentError, "need at least one party" if num < 1
|
513
|
-
|
514
|
-
if self.class.infinite_precision
|
515
|
-
split_infinite(num)
|
516
|
-
else
|
517
|
-
split_flat(num)
|
518
|
-
end
|
519
|
+
def allocate(parts)
|
520
|
+
amounts = Money::Allocation.generate(fractional, parts, !Money.infinite_precision)
|
521
|
+
amounts.map { |amount| self.class.new(amount, currency) }
|
519
522
|
end
|
523
|
+
alias_method :split, :allocate
|
520
524
|
|
521
525
|
# Round the monetary amount to smallest unit of coinage.
|
522
526
|
#
|
@@ -533,85 +537,46 @@ class Money
|
|
533
537
|
# @see
|
534
538
|
# Money.infinite_precision
|
535
539
|
#
|
536
|
-
def round(rounding_mode = self.class.rounding_mode)
|
537
|
-
|
538
|
-
|
539
|
-
else
|
540
|
-
self
|
541
|
-
end
|
540
|
+
def round(rounding_mode = self.class.rounding_mode, rounding_precision = 0)
|
541
|
+
rounded_amount = as_d(@fractional).round(rounding_precision, rounding_mode)
|
542
|
+
self.class.new(rounded_amount, currency, bank)
|
542
543
|
end
|
543
544
|
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
end
|
553
|
-
|
554
|
-
def strings_from_fractional
|
555
|
-
unit, subunit = fractional().abs.divmod(currency.subunit_to_unit)
|
556
|
-
|
557
|
-
if self.class.infinite_precision
|
558
|
-
strings_for_infinite_precision(unit, subunit)
|
559
|
-
else
|
560
|
-
strings_for_base_precision(unit, subunit)
|
561
|
-
end
|
562
|
-
end
|
563
|
-
|
564
|
-
def strings_for_infinite_precision(unit, subunit)
|
565
|
-
subunit, fraction = subunit.divmod(BigDecimal("1"))
|
566
|
-
fraction = fraction.to_s("F")[2..-1] # want fractional part "0.xxx"
|
567
|
-
fraction = "" if fraction =~ /^0+$/
|
568
|
-
|
569
|
-
[unit.to_i.to_s, subunit.to_i.to_s, fraction]
|
570
|
-
end
|
571
|
-
|
572
|
-
def strings_for_base_precision(unit, subunit)
|
573
|
-
[unit.to_s, subunit.to_s, ""]
|
574
|
-
end
|
575
|
-
|
576
|
-
def pad_subunit(subunit)
|
577
|
-
cnt = currency.decimal_places
|
578
|
-
padding = "0" * cnt
|
579
|
-
"#{padding}#{subunit}"[-1 * cnt, cnt]
|
580
|
-
end
|
581
|
-
|
582
|
-
def allocations_from_splits(splits)
|
583
|
-
splits.inject(0) { |sum, n| sum + as_d(n) }
|
545
|
+
# Creates a formatted price string according to several rules.
|
546
|
+
#
|
547
|
+
# @param [Hash] See Money::Formatter for the list of formatting options
|
548
|
+
#
|
549
|
+
# @return [String]
|
550
|
+
#
|
551
|
+
def format(*rules)
|
552
|
+
Money::Formatter.new(self, *rules).to_s
|
584
553
|
end
|
585
554
|
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
(fractional * ratio / allocations).floor.tap do |frac|
|
594
|
-
left_over -= frac
|
595
|
-
end
|
596
|
-
end
|
597
|
-
end
|
598
|
-
|
599
|
-
[amounts, left_over]
|
555
|
+
# Returns a thousands separator according to the locale
|
556
|
+
#
|
557
|
+
# @return [String]
|
558
|
+
#
|
559
|
+
def thousands_separator
|
560
|
+
(locale_backend && locale_backend.lookup(:thousands_separator, currency)) ||
|
561
|
+
Money::Formatter::DEFAULTS[:thousands_separator]
|
600
562
|
end
|
601
563
|
|
602
|
-
|
603
|
-
|
604
|
-
|
564
|
+
# Returns a decimal mark according to the locale
|
565
|
+
#
|
566
|
+
# @return [String]
|
567
|
+
#
|
568
|
+
def decimal_mark
|
569
|
+
(locale_backend && locale_backend.lookup(:decimal_mark, currency)) ||
|
570
|
+
Money::Formatter::DEFAULTS[:decimal_mark]
|
605
571
|
end
|
606
572
|
|
607
|
-
|
608
|
-
low = self.class.new(fractional / num, currency)
|
609
|
-
high = self.class.new(low.fractional + 1, currency)
|
610
|
-
|
611
|
-
remainder = fractional % num
|
573
|
+
private
|
612
574
|
|
613
|
-
|
614
|
-
|
575
|
+
def as_d(num)
|
576
|
+
if num.respond_to?(:to_d)
|
577
|
+
num.is_a?(Rational) ? num.to_d(self.class.conversion_precision) : num.to_d
|
578
|
+
else
|
579
|
+
BigDecimal(num.to_s.empty? ? 0 : num.to_s)
|
615
580
|
end
|
616
581
|
end
|
617
582
|
|
@@ -622,4 +587,8 @@ class Money
|
|
622
587
|
value.round(0, self.class.rounding_mode).to_i
|
623
588
|
end
|
624
589
|
end
|
590
|
+
|
591
|
+
def locale_backend
|
592
|
+
self.class.locale_backend
|
593
|
+
end
|
625
594
|
end
|