money 6.19.0 → 7.0.1

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.
@@ -1,4 +1,4 @@
1
- # encoding: UTF-8
1
+ # frozen_string_literal: true
2
2
 
3
3
  class Money
4
4
  class FormattingRules
@@ -9,12 +9,9 @@ class Money
9
9
  @rules = normalize_formatting_rules(raw_rules)
10
10
 
11
11
  @rules = default_formatting_rules.merge(@rules) unless @rules[:ignore_defaults]
12
- @rules = localize_formatting_rules(@rules)
13
12
  @rules = translate_formatting_rules(@rules) if @rules[:translate]
14
- @rules[:format] ||= determine_format_from_formatting_rules(@rules)
13
+ @rules[:format] ||= determine_format
15
14
  @rules[:delimiter_pattern] ||= delimiter_pattern_rule(@rules)
16
-
17
- warn_about_deprecated_rules(@rules)
18
15
  end
19
16
 
20
17
  def [](key)
@@ -71,72 +68,25 @@ class Money
71
68
  rules
72
69
  end
73
70
 
74
- def localize_formatting_rules(rules)
75
- if currency.iso_code == "JPY" && I18n.locale == :ja
76
- rules[:symbol] = "円" unless rules[:symbol] == false
77
- rules[:format] = '%n%u'
78
- end
79
- rules
71
+ def determine_format
72
+ Money.locale_backend&.lookup(:format, @currency) || default_format
80
73
  end
81
74
 
82
- def determine_format_from_formatting_rules(rules)
83
- return currency.format if currency.format && !rules.has_key?(:symbol_position)
84
-
85
- symbol_position = symbol_position_from(rules)
86
-
87
- if symbol_position == :before
88
- rules.fetch(:symbol_before_without_space, true) ? '%u%n' : '%u %n'
75
+ def default_format
76
+ if currency.format
77
+ currency.format
89
78
  else
90
- rules[:symbol_after_without_space] ? '%n%u' : '%n %u'
79
+ currency.symbol_first? ? "%u%n" : "%n %u"
91
80
  end
92
81
  end
93
82
 
94
83
  def delimiter_pattern_rule(rules)
95
84
  if rules[:south_asian_number_formatting]
96
- # from http://blog.revathskumar.com/2014/11/regex-comma-seperated-indian-currency-format.html
85
+ # from https://blog.revathskumar.com/2014/11/regex-comma-seperated-indian-currency-format.html
97
86
  /(\d+?)(?=(\d\d)+(\d)(?!\d))(\.\d+)?/
98
87
  else
99
88
  /(\d)(?=(?:\d{3})+(?:[^\d]{1}|$))/
100
89
  end
101
90
  end
102
-
103
- def symbol_position_from(rules)
104
- if rules.has_key?(:symbol_position)
105
- if [:before, :after].include?(rules[:symbol_position])
106
- return rules[:symbol_position]
107
- else
108
- raise ArgumentError, ":symbol_position must be ':before' or ':after'"
109
- end
110
- elsif currency.symbol_first?
111
- :before
112
- else
113
- :after
114
- end
115
- end
116
-
117
- def warn_about_deprecated_rules(rules)
118
- if rules.has_key?(:symbol_position)
119
- position = rules[:symbol_position]
120
- template = position == :before ? '%u%n' : '%n%u'
121
-
122
- warn "[DEPRECATION] `symbol_position: :#{position}` is deprecated - you can replace it with `format: #{template}`"
123
- end
124
-
125
- if rules.has_key?(:symbol_before_without_space)
126
- warn "[DEPRECATION] `symbol_before_without_space:` option is deprecated - you can replace it with `format: '%u%n'`"
127
- end
128
-
129
- if rules.has_key?(:symbol_after_without_space)
130
- warn "[DEPRECATION] `symbol_after_without_space:` option is deprecated - you can replace it with `format: '%n%u'`"
131
- end
132
-
133
- if rules.has_key?(:html)
134
- warn "[DEPRECATION] `html` is deprecated - use `html_wrap` instead. Please note that `html_wrap` will wrap all parts of currency and if you use `with_currency` option, currency element class changes from `currency` to `money-currency`."
135
- end
136
-
137
- if rules.has_key?(:html_wrap_symbol)
138
- warn "[DEPRECATION] `html_wrap_symbol` is deprecated - use `html_wrap` instead. Please note that `html_wrap` will wrap all parts of currency."
139
- end
140
- end
141
91
  end
142
92
  end
@@ -1,14 +1,12 @@
1
- # encoding: UTF-8
1
+ # frozen_string_literal: true
2
2
 
3
3
  require 'money/locale_backend/errors'
4
- require 'money/locale_backend/legacy'
5
4
  require 'money/locale_backend/i18n'
6
5
  require 'money/locale_backend/currency'
7
6
 
8
7
  class Money
9
8
  module LocaleBackend
10
9
  BACKENDS = {
11
- legacy: Money::LocaleBackend::Legacy,
12
10
  i18n: Money::LocaleBackend::I18n,
13
11
  currency: Money::LocaleBackend::Currency
14
12
  }.freeze
data/lib/money/money.rb CHANGED
@@ -1,4 +1,5 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
+
2
3
  require "money/bank/variable_exchange"
3
4
  require "money/bank/single_currency"
4
5
  require "money/money/arithmetic"
@@ -15,7 +16,7 @@ require "money/money/locale_backend"
15
16
  #
16
17
  # Money is a value object and should be treated as immutable.
17
18
  #
18
- # @see http://en.wikipedia.org/wiki/Money
19
+ # @see https://en.wikipedia.org/wiki/Money
19
20
  class Money
20
21
  include Comparable
21
22
  include Money::Arithmetic
@@ -68,15 +69,31 @@ class Money
68
69
  #
69
70
  # @see infinite_precision
70
71
  def round_to_nearest_cash_value
72
+ warn "[DEPRECATION] `round_to_nearest_cash_value` is deprecated - use " \
73
+ "`to_nearest_cash_value.fractional` instead"
74
+
75
+ to_nearest_cash_value.fractional
76
+ end
77
+
78
+ # Round a given amount of money to the nearest possible money in cash value.
79
+ # For example, in Swiss franc (CHF), the smallest possible amount of cash
80
+ # value is CHF 0.05. Therefore, this method rounds CHF 0.07 to CHF 0.05, and
81
+ # CHF 0.08 to CHF 0.10.
82
+ #
83
+ # @return [Money]
84
+ def to_nearest_cash_value
71
85
  unless self.currency.smallest_denomination
72
- raise UndefinedSmallestDenomination, 'Smallest denomination of this currency is not defined'
86
+ raise UndefinedSmallestDenomination,
87
+ "Smallest denomination of this currency is not defined"
73
88
  end
74
89
 
75
90
  fractional = as_d(@fractional)
76
91
  smallest_denomination = as_d(self.currency.smallest_denomination)
77
- rounded_value = (fractional / smallest_denomination).round(0, self.class.rounding_mode) * smallest_denomination
92
+ rounded_value =
93
+ (fractional / smallest_denomination)
94
+ .round(0, self.class.rounding_mode) * smallest_denomination
78
95
 
79
- return_value(rounded_value)
96
+ dup_with(fractional: return_value(rounded_value))
80
97
  end
81
98
 
82
99
  # @!attribute [r] currency
@@ -116,11 +133,6 @@ class Money
116
133
  #
117
134
  # @return [Hash]
118
135
  #
119
- # @!attribute [rw] use_i18n
120
- # Used to disable i18n even if it's used by other components of your app.
121
- #
122
- # @return [Boolean]
123
- #
124
136
  # @!attribute [rw] default_infinite_precision
125
137
  # @return [Boolean] Use this to enable infinite precision cents as the
126
138
  # global default
@@ -129,34 +141,38 @@ class Money
129
141
  # Used to specify precision for converting Rational to BigDecimal
130
142
  #
131
143
  # @return [Integer]
132
- attr_accessor :default_formatting_rules, :default_infinite_precision, :conversion_precision
133
- attr_reader :use_i18n, :locale_backend
144
+ #
145
+ # @!attribute [rw] strict_eql_compare
146
+ # Use this to specify how +Money#eql?+ behaves. Opt-in to the new
147
+ # behavior by setting this to +true+ and disable warnings when comparing
148
+ # zero amounts with different currencies.
149
+ #
150
+ # @example
151
+ # Money.strict_eql_compare = false # (default)
152
+ # Money.new(0, "USD").eql?(Money.new(0, "EUR")) # => true
153
+ # # => [DEPRECATION] warning
154
+ #
155
+ # Money.strict_eql_compare = true
156
+ # Money.new(0, "USD").eql?(Money.new(0, "EUR")) # => false
157
+ #
158
+ # @return [Boolean]
159
+ #
160
+ # @see Money#eql
161
+ attr_accessor :default_formatting_rules,
162
+ :default_infinite_precision,
163
+ :conversion_precision,
164
+ :strict_eql_compare
165
+ attr_reader :locale_backend
134
166
  attr_writer :default_bank
135
-
136
- def infinite_precision
137
- warn '[DEPRECATION] `Money.infinite_precision` is deprecated - use `Money.default_infinite_precision` instead'
138
- default_infinite_precision
139
- end
140
-
141
- def infinite_precision=(value)
142
- warn '[DEPRECATION] `Money.infinite_precision=` is deprecated - use `Money.default_infinite_precision= ` instead'
143
- self.default_infinite_precision = value
144
- end
145
167
  end
146
168
 
147
169
  # @!attribute default_currency
148
170
  # @return [Money::Currency] The default currency, which is used when
149
- # +Money.new+ is called without an explicit currency argument. The
150
- # default value is Currency.new("USD"). The value must be a valid
151
- # +Money::Currency+ instance.
171
+ # +Money.new+ is called without an explicit currency argument.
152
172
  def self.default_currency
153
- if @using_deprecated_default_currency
154
- warn '[WARNING] The default currency will change from `USD` to `nil` in the next major release. Make ' \
155
- 'sure to set it explicitly using `Money.default_currency=` to avoid potential issues'
156
- @using_deprecated_default_currency = false
157
- end
158
-
159
- if @default_currency.respond_to?(:call)
173
+ if @default_currency.nil?
174
+ nil
175
+ elsif @default_currency.respond_to?(:call)
160
176
  Money::Currency.new(@default_currency.call)
161
177
  else
162
178
  Money::Currency.new(@default_currency)
@@ -164,11 +180,14 @@ class Money
164
180
  end
165
181
 
166
182
  def self.default_currency=(currency)
167
- @using_deprecated_default_currency = false
168
183
  @default_currency = currency
169
184
  end
170
185
 
186
+ # Modified to support thread-local bank override
171
187
  def self.default_bank
188
+ # Check for thread-local bank first, then fall back to global default
189
+ return Thread.current[:money_bank] if Thread.current[:money_bank]
190
+
172
191
  if @default_bank.respond_to?(:call)
173
192
  @default_bank.call
174
193
  else
@@ -176,49 +195,53 @@ class Money
176
195
  end
177
196
  end
178
197
 
198
+ # Thread-safe bank switching method
199
+ # Temporarily changes the default bank in the current thread only
200
+ #
201
+ # @param [Money::Bank::Base] bank The bank to use within the block
202
+ # @yield The block within which the bank will be changed
203
+ # @return [Object] block results
204
+ #
205
+ # @example
206
+ # Money.with_bank(european_bank) do
207
+ # Money.new(100, "USD").exchange_to("EUR")
208
+ # end
209
+ def self.with_bank(bank)
210
+ original_bank = Thread.current[:money_bank]
211
+ Thread.current[:money_bank] = bank
212
+ yield
213
+ ensure
214
+ Thread.current[:money_bank] = original_bank
215
+ end
216
+
179
217
  def self.locale_backend=(value)
180
218
  @locale_backend = value ? LocaleBackend.find(value) : nil
181
219
  end
182
220
 
183
221
  # @attr_writer rounding_mode Use this to specify the rounding mode
184
222
  def self.rounding_mode=(new_rounding_mode)
185
- @using_deprecated_default_rounding_mode = false
186
223
  @rounding_mode = new_rounding_mode
187
224
  end
188
225
 
189
- def self.use_i18n=(value)
190
- if value
191
- warn '[DEPRECATION] `use_i18n` is deprecated - use `Money.locale_backend = :i18n` instead for locale based formatting'
192
- else
193
- warn '[DEPRECATION] `use_i18n` is deprecated - use `Money.locale_backend = :currency` instead for currency based formatting'
194
- end
195
-
196
- @use_i18n = value
197
- end
198
-
199
226
  def self.setup_defaults
200
227
  # Set the default bank for creating new +Money+ objects.
201
228
  self.default_bank = Bank::VariableExchange.instance
202
229
 
203
- # Set the default currency for creating new +Money+ object.
204
- self.default_currency = Currency.new("USD")
205
- @using_deprecated_default_currency = true
206
-
207
- # Default to using i18n
208
- @use_i18n = true
209
-
210
- # Default to using legacy locale backend
211
- self.locale_backend = :legacy
230
+ # Default to using currency backend
231
+ self.locale_backend = :currency
212
232
 
213
233
  # Default to not using infinite precision cents
214
234
  self.default_infinite_precision = false
215
235
 
216
- # Default to bankers rounding
217
- self.rounding_mode = BigDecimal::ROUND_HALF_EVEN
218
- @using_deprecated_default_rounding_mode = true
236
+ # Default rounding mode toward the nearest neighbor; if the neighbors are equidistant, round away from zero
237
+ self.rounding_mode = BigDecimal::ROUND_HALF_UP
219
238
 
220
239
  # Default the conversion of Rationals precision to 16
221
240
  self.conversion_precision = 16
241
+
242
+ # Defaults to the deprecated behavior where
243
+ # `Money.new(0, "USD").eql?(Money.new(0, "EUR"))` is true.
244
+ self.strict_eql_compare = false
222
245
  end
223
246
 
224
247
  def self.inherited(base)
@@ -229,23 +252,10 @@ class Money
229
252
 
230
253
  # Use this to return the rounding mode.
231
254
  #
232
- # @param [BigDecimal::ROUND_MODE] mode
233
- #
234
255
  # @return [BigDecimal::ROUND_MODE] rounding mode
235
- def self.rounding_mode(mode = nil)
236
- if mode
237
- warn "[DEPRECATION] calling `rounding_mode` with a block is deprecated. Please use `.with_rounding_mode` instead."
238
- return with_rounding_mode(mode) { yield }
239
- end
240
-
256
+ def self.rounding_mode
241
257
  return Thread.current[:money_rounding_mode] if Thread.current[:money_rounding_mode]
242
258
 
243
- if @using_deprecated_default_rounding_mode
244
- warn '[WARNING] The default rounding mode will change from `ROUND_HALF_EVEN` to `ROUND_HALF_UP` in the ' \
245
- 'next major release. Set it explicitly using `Money.rounding_mode=` to avoid potential problems.'
246
- @using_deprecated_default_rounding_mode = false
247
- end
248
-
249
259
  @rounding_mode
250
260
  end
251
261
 
@@ -259,14 +269,15 @@ class Money
259
269
  # @return [Object] block results
260
270
  #
261
271
  # @example
262
- # fee = Money.with_rounding_mode(BigDecimal::ROUND_HALF_UP) do
272
+ # fee = Money.with_rounding_mode(BigDecimal::ROUND_HALF_DOWN) do
263
273
  # Money.new(1200) * BigDecimal('0.029')
264
274
  # end
265
275
  def self.with_rounding_mode(mode)
276
+ original_mode = Thread.current[:money_rounding_mode]
266
277
  Thread.current[:money_rounding_mode] = mode
267
278
  yield
268
279
  ensure
269
- Thread.current[:money_rounding_mode] = nil
280
+ Thread.current[:money_rounding_mode] = original_mode
270
281
  end
271
282
 
272
283
  # Adds a new exchange rate to the default bank and return the rate.
@@ -308,13 +319,24 @@ class Money
308
319
  raise ArgumentError, "'amount' must be numeric" unless Numeric === amount
309
320
 
310
321
  currency = Currency.wrap(currency) || Money.default_currency
322
+ raise Currency::NoCurrency, 'must provide a currency' if currency.nil?
323
+
311
324
  value = amount.to_d * currency.subunit_to_unit
312
325
  new(value, currency, options)
313
326
  end
314
327
 
328
+ # DEPRECATED.
329
+ #
330
+ # @see Money.from_amount
331
+ def self.from_dollars(amount, currency = default_currency, options = {})
332
+ warn "[DEPRECATION] `Money.from_dollars` is deprecated in favor of " \
333
+ "`Money.from_amount`."
334
+
335
+ from_amount(amount, currency, options)
336
+ end
337
+
315
338
  class << self
316
339
  alias_method :from_cents, :new
317
- alias_method :from_dollars, :from_amount
318
340
  end
319
341
 
320
342
  # Creates a new Money object of value given in the
@@ -338,8 +360,8 @@ class Money
338
360
  # Money.new(100, "USD") #=> #<Money @fractional=100 @currency="USD">
339
361
  # Money.new(100, "EUR") #=> #<Money @fractional=100 @currency="EUR">
340
362
  #
341
- def initialize( obj, currency = Money.default_currency, options = {})
342
- # For backwards compatability, if options is not a Hash, treat it as a bank parameter
363
+ def initialize(obj, currency = nil, options = {})
364
+ # For backwards compatibility, if options is not a Hash, treat it as a bank parameter
343
365
  unless options.is_a?(Hash)
344
366
  options = { bank: options }
345
367
  end
@@ -352,66 +374,32 @@ class Money
352
374
 
353
375
  # BigDecimal can be Infinity and NaN, money of that amount does not make sense
354
376
  raise ArgumentError, 'must be initialized with a finite value' unless @fractional.finite?
377
+ raise Currency::NoCurrency, 'must provide a currency' if @currency.nil?
355
378
  end
356
379
 
357
- # Assuming using a currency using dollars:
358
- # Returns the value of the money in dollars,
359
- # instead of in the fractional unit cents.
360
- #
361
- # Synonym of #amount
362
- #
363
- # @return [BigDecimal]
364
- #
365
- # @example
366
- # Money.new(1_00, "USD").dollars # => BigDecimal("1.00")
380
+ # DEPRECATED.
367
381
  #
368
382
  # @see #amount
369
- # @see #to_d
370
- # @see #cents
371
- #
372
383
  def dollars
384
+ warn "[DEPRECATION] `Money#dollars` is deprecated in favor of " \
385
+ "`Money#amount`."
386
+
373
387
  amount
374
388
  end
375
389
 
376
- # Returns the numerical value of the money
390
+ # Returns the numerical value of the money.
377
391
  #
378
392
  # @return [BigDecimal]
379
393
  #
380
394
  # @example
381
- # Money.new(1_00, "USD").amount # => BigDecimal("1.00")
395
+ # Money.new(1_00, "USD").amount # => BigDecimal("1.00")
382
396
  #
383
397
  # @see #to_d
384
398
  # @see #fractional
385
- #
386
399
  def amount
387
400
  to_d
388
401
  end
389
402
 
390
- # Return string representation of currency object
391
- #
392
- # @return [String]
393
- #
394
- # @example
395
- # Money.new(100, :USD).currency_as_string #=> "USD"
396
- def currency_as_string
397
- warn "[DEPRECATION] `currency_as_string` is deprecated. Please use `.currency.to_s` instead."
398
- currency.to_s
399
- end
400
-
401
- # Set currency object using a string
402
- #
403
- # @param [String] val The currency string.
404
- #
405
- # @return [Money::Currency]
406
- #
407
- # @example
408
- # Money.new(100).currency_as_string("CAD") #=> #<Money::Currency id: cad>
409
- def currency_as_string=(val)
410
- warn "[DEPRECATION] `currency_as_string=` is deprecated - Money instances are immutable." \
411
- " Please use `with_currency` instead."
412
- @currency = Currency.wrap(val)
413
- end
414
-
415
403
  # Returns a Integer hash value based on the +fractional+ and +currency+ attributes
416
404
  # in order to use functions like & (intersection), group_by, etc.
417
405
  #
@@ -610,9 +598,7 @@ class Money
610
598
  # @example
611
599
  # Money.new(10.1, 'USD').round #=> Money.new(10, 'USD')
612
600
  #
613
- # @see
614
- # Money.default_infinite_precision
615
- #
601
+ # @see Money.default_infinite_precision
616
602
  def round(rounding_mode = self.class.rounding_mode, rounding_precision = 0)
617
603
  rounded_amount = as_d(@fractional).round(rounding_precision, rounding_mode)
618
604
  dup_with(fractional: rounded_amount)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'monitor'
2
4
 
3
5
  class Money
data/lib/money/version.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Money
2
- VERSION = '6.19.0'
4
+ VERSION = '7.0.1'.freeze
3
5
  end
data/lib/money.rb CHANGED
@@ -1,6 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bigdecimal"
2
4
  require "bigdecimal/util"
3
5
  require "set"
4
6
  require "i18n"
5
7
  require "money/currency"
6
8
  require "money/money"
9
+ require "money/version"
data/money.gemspec CHANGED
@@ -1,4 +1,5 @@
1
- # -*- encoding: utf-8 -*-
1
+ # frozen_string_literal: true
2
+
2
3
  lib = File.expand_path('../lib', __FILE__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require "money/version"
@@ -14,20 +15,26 @@ Gem::Specification.new do |s|
14
15
  s.description = "A Ruby Library for dealing with money and currency conversion."
15
16
  s.license = "MIT"
16
17
 
17
- s.add_dependency 'i18n', [">= 0.6.4", '<= 2']
18
+ s.add_dependency "bigdecimal"
19
+ s.add_dependency 'i18n', "~> 1.9"
18
20
 
19
21
  s.add_development_dependency "bundler"
20
22
  s.add_development_dependency "rake"
21
23
  s.add_development_dependency "rspec", "~> 3.4"
22
- s.add_development_dependency "yard", "~> 0.9.11"
23
- s.add_development_dependency "kramdown", "~> 2.3"
24
+ # Documentation
25
+ s.add_development_dependency "yard", "~> 0.9.38"
26
+ s.add_development_dependency "rdoc"
27
+ s.add_development_dependency "redcarpet" unless RUBY_PLATFORM == "java"
28
+
29
+ s.required_ruby_version = '>= 3.1'
24
30
 
25
31
  s.files = `git ls-files -z -- config/* lib/* CHANGELOG.md LICENSE money.gemspec README.md`.split("\x0")
26
32
  s.require_paths = ["lib"]
27
33
 
28
34
  if s.respond_to?(:metadata)
29
- s.metadata['changelog_uri'] = 'https://github.com/RubyMoney/money/blob/master/CHANGELOG.md'
35
+ s.metadata['changelog_uri'] = 'https://github.com/RubyMoney/money/blob/main/CHANGELOG.md'
30
36
  s.metadata['source_code_uri'] = 'https://github.com/RubyMoney/money/'
31
37
  s.metadata['bug_tracker_uri'] = 'https://github.com/RubyMoney/money/issues'
38
+ s.metadata['rubygems_mfa_required'] = 'true'
32
39
  end
33
40
  end