money 6.7.0 → 6.13.0

Sign up to get free protection for your applications and to get access to all the features.
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