money 6.5.1 → 6.16.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 +5 -5
  2. data/CHANGELOG.md +209 -5
  3. data/LICENSE +18 -16
  4. data/README.md +321 -70
  5. data/config/currency_backwards_compatible.json +65 -0
  6. data/config/currency_iso.json +280 -94
  7. data/config/currency_non_iso.json +101 -3
  8. data/lib/money/bank/base.rb +1 -3
  9. data/lib/money/bank/variable_exchange.rb +88 -96
  10. data/lib/money/currency/heuristics.rb +1 -143
  11. data/lib/money/currency/loader.rb +15 -13
  12. data/lib/money/currency.rb +98 -81
  13. data/lib/money/locale_backend/base.rb +7 -0
  14. data/lib/money/locale_backend/currency.rb +11 -0
  15. data/lib/money/locale_backend/errors.rb +6 -0
  16. data/lib/money/locale_backend/i18n.rb +25 -0
  17. data/lib/money/locale_backend/legacy.rb +28 -0
  18. data/lib/money/money/allocation.rb +46 -0
  19. data/lib/money/money/arithmetic.rb +97 -52
  20. data/lib/money/money/constructors.rb +5 -6
  21. data/lib/money/money/formatter.rb +399 -0
  22. data/lib/money/money/formatting_rules.rb +142 -0
  23. data/lib/money/money/locale_backend.rb +22 -0
  24. data/lib/money/money.rb +268 -194
  25. data/lib/money/rates_store/memory.rb +120 -0
  26. data/lib/money/version.rb +1 -1
  27. data/money.gemspec +15 -20
  28. metadata +36 -59
  29. data/.coveralls.yml +0 -1
  30. data/.gitignore +0 -22
  31. data/.travis.yml +0 -13
  32. data/AUTHORS +0 -116
  33. data/CONTRIBUTING.md +0 -17
  34. data/Gemfile +0 -7
  35. data/Rakefile +0 -17
  36. data/lib/money/money/formatting.rb +0 -386
  37. data/spec/bank/base_spec.rb +0 -77
  38. data/spec/bank/single_currency_spec.rb +0 -11
  39. data/spec/bank/variable_exchange_spec.rb +0 -275
  40. data/spec/currency/heuristics_spec.rb +0 -84
  41. data/spec/currency_spec.rb +0 -321
  42. data/spec/money/arithmetic_spec.rb +0 -568
  43. data/spec/money/constructors_spec.rb +0 -75
  44. data/spec/money/formatting_spec.rb +0 -667
  45. data/spec/money_spec.rb +0 -745
  46. data/spec/spec_helper.rb +0 -23
@@ -1,5 +1,13 @@
1
1
  class Money
2
2
  module Arithmetic
3
+ # Wrapper for coerced numeric values to distinguish
4
+ # when numeric was on the 1st place in operation.
5
+ CoercedNumeric = Struct.new(:value) do
6
+ # Proxy #zero? method to skip unnecessary typecasts. See #- and #+.
7
+ def zero?
8
+ value.zero?
9
+ end
10
+ end
3
11
 
4
12
  # Returns a money object with changed polarity.
5
13
  #
@@ -8,40 +16,65 @@ class Money
8
16
  # @example
9
17
  # - Money.new(100) #=> #<Money @fractional=-100>
10
18
  def -@
11
- Money.new(-fractional, currency)
19
+ dup_with(fractional: -fractional)
12
20
  end
13
21
 
14
- # Checks whether two money objects have the same currency and the same
15
- # amount. Checks against money objects with a different currency and checks
16
- # against objects that do not respond to #to_money will always return false.
22
+ # Checks whether two Money objects have the same currency and the same
23
+ # amount. If Money objects have a different currency it will only be true
24
+ # if the amounts are both zero. Checks against objects that are not Money or
25
+ # a subclass will always return false.
17
26
  #
18
27
  # @param [Money] other_money Value to compare with.
19
28
  #
20
29
  # @return [Boolean]
21
30
  #
22
31
  # @example
23
- # Money.new(100) == Money.new(101) #=> false
24
- # Money.new(100) == Money.new(100) #=> true
25
- def ==(other_money)
26
- if other_money.respond_to?(:to_money)
27
- other_money = other_money.to_money
28
- fractional == other_money.fractional && currency == other_money.currency
32
+ # Money.new(100).eql?(Money.new(101)) #=> false
33
+ # Money.new(100).eql?(Money.new(100)) #=> true
34
+ # Money.new(100, "USD").eql?(Money.new(100, "GBP")) #=> false
35
+ # Money.new(0, "USD").eql?(Money.new(0, "EUR")) #=> true
36
+ # Money.new(100).eql?("1.00") #=> false
37
+ def eql?(other_money)
38
+ if other_money.is_a?(Money)
39
+ (fractional == other_money.fractional && currency == other_money.currency) ||
40
+ (fractional == 0 && other_money.fractional == 0)
29
41
  else
30
42
  false
31
43
  end
32
44
  end
33
- alias_method :eql?, :==
34
45
 
35
- def <=>(val)
36
- if val.respond_to?(:to_money)
37
- val = val.to_money unless val.respond_to?(:fractional)
38
- if fractional != 0 && val.fractional != 0 && currency != val.currency
39
- val = val.exchange_to(currency)
40
- end
41
- fractional <=> val.fractional
42
- else
43
- raise ArgumentError, "Comparison of #{self.class} with #{val.inspect} failed"
46
+ # Compares two Money objects. If money objects have a different currency it
47
+ # will attempt to convert the currency.
48
+ #
49
+ # @param [Money] other Value to compare with.
50
+ #
51
+ # @return [Integer]
52
+ #
53
+ # @raise [TypeError] when other object is not Money
54
+ #
55
+ def <=>(other)
56
+ unless other.is_a?(Money)
57
+ return unless other.respond_to?(:zero?) && other.zero?
58
+ return other.is_a?(CoercedNumeric) ? 0 <=> fractional : fractional <=> 0
59
+ end
60
+
61
+ # Always allow comparison with zero
62
+ if zero? || other.zero?
63
+ return fractional <=> other.fractional
44
64
  end
65
+
66
+ other = other.exchange_to(currency)
67
+ fractional <=> other.fractional
68
+ rescue Money::Bank::UnknownRate
69
+ end
70
+
71
+ # Uses Comparable's implementation but raises ArgumentError if non-zero
72
+ # numeric value is given.
73
+ def ==(other)
74
+ if other.is_a?(Numeric) && !other.zero?
75
+ raise ArgumentError, 'Money#== supports only zero numerics'
76
+ end
77
+ super
45
78
  end
46
79
 
47
80
  # Test if the amount is positive. Returns +true+ if the money amount is
@@ -70,37 +103,51 @@ class Money
70
103
  fractional < 0
71
104
  end
72
105
 
106
+ # @method +(other)
73
107
  # Returns a new Money object containing the sum of the two operands' monetary
74
108
  # values. If +other_money+ has a different currency then its monetary value
75
109
  # is automatically exchanged to this object's currency using +exchange_to+.
76
110
  #
77
- # @param [Money] other_money Other +Money+ object to add.
111
+ # @param [Money] other Other +Money+ object to add.
78
112
  #
79
113
  # @return [Money]
80
114
  #
81
115
  # @example
82
116
  # Money.new(100) + Money.new(100) #=> #<Money @fractional=200>
83
- def +(other_money)
84
- return self if other_money == 0
85
- other_money = other_money.exchange_to(currency)
86
- Money.new(fractional + other_money.fractional, currency)
87
- end
88
-
117
+ #
118
+ # @method -(other)
89
119
  # Returns a new Money object containing the difference between the two
90
120
  # operands' monetary values. If +other_money+ has a different currency then
91
121
  # its monetary value is automatically exchanged to this object's currency
92
122
  # using +exchange_to+.
93
123
  #
94
- # @param [Money] other_money Other +Money+ object to subtract.
124
+ # @param [Money] other Other +Money+ object to subtract.
95
125
  #
96
126
  # @return [Money]
97
127
  #
98
128
  # @example
99
129
  # Money.new(100) - Money.new(99) #=> #<Money @fractional=1>
100
- def -(other_money)
101
- return self if other_money == 0
102
- other_money = other_money.exchange_to(currency)
103
- Money.new(fractional - other_money.fractional, currency)
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
+
135
+ define_method(op) do |other|
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}"
149
+ end
150
+ end
104
151
  end
105
152
 
106
153
  # Multiplies the monetary value with the given number and returns a new
@@ -112,16 +159,17 @@ class Money
112
159
  #
113
160
  # @return [Money] The resulting money.
114
161
  #
115
- # @raise [ArgumentError] If +value+ is NOT a number.
162
+ # @raise [TypeError] If +value+ is NOT a number.
116
163
  #
117
164
  # @example
118
165
  # Money.new(100) * 2 #=> #<Money @fractional=200>
119
166
  #
120
167
  def *(value)
168
+ value = value.value if value.is_a?(CoercedNumeric)
121
169
  if value.is_a? Numeric
122
- Money.new(fractional * value, currency)
170
+ dup_with(fractional: fractional * value)
123
171
  else
124
- raise ArgumentError, "Can't multiply a Money by a #{value.class.name}'s value"
172
+ raise TypeError, "Can't multiply a #{self.class.name} by a #{value.class.name}'s value"
125
173
  end
126
174
  end
127
175
 
@@ -141,10 +189,11 @@ class Money
141
189
  # Money.new(100) / Money.new(10) #=> 10.0
142
190
  #
143
191
  def /(value)
144
- if value.is_a?(Money)
192
+ if value.is_a?(self.class)
145
193
  fractional / as_d(value.exchange_to(currency).fractional).to_f
146
194
  else
147
- Money.new(fractional / as_d(value), currency)
195
+ raise TypeError, 'Can not divide by Money' if value.is_a?(CoercedNumeric)
196
+ dup_with(fractional: fractional / as_d(value))
148
197
  end
149
198
  end
150
199
 
@@ -164,9 +213,9 @@ class Money
164
213
  # Divide money by money or fixnum and return array containing quotient and
165
214
  # modulus.
166
215
  #
167
- # @param [Money, Fixnum] val Number to divmod by.
216
+ # @param [Money, Integer] val Number to divmod by.
168
217
  #
169
- # @return [Array<Money,Money>,Array<Fixnum,Money>]
218
+ # @return [Array<Money,Money>,Array<Integer,Money>]
170
219
  #
171
220
  # @example
172
221
  # Money.new(100).divmod(9) #=> [#<Money @fractional=11>, #<Money @fractional=1>]
@@ -182,23 +231,19 @@ class Money
182
231
  def divmod_money(val)
183
232
  cents = val.exchange_to(currency).cents
184
233
  quotient, remainder = fractional.divmod(cents)
185
- [quotient, Money.new(remainder, currency)]
234
+ [quotient, dup_with(fractional: remainder)]
186
235
  end
187
236
  private :divmod_money
188
237
 
189
238
  def divmod_other(val)
190
- if self.class.infinite_precision
191
- quotient, remainder = fractional.divmod(as_d(val))
192
- [Money.new(quotient, currency), Money.new(remainder, currency)]
193
- else
194
- [div(val), Money.new(fractional.modulo(val), currency)]
195
- end
239
+ quotient, remainder = fractional.divmod(as_d(val))
240
+ [dup_with(fractional: quotient), dup_with(fractional: remainder)]
196
241
  end
197
242
  private :divmod_other
198
243
 
199
244
  # Equivalent to +self.divmod(val)[1]+
200
245
  #
201
- # @param [Money, Fixnum] val Number take modulo with.
246
+ # @param [Money, Integer] val Number take modulo with.
202
247
  #
203
248
  # @return [Money]
204
249
  #
@@ -211,7 +256,7 @@ class Money
211
256
 
212
257
  # Synonym for +#modulo+.
213
258
  #
214
- # @param [Money, Fixnum] val Number take modulo with.
259
+ # @param [Money, Integer] val Number take modulo with.
215
260
  #
216
261
  # @return [Money]
217
262
  #
@@ -222,7 +267,7 @@ class Money
222
267
 
223
268
  # If different signs +self.modulo(val) - val+ otherwise +self.modulo(val)+
224
269
  #
225
- # @param [Money, Fixnum] val Number to rake remainder with.
270
+ # @param [Money, Integer] val Number to rake remainder with.
226
271
  #
227
272
  # @return [Money]
228
273
  #
@@ -236,7 +281,7 @@ class Money
236
281
  if (fractional < 0 && val < 0) || (fractional > 0 && val > 0)
237
282
  self.modulo(val)
238
283
  else
239
- self.modulo(val) - (val.is_a?(Money) ? val : Money.new(val, currency))
284
+ self.modulo(val) - (val.is_a?(Money) ? val : dup_with(fractional: val))
240
285
  end
241
286
  end
242
287
 
@@ -247,7 +292,7 @@ class Money
247
292
  # @example
248
293
  # Money.new(-100).abs #=> #<Money @fractional=100>
249
294
  def abs
250
- Money.new(fractional.abs, currency)
295
+ dup_with(fractional: fractional.abs)
251
296
  end
252
297
 
253
298
  # Test if the money amount is zero.
@@ -279,7 +324,7 @@ class Money
279
324
  # @example
280
325
  # 2 * Money.new(10) #=> #<Money @fractional=20>
281
326
  def coerce(other)
282
- [self, other]
327
+ [self, CoercedNumeric.new(other)]
283
328
  end
284
329
  end
285
330
  end
@@ -10,8 +10,7 @@ class Money
10
10
  # @example
11
11
  # Money.empty #=> #<Money @fractional=0>
12
12
  def empty(currency = default_currency)
13
- @empty ||= {}
14
- @empty[currency] ||= Money.new(0, currency).freeze
13
+ new(0, currency)
15
14
  end
16
15
  alias_method :zero, :empty
17
16
 
@@ -28,7 +27,7 @@ class Money
28
27
  # n.cents #=> 100
29
28
  # n.currency #=> #<Money::Currency id: cad>
30
29
  def ca_dollar(cents)
31
- Money.new(cents, "CAD")
30
+ new(cents, "CAD")
32
31
  end
33
32
  alias_method :cad, :ca_dollar
34
33
 
@@ -45,7 +44,7 @@ class Money
45
44
  # n.cents #=> 100
46
45
  # n.currency #=> #<Money::Currency id: usd>
47
46
  def us_dollar(cents)
48
- Money.new(cents, "USD")
47
+ new(cents, "USD")
49
48
  end
50
49
  alias_method :usd, :us_dollar
51
50
 
@@ -61,7 +60,7 @@ class Money
61
60
  # n.cents #=> 100
62
61
  # n.currency #=> #<Money::Currency id: eur>
63
62
  def euro(cents)
64
- Money.new(cents, "EUR")
63
+ new(cents, "EUR")
65
64
  end
66
65
  alias_method :eur, :euro
67
66
 
@@ -77,7 +76,7 @@ class Money
77
76
  # n.fractional #=> 100
78
77
  # n.currency #=> #<Money::Currency id: gbp>
79
78
  def pound_sterling(pence)
80
- Money.new(pence, "GBP")
79
+ new(pence, "GBP")
81
80
  end
82
81
  alias_method :gbp, :pound_sterling
83
82