money 6.7.0 → 6.13.0
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/.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
|