money 6.13.0 → 6.13.8

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 +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