money 6.13.0 → 6.13.8

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 +49 -2
  3. data/README.md +93 -3
  4. data/config/currency_backwards_compatible.json +31 -0
  5. data/config/currency_iso.json +18 -18
  6. data/lib/money/bank/variable_exchange.rb +14 -6
  7. data/lib/money/currency.rb +4 -4
  8. data/lib/money/currency/loader.rb +15 -13
  9. data/lib/money/locale_backend/currency.rb +11 -0
  10. data/lib/money/locale_backend/i18n.rb +2 -1
  11. data/lib/money/locale_backend/legacy.rb +2 -2
  12. data/lib/money/money.rb +100 -52
  13. data/lib/money/money/allocation.rb +8 -5
  14. data/lib/money/money/arithmetic.rb +20 -10
  15. data/lib/money/money/formatter.rb +3 -0
  16. data/lib/money/money/formatting_rules.rb +15 -5
  17. data/lib/money/money/locale_backend.rb +3 -1
  18. data/lib/money/rates_store/memory.rb +24 -23
  19. data/lib/money/version.rb +1 -1
  20. data/money.gemspec +4 -4
  21. metadata +11 -51
  22. data/.coveralls.yml +0 -1
  23. data/.gitignore +0 -23
  24. data/.rspec +0 -2
  25. data/.travis.yml +0 -32
  26. data/AUTHORS +0 -131
  27. data/CONTRIBUTING.md +0 -17
  28. data/Gemfile +0 -16
  29. data/Rakefile +0 -17
  30. data/spec/bank/base_spec.rb +0 -79
  31. data/spec/bank/single_currency_spec.rb +0 -13
  32. data/spec/bank/variable_exchange_spec.rb +0 -265
  33. data/spec/currency/heuristics_spec.rb +0 -11
  34. data/spec/currency/loader_spec.rb +0 -19
  35. data/spec/currency_spec.rb +0 -400
  36. data/spec/locale_backend/i18n_spec.rb +0 -62
  37. data/spec/locale_backend/legacy_spec.rb +0 -74
  38. data/spec/money/allocation_spec.rb +0 -130
  39. data/spec/money/arithmetic_spec.rb +0 -756
  40. data/spec/money/constructors_spec.rb +0 -91
  41. data/spec/money/formatting_spec.rb +0 -855
  42. data/spec/money/locale_backend_spec.rb +0 -14
  43. data/spec/money_spec.rb +0 -880
  44. data/spec/rates_store/memory_spec.rb +0 -80
  45. data/spec/spec_helper.rb +0 -30
  46. data/spec/support/shared_examples/money_examples.rb +0 -14
@@ -3,21 +3,23 @@ class Money
3
3
  module Loader
4
4
  DATA_PATH = File.expand_path("../../../../config", __FILE__)
5
5
 
6
- # Loads and returns the currencies stored in JSON files in the config directory.
7
- #
8
- # @return [Hash]
9
- def load_currencies
10
- currencies = parse_currency_file("currency_iso.json")
11
- currencies.merge! parse_currency_file("currency_non_iso.json")
12
- currencies.merge! parse_currency_file("currency_backwards_compatible.json")
13
- end
6
+ class << self
7
+ # Loads and returns the currencies stored in JSON files in the config directory.
8
+ #
9
+ # @return [Hash]
10
+ def load_currencies
11
+ currencies = parse_currency_file("currency_iso.json")
12
+ currencies.merge! parse_currency_file("currency_non_iso.json")
13
+ currencies.merge! parse_currency_file("currency_backwards_compatible.json")
14
+ end
14
15
 
15
- private
16
+ private
16
17
 
17
- def parse_currency_file(filename)
18
- json = File.read("#{DATA_PATH}/#{filename}")
19
- json.force_encoding(::Encoding::UTF_8) if defined?(::Encoding)
20
- JSON.parse(json, symbolize_names: true)
18
+ def parse_currency_file(filename)
19
+ json = File.read("#{DATA_PATH}/#{filename}")
20
+ json.force_encoding(::Encoding::UTF_8) if defined?(::Encoding)
21
+ JSON.parse(json, symbolize_names: true)
22
+ end
21
23
  end
22
24
  end
23
25
  end
@@ -0,0 +1,11 @@
1
+ require 'money/locale_backend/base'
2
+
3
+ class Money
4
+ module LocaleBackend
5
+ class Currency < Base
6
+ def lookup(key, currency)
7
+ currency.public_send(key) if currency.respond_to?(key)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -5,7 +5,8 @@ class Money
5
5
  class I18n < Base
6
6
  KEY_MAP = {
7
7
  thousands_separator: :delimiter,
8
- decimal_mark: :separator
8
+ decimal_mark: :separator,
9
+ symbol: :unit
9
10
  }.freeze
10
11
 
11
12
  def initialize
@@ -9,9 +9,9 @@ class Money
9
9
  end
10
10
 
11
11
  def lookup(key, currency)
12
- if Money.use_i18n
13
- warn '[DEPRECATION] `use_i18n` is deprecated - use `Money.locale_backend = :i18n` instead'
12
+ warn '[DEPRECATION] You are using the default localization behaviour that will change in the next major release. Find out more - https://github.com/RubyMoney/money#deprecation'
14
13
 
14
+ if Money.use_i18n
15
15
  i18n_backend.lookup(key, nil) || currency.public_send(key)
16
16
  else
17
17
  currency.public_send(key)
@@ -91,51 +91,63 @@ class Money
91
91
  class << self
92
92
 
93
93
  # @!attribute [rw] default_bank
94
- # @return [Money::Bank::Base] Each Money object is associated to a bank
95
- # object, which is responsible for currency exchange. This property
96
- # allows you to specify the default bank object. The default value for
97
- # this property is an instance of +Bank::VariableExchange.+ It allows
98
- # one to specify custom exchange rates.
94
+ # Used to set a default bank for currency exchange.
95
+ #
96
+ # Each Money object is associated with a bank
97
+ # object, which is responsible for currency exchange. This property
98
+ # allows you to specify the default bank object. The default value for
99
+ # this property is an instance of +Bank::VariableExchange.+ It allows
100
+ # one to specify custom exchange rates.
101
+ #
102
+ # @return [Money::Bank::Base]
99
103
  #
100
104
  # @!attribute default_formatting_rules
101
- # @return [Hash] Use this to define a default hash of rules for every time
102
- # +Money#format+ is called. Rules provided on method call will be
103
- # merged with the default ones. To overwrite a rule, just provide the
104
- # intended value while calling +format+.
105
+ # Used to define a default hash of rules for every time
106
+ # +Money#format+ is called. Rules provided on method call will be
107
+ # merged with the default ones. To overwrite a rule, just provide the
108
+ # intended value while calling +format+.
105
109
  #
106
- # @see +Money::Formatting#format+ for more details.
110
+ # @see Money::Formatter#initialize Money::Formatter for more details
107
111
  #
108
112
  # @example
109
113
  # Money.default_formatting_rules = { display_free: true }
110
114
  # Money.new(0, "USD").format # => "free"
111
115
  # Money.new(0, "USD").format(display_free: false) # => "$0.00"
112
116
  #
117
+ # @return [Hash]
118
+ #
113
119
  # @!attribute [rw] use_i18n
114
- # @return [Boolean] Use this to disable i18n even if it's used by other
115
- # objects in your app.
120
+ # Used to disable i18n even if it's used by other components of your app.
121
+ #
122
+ # @return [Boolean]
116
123
  #
117
124
  # @!attribute [rw] infinite_precision
118
- # @return [Boolean] Use this to enable infinite precision cents
125
+ # Used to enable infinite precision cents
126
+ #
127
+ # @return [Boolean]
119
128
  #
120
129
  # @!attribute [rw] conversion_precision
121
- # @return [Integer] Use this to specify precision for converting Rational
122
- # to BigDecimal
123
- attr_accessor :default_bank, :default_formatting_rules,
124
- :use_i18n, :infinite_precision, :conversion_precision,
125
- :locale_backend
126
-
127
- # @attr_writer rounding_mode Use this to specify the rounding mode
130
+ # Used to specify precision for converting Rational to BigDecimal
128
131
  #
129
- # @!attribute default_currency
130
- # @return [Money::Currency] The default currency, which is used when
131
- # +Money.new+ is called without an explicit currency argument. The
132
- # default value is Currency.new("USD"). The value must be a valid
133
- # +Money::Currency+ instance.
134
- attr_writer :rounding_mode, :default_currency
132
+ # @return [Integer]
133
+ attr_accessor :default_bank, :default_formatting_rules,
134
+ :infinite_precision, :conversion_precision
135
135
 
136
+ attr_reader :use_i18n, :locale_backend
136
137
  end
137
138
 
139
+ # @!attribute default_currency
140
+ # @return [Money::Currency] The default currency, which is used when
141
+ # +Money.new+ is called without an explicit currency argument. The
142
+ # default value is Currency.new("USD"). The value must be a valid
143
+ # +Money::Currency+ instance.
138
144
  def self.default_currency
145
+ if @using_deprecated_default_currency
146
+ warn '[WARNING] The default currency will change from `USD` to `nil` in the next major release. Make ' \
147
+ 'sure to set it explicitly using `Money.default_currency=` to avoid potential issues'
148
+ @using_deprecated_default_currency = false
149
+ end
150
+
139
151
  if @default_currency.respond_to?(:call)
140
152
  Money::Currency.new(@default_currency.call)
141
153
  else
@@ -143,13 +155,26 @@ class Money
143
155
  end
144
156
  end
145
157
 
158
+ def self.default_currency=(currency)
159
+ @using_deprecated_default_currency = false
160
+ @default_currency = currency
161
+ end
162
+
146
163
  def self.locale_backend=(value)
147
164
  @locale_backend = value ? LocaleBackend.find(value) : nil
148
165
  end
149
166
 
167
+ # @attr_writer rounding_mode Use this to specify the rounding mode
168
+ def self.rounding_mode=(new_rounding_mode)
169
+ @using_deprecated_default_rounding_mode = false
170
+ @rounding_mode = new_rounding_mode
171
+ end
172
+
150
173
  def self.use_i18n=(value)
151
174
  if value
152
- warn '[DEPRECATION] `use_i18n` is deprecated - use `Money.locale_backend = :i18n` instead'
175
+ warn '[DEPRECATION] `use_i18n` is deprecated - use `Money.locale_backend = :i18n` instead for locale based formatting'
176
+ else
177
+ warn '[DEPRECATION] `use_i18n` is deprecated - use `Money.locale_backend = :currency` instead for currency based formatting'
153
178
  end
154
179
 
155
180
  @use_i18n = value
@@ -161,6 +186,7 @@ class Money
161
186
 
162
187
  # Set the default currency for creating new +Money+ object.
163
188
  self.default_currency = Currency.new("USD")
189
+ @using_deprecated_default_currency = true
164
190
 
165
191
  # Default to using i18n
166
192
  @use_i18n = true
@@ -173,6 +199,7 @@ class Money
173
199
 
174
200
  # Default to bankers rounding
175
201
  self.rounding_mode = BigDecimal::ROUND_HALF_EVEN
202
+ @using_deprecated_default_rounding_mode = true
176
203
 
177
204
  # Default the conversion of Rationals precision to 16
178
205
  self.conversion_precision = 16
@@ -184,32 +211,48 @@ class Money
184
211
 
185
212
  setup_defaults
186
213
 
187
- # Use this to return the rounding mode. You may also pass a
188
- # rounding mode and a block to temporarily change it. It will
189
- # then return the results of the block instead.
214
+ # Use this to return the rounding mode.
190
215
  #
191
216
  # @param [BigDecimal::ROUND_MODE] mode
192
217
  #
193
- # @return [BigDecimal::ROUND_MODE,Yield] rounding mode or block results
218
+ # @return [BigDecimal::ROUND_MODE] rounding mode
219
+ def self.rounding_mode(mode = nil)
220
+ if mode
221
+ warn "[DEPRECATION] calling `rounding_mode` with a block is deprecated. Please use `.with_rounding_mode` instead."
222
+ return with_rounding_mode(mode) { yield }
223
+ end
224
+
225
+ return Thread.current[:money_rounding_mode] if Thread.current[:money_rounding_mode]
226
+
227
+ if @using_deprecated_default_rounding_mode
228
+ warn '[WARNING] The default rounding mode will change from `ROUND_HALF_EVEN` to `ROUND_HALF_UP` in the ' \
229
+ 'next major release. Set it explicitly using `Money.rounding_mode=` to avoid potential problems.'
230
+ @using_deprecated_default_rounding_mode = false
231
+ end
232
+
233
+ @rounding_mode
234
+ end
235
+
236
+ # Temporarily changes the rounding mode in a given block.
237
+ #
238
+ # @param [BigDecimal::ROUND_MODE] mode
239
+ #
240
+ # @yield The block within which rounding mode will be changed. Its return
241
+ # value will also be the return value of the whole method.
242
+ #
243
+ # @return [Object] block results
194
244
  #
195
245
  # @example
196
- # fee = Money.rounding_mode(BigDecimal::ROUND_HALF_UP) do
246
+ # fee = Money.with_rounding_mode(BigDecimal::ROUND_HALF_UP) do
197
247
  # Money.new(1200) * BigDecimal('0.029')
198
248
  # end
199
- def self.rounding_mode(mode = nil)
200
- if mode.nil?
201
- Thread.current[:money_rounding_mode] || @rounding_mode
202
- else
203
- begin
204
- Thread.current[:money_rounding_mode] = mode
205
- yield
206
- ensure
207
- Thread.current[:money_rounding_mode] = nil
208
- end
209
- end
249
+ def self.with_rounding_mode(mode)
250
+ Thread.current[:money_rounding_mode] = mode
251
+ yield
252
+ ensure
253
+ Thread.current[:money_rounding_mode] = nil
210
254
  end
211
255
 
212
-
213
256
  # Adds a new exchange rate to the default bank and return the rate.
214
257
  #
215
258
  # @param [Currency, String, Symbol] from_currency Currency to exchange from.
@@ -274,10 +317,13 @@ class Money
274
317
  # Money.new(100, "EUR") #=> #<Money @fractional=100 @currency="EUR">
275
318
  #
276
319
  def initialize(obj, currency = Money.default_currency, bank = Money.default_bank)
277
- @fractional = obj.respond_to?(:fractional) ? obj.fractional : as_d(obj)
320
+ @fractional = as_d(obj.respond_to?(:fractional) ? obj.fractional : obj)
278
321
  @currency = obj.respond_to?(:currency) ? obj.currency : Currency.wrap(currency)
279
322
  @currency ||= Money.default_currency
280
323
  @bank = obj.respond_to?(:bank) ? obj.bank : bank
324
+
325
+ # BigDecimal can be Infinity and NaN, money of that amount does not make sense
326
+ raise ArgumentError, 'must be initialized with a finite value' unless @fractional.finite?
281
327
  end
282
328
 
283
329
  # Assuming using a currency using dollars:
@@ -500,13 +546,15 @@ class Money
500
546
  exchange_to("EUR")
501
547
  end
502
548
 
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.
549
+ # Splits a given amount in parts without losing pennies. The left-over pennies will be
550
+ # distributed round-robin amongst the parties. This means that parts listed first will likely
551
+ # receive more pennies than ones listed later.
552
+ #
553
+ # Pass [2, 1, 1] as input to give twice as much to part1 as part2 or
554
+ # part3 which results in 50% of the cash to party1, 25% to part2, and 25% to part3. Passing a
555
+ # number instead of an array will split the amount evenly (without losing pennies when rounding).
506
556
  #
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).
557
+ # @param [Array<Numeric>, Numeric] parts how amount should be distributed to parts
510
558
  #
511
559
  # @return [Array<Money>]
512
560
  #
@@ -544,7 +592,7 @@ class Money
544
592
 
545
593
  # Creates a formatted price string according to several rules.
546
594
  #
547
- # @param [Hash] See Money::Formatter for the list of formatting options
595
+ # @param [Hash] rules See {Money::Formatter Money::Formatter} for the list of formatting options
548
596
  #
549
597
  # @return [String]
550
598
  #
@@ -2,9 +2,9 @@
2
2
 
3
3
  class Money
4
4
  class Allocation
5
- # Splits a given amount in parts without loosing pennies.
6
- # The left-over pennies will be distributed round-robin amongst the parties. This means that
7
- # parties listed first will likely receive more pennies than ones that are listed later.
5
+ # Splits a given amount in parts without losing pennies.
6
+ # The left-over pennies will be distributed round-robin amongst the parts. This means that
7
+ # parts listed first will likely receive more pennies than the ones listed later.
8
8
  #
9
9
  # The results should always add up to the original amount.
10
10
  #
@@ -24,8 +24,11 @@ class Money
24
24
  parts_sum = parts.inject(0, :+)
25
25
  part = parts.pop
26
26
 
27
- current_split = remaining_amount * part / parts_sum
28
- current_split = current_split.truncate if whole_amounts
27
+ current_split = 0
28
+ if parts_sum > 0
29
+ current_split = remaining_amount * part / parts_sum
30
+ current_split = current_split.truncate if whole_amounts
31
+ end
29
32
 
30
33
  result.unshift current_split
31
34
  remaining_amount -= current_split
@@ -46,7 +46,7 @@ class Money
46
46
  # Compares two Money objects. If money objects have a different currency it
47
47
  # will attempt to convert the currency.
48
48
  #
49
- # @param [Money] other_money Value to compare with.
49
+ # @param [Money] other Value to compare with.
50
50
  #
51
51
  # @return [Integer]
52
52
  #
@@ -103,7 +103,7 @@ class Money
103
103
  # values. If +other_money+ has a different currency then its monetary value
104
104
  # is automatically exchanged to this object's currency using +exchange_to+.
105
105
  #
106
- # @param [Money] other_money Other +Money+ object to add.
106
+ # @param [Money] other Other +Money+ object to add.
107
107
  #
108
108
  # @return [Money]
109
109
  #
@@ -116,22 +116,32 @@ class Money
116
116
  # its monetary value is automatically exchanged to this object's currency
117
117
  # using +exchange_to+.
118
118
  #
119
- # @param [Money] other_money Other +Money+ object to subtract.
119
+ # @param [Money] other Other +Money+ object to subtract.
120
120
  #
121
121
  # @return [Money]
122
122
  #
123
123
  # @example
124
124
  # Money.new(100) - Money.new(99) #=> #<Money @fractional=1>
125
125
  [:+, :-].each do |op|
126
+ non_zero_message = lambda do |value|
127
+ "Can't add or subtract a non-zero #{value.class.name} value"
128
+ end
129
+
126
130
  define_method(op) do |other|
127
- unless other.is_a?(Money)
128
- if other.zero?
129
- return other.is_a?(CoercedNumeric) ? Money.empty(currency).public_send(op, self) : self
130
- end
131
- raise TypeError
131
+ case other
132
+ when Money
133
+ other = other.exchange_to(currency)
134
+ new_fractional = fractional.public_send(op, other.fractional)
135
+ self.class.new(new_fractional, currency, bank)
136
+ when CoercedNumeric
137
+ raise TypeError, non_zero_message.call(other.value) unless other.zero?
138
+ self.class.new(other.value.public_send(op, fractional), currency)
139
+ when Numeric
140
+ raise TypeError, non_zero_message.call(other) unless other.zero?
141
+ self
142
+ else
143
+ raise TypeError, "Unsupported argument type: #{other.class.name}"
132
144
  end
133
- other = other.exchange_to(currency)
134
- self.class.new(fractional.public_send(op, other.fractional), currency, bank)
135
145
  end
136
146
  end
137
147
 
@@ -204,6 +204,9 @@ class Money
204
204
  # # CAD: "CAD$"
205
205
  # Money.new(10000, "CAD").format(translate: true) #=> "CAD$100.00"
206
206
  #
207
+ # @option rules [Boolean] :drop_trailing_zeros (false) Ignore trailing zeros after
208
+ # the decimal mark
209
+ #
207
210
  # @example
208
211
  # Money.new(89000, :btc).format(drop_trailing_zeros: true) #=> B⃦0.00089
209
212
  # Money.new(110, :usd).format(drop_trailing_zeros: true) #=> $1.1
@@ -38,14 +38,22 @@ class Money
38
38
  rules = {}
39
39
  elsif rules.size == 1
40
40
  rules = rules.pop
41
- rules = { rules => true } if rules.is_a?(Symbol)
41
+ rules = rules.dup if rules.is_a?(Hash)
42
+
43
+ if rules.is_a?(Symbol)
44
+ warn '[DEPRECATION] Use Hash when passing rules to Money#format.'
45
+ rules = { rules => true }
46
+ end
42
47
  end
48
+
43
49
  if !rules.include?(:decimal_mark) && rules.include?(:separator)
44
50
  rules[:decimal_mark] = rules[:separator]
45
51
  end
52
+
46
53
  if !rules.include?(:thousands_separator) && rules.include?(:delimiter)
47
54
  rules[:thousands_separator] = rules[:delimiter]
48
55
  end
56
+
49
57
  rules
50
58
  end
51
59
 
@@ -96,15 +104,18 @@ class Money
96
104
 
97
105
  def warn_about_deprecated_rules(rules)
98
106
  if rules.has_key?(:symbol_position)
99
- warn '[DEPRECATION] `symbol_position:` option is deprecated - use `format` to specify the formatting template.'
107
+ position = rules[:symbol_position]
108
+ template = position == :before ? '%u %n' : '%n %u'
109
+
110
+ warn "[DEPRECATION] `symbol_position: :#{position}` is deprecated - you can replace it with `format: #{template}`"
100
111
  end
101
112
 
102
113
  if rules.has_key?(:symbol_before_without_space)
103
- warn '[DEPRECATION] `symbol_before_without_space:` option is deprecated - use `format` to specify the formatting template.'
114
+ warn "[DEPRECATION] `symbol_before_without_space:` option is deprecated - you can replace it with `format: '%u%n'`"
104
115
  end
105
116
 
106
117
  if rules.has_key?(:symbol_after_without_space)
107
- warn '[DEPRECATION] `symbol_after_without_space:` option is deprecated - use `format` to specify the formatting template.'
118
+ warn "[DEPRECATION] `symbol_after_without_space:` option is deprecated - you can replace it with `format: '%n%u'`"
108
119
  end
109
120
 
110
121
  if rules.has_key?(:html)
@@ -114,7 +125,6 @@ class Money
114
125
  if rules.has_key?(:html_wrap_symbol)
115
126
  warn "[DEPRECATION] `html_wrap_symbol` is deprecated - use `html_wrap` instead. Please note that `html_wrap` will wrap all parts of currency."
116
127
  end
117
-
118
128
  end
119
129
  end
120
130
  end