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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +2 -1
  3. data/.travis.yml +22 -5
  4. data/AUTHORS +5 -0
  5. data/CHANGELOG.md +109 -3
  6. data/Gemfile +13 -4
  7. data/LICENSE +2 -0
  8. data/README.md +69 -49
  9. data/config/currency_backwards_compatible.json +30 -0
  10. data/config/currency_iso.json +139 -62
  11. data/config/currency_non_iso.json +66 -2
  12. data/lib/money.rb +0 -13
  13. data/lib/money/bank/variable_exchange.rb +9 -22
  14. data/lib/money/currency.rb +35 -38
  15. data/lib/money/currency/heuristics.rb +1 -144
  16. data/lib/money/currency/loader.rb +1 -1
  17. data/lib/money/locale_backend/base.rb +7 -0
  18. data/lib/money/locale_backend/errors.rb +6 -0
  19. data/lib/money/locale_backend/i18n.rb +24 -0
  20. data/lib/money/locale_backend/legacy.rb +28 -0
  21. data/lib/money/money.rb +120 -151
  22. data/lib/money/money/allocation.rb +37 -0
  23. data/lib/money/money/arithmetic.rb +57 -52
  24. data/lib/money/money/constructors.rb +1 -2
  25. data/lib/money/money/formatter.rb +397 -0
  26. data/lib/money/money/formatting_rules.rb +120 -0
  27. data/lib/money/money/locale_backend.rb +20 -0
  28. data/lib/money/rates_store/memory.rb +1 -2
  29. data/lib/money/version.rb +1 -1
  30. data/money.gemspec +10 -16
  31. data/spec/bank/variable_exchange_spec.rb +7 -3
  32. data/spec/currency/heuristics_spec.rb +2 -153
  33. data/spec/currency_spec.rb +45 -4
  34. data/spec/locale_backend/i18n_spec.rb +62 -0
  35. data/spec/locale_backend/legacy_spec.rb +74 -0
  36. data/spec/money/allocation_spec.rb +130 -0
  37. data/spec/money/arithmetic_spec.rb +217 -104
  38. data/spec/money/constructors_spec.rb +0 -12
  39. data/spec/money/formatting_spec.rb +320 -179
  40. data/spec/money/locale_backend_spec.rb +14 -0
  41. data/spec/money_spec.rb +159 -26
  42. data/spec/rates_store/memory_spec.rb +13 -2
  43. data/spec/spec_helper.rb +2 -0
  44. data/spec/support/shared_examples/money_examples.rb +14 -0
  45. metadata +32 -41
  46. data/lib/money/money/formatting.rb +0 -417
@@ -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/formatting"
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, Money::Arithmetic, Money::Formatting
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 Dollar currency the fractional unit is cents, and
38
- # there are 100 cents in one US Dollar. So given the Money representation of
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 Dinar. In this case the fractional
42
- # unit is the Fils and there 1000 Fils to one Kuwaiti Dinar. So given the
43
- # Money representation of one Kuwaiti Dinar, the fractional interpretation is
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 infintie_precision is true
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 francs (CHF), the smallest possible amount of cash value is
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 everytime
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 = { :display_free => true }
109
+ # Money.default_formatting_rules = { display_free: true }
107
110
  # Money.new(0, "USD").format # => "free"
108
- # Money.new(0, "USD").format(:display_free => false) # => "$0.00"
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 [Fixnum] Use this to specify precision for converting Rational
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
- self.use_i18n = true
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 temporatly change it. It will
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.new('0.029')
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
- Numeric === amount or raise ArgumentError, "'amount' must be numeric"
230
- currency = Currency.wrap(currency)
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.new("1.00")
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.new("1.00")
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 Fixnum hash value based on the +fractional+ and +currency+ attributes
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 [Fixnum]
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
- unit, subunit, fraction = strings_from_fractional
354
-
355
- str = if currency.decimal_places == 0
356
- if fraction == ""
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.new("1.00")
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
- # Conversation to +self+.
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 american dollars.
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 canadian dollar.
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
- # Allocates money between different parties without losing pennies.
475
- # After the mathematical split has been performed, leftover pennies will
476
- # be distributed round-robin amongst the parties. This means that parties
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>] splits [0.50, 0.25, 0.25] to give 50% of the cash to party1, 25% to party2, and 25% to party3.
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([0.3, 0.7]) #=> [Money.new(2), Money.new(3)]
485
- # Money.new(100, "USD").allocate([0.33, 0.33, 0.33]) #=> [Money.new(34), Money.new(33), Money.new(33)]
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(splits)
488
- allocations = allocations_from_splits(splits)
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
- if self.class.infinite_precision
538
- self.class.new(fractional.round(0, rounding_mode), self.currency)
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
- private
545
-
546
- def as_d(num)
547
- if num.respond_to?(:to_d)
548
- num.is_a?(Rational) ? num.to_d(self.class.conversion_precision) : num.to_d
549
- else
550
- BigDecimal.new(num.to_s)
551
- end
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
- def amounts_from_splits(allocations, splits)
587
- left_over = fractional
588
-
589
- amounts = splits.map do |ratio|
590
- if self.class.infinite_precision
591
- fractional * ratio
592
- else
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
- def split_infinite(num)
603
- amt = div(as_d(num))
604
- 1.upto(num).map{amt}
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
- def split_flat(num)
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
- Array.new(num).each_with_index.map do |_, index|
614
- index < remainder ? high : low
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