money 6.7.0 → 6.13.0

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 +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
@@ -0,0 +1,120 @@
1
+ # encoding: UTF-8
2
+
3
+ class Money
4
+ class FormattingRules
5
+ def initialize(currency, *raw_rules)
6
+ @currency = currency
7
+
8
+ # support for old format parameters
9
+ @rules = normalize_formatting_rules(raw_rules)
10
+
11
+ @rules = default_formatting_rules.merge(@rules) unless @rules[:ignore_defaults]
12
+ @rules = localize_formatting_rules(@rules)
13
+ @rules = translate_formatting_rules(@rules) if @rules[:translate]
14
+ @rules[:format] ||= determine_format_from_formatting_rules(@rules)
15
+
16
+ warn_about_deprecated_rules(@rules)
17
+ end
18
+
19
+ def [](key)
20
+ @rules[key]
21
+ end
22
+
23
+ def has_key?(key)
24
+ @rules.has_key? key
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :currency
30
+
31
+ # Cleans up formatting rules.
32
+ #
33
+ # @param [Hash] rules
34
+ #
35
+ # @return [Hash]
36
+ def normalize_formatting_rules(rules)
37
+ if rules.size == 0
38
+ rules = {}
39
+ elsif rules.size == 1
40
+ rules = rules.pop
41
+ rules = { rules => true } if rules.is_a?(Symbol)
42
+ end
43
+ if !rules.include?(:decimal_mark) && rules.include?(:separator)
44
+ rules[:decimal_mark] = rules[:separator]
45
+ end
46
+ if !rules.include?(:thousands_separator) && rules.include?(:delimiter)
47
+ rules[:thousands_separator] = rules[:delimiter]
48
+ end
49
+ rules
50
+ end
51
+
52
+ def default_formatting_rules
53
+ Money.default_formatting_rules || {}
54
+ end
55
+
56
+ def translate_formatting_rules(rules)
57
+ begin
58
+ rules[:symbol] = I18n.t currency.iso_code, scope: "number.currency.symbol", raise: true
59
+ rescue I18n::MissingTranslationData
60
+ # Do nothing
61
+ end
62
+ rules
63
+ end
64
+
65
+ def localize_formatting_rules(rules)
66
+ if currency.iso_code == "JPY" && I18n.locale == :ja
67
+ rules[:symbol] = "円" unless rules[:symbol] == false
68
+ rules[:format] = '%n%u'
69
+ end
70
+ rules
71
+ end
72
+
73
+ def determine_format_from_formatting_rules(rules)
74
+ symbol_position = symbol_position_from(rules)
75
+
76
+ if symbol_position == :before
77
+ rules.fetch(:symbol_before_without_space, true) ? '%u%n' : '%u %n'
78
+ else
79
+ rules[:symbol_after_without_space] ? '%n%u' : '%n %u'
80
+ end
81
+ end
82
+
83
+ def symbol_position_from(rules)
84
+ if rules.has_key?(:symbol_position)
85
+ if [:before, :after].include?(rules[:symbol_position])
86
+ return rules[:symbol_position]
87
+ else
88
+ raise ArgumentError, ":symbol_position must be ':before' or ':after'"
89
+ end
90
+ elsif currency.symbol_first?
91
+ :before
92
+ else
93
+ :after
94
+ end
95
+ end
96
+
97
+ def warn_about_deprecated_rules(rules)
98
+ if rules.has_key?(:symbol_position)
99
+ warn '[DEPRECATION] `symbol_position:` option is deprecated - use `format` to specify the formatting template.'
100
+ end
101
+
102
+ 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.'
104
+ end
105
+
106
+ 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.'
108
+ end
109
+
110
+ if rules.has_key?(:html)
111
+ 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`."
112
+ end
113
+
114
+ if rules.has_key?(:html_wrap_symbol)
115
+ warn "[DEPRECATION] `html_wrap_symbol` is deprecated - use `html_wrap` instead. Please note that `html_wrap` will wrap all parts of currency."
116
+ end
117
+
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,20 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'money/locale_backend/errors'
4
+ require 'money/locale_backend/legacy'
5
+ require 'money/locale_backend/i18n'
6
+
7
+ class Money
8
+ module LocaleBackend
9
+ BACKENDS = {
10
+ legacy: Money::LocaleBackend::Legacy,
11
+ i18n: Money::LocaleBackend::I18n
12
+ }.freeze
13
+
14
+ def self.find(name)
15
+ raise Unknown, "Unknown locale backend: #{name}" unless BACKENDS.key?(name)
16
+
17
+ BACKENDS[name].new
18
+ end
19
+ end
20
+ end
@@ -58,10 +58,9 @@ class Money
58
58
  end
59
59
 
60
60
  def marshal_dump
61
- [self.class, index, options]
61
+ [self.class, options, index]
62
62
  end
63
63
 
64
-
65
64
  # Wraps block execution in a thread-safe transaction
66
65
  def transaction(&block)
67
66
  if @in_transaction || options[:without_mutex]
@@ -1,3 +1,3 @@
1
1
  class Money
2
- VERSION = "6.7.0"
2
+ VERSION = '6.13.0'
3
3
  end
@@ -9,33 +9,27 @@ Gem::Specification.new do |s|
9
9
  s.platform = Gem::Platform::RUBY
10
10
  s.authors = ["Shane Emmons"]
11
11
  s.email = ["shane@emmons.io"]
12
- s.homepage = "http://rubymoney.github.io/money"
12
+ s.homepage = "https://rubymoney.github.io/money"
13
13
  s.summary = "A Ruby Library for dealing with money and currency conversion."
14
14
  s.description = "A Ruby Library for dealing with money and currency conversion."
15
15
  s.license = "MIT"
16
16
 
17
- s.post_install_message = <<MSG
18
- Please note the following API changes in Money version 6
19
-
20
- - Money#amount, Money#dollars methods now return instances of BigDecimal (rather than Float).
21
-
22
- Please read the migration notes at https://github.com/RubyMoney/money#migration-notes
23
- and choose the migration that best suits your application.
24
-
25
- Test responsibly :-)
26
- MSG
27
-
28
- s.add_dependency 'i18n', ['>= 0.6.4', '<= 0.7.0']
29
- s.add_dependency 'sixarm_ruby_unaccent', ['>= 1.1.1', '< 2']
17
+ s.add_dependency 'i18n', [">= 0.6.4", '<= 2']
30
18
 
31
19
  s.add_development_dependency "bundler", "~> 1.3"
32
20
  s.add_development_dependency "rake"
33
- s.add_development_dependency "rspec", "~> 3.4.0"
34
- s.add_development_dependency "yard", "~> 0.8"
21
+ s.add_development_dependency "rspec", "~> 3.4"
22
+ s.add_development_dependency "yard", "~> 0.9.11"
35
23
  s.add_development_dependency "kramdown", "~> 1.1"
36
24
 
37
25
  s.files = `git ls-files`.split($/)
38
26
  s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
39
27
  s.test_files = s.files.grep(%r{^(test|spec|features)/})
40
28
  s.require_paths = ["lib"]
29
+
30
+ if s.respond_to?(:metadata)
31
+ s.metadata['changelog_uri'] = 'https://github.com/RubyMoney/money/blob/master/CHANGELOG.md'
32
+ s.metadata['source_code_uri'] = 'https://github.com/RubyMoney/money/'
33
+ s.metadata['bug_tracker_uri'] = 'https://github.com/RubyMoney/money/issues'
34
+ end
41
35
  end
@@ -75,6 +75,10 @@ class Money
75
75
  special_money_class = Class.new(Money)
76
76
  expect(bank.exchange_with(special_money_class.new(100, 'USD'), 'EUR')).to be_a special_money_class
77
77
  end
78
+
79
+ it "doesn't loose precision when handling larger amounts" do
80
+ expect(bank.exchange_with(Money.new(100_000_000_000_000_01, 'USD'), 'EUR')).to eq Money.new(133_000_000_000_000_01, 'EUR')
81
+ end
78
82
  end
79
83
  end
80
84
 
@@ -147,7 +151,7 @@ class Money
147
151
 
148
152
  it "delegates options to store, options are a no-op" do
149
153
  expect(subject.store).to receive(:get_rate).with('USD', 'EUR')
150
- subject.get_rate('USD', 'EUR', :without_mutex => true)
154
+ subject.get_rate('USD', 'EUR', without_mutex: true)
151
155
  end
152
156
  end
153
157
 
@@ -197,7 +201,7 @@ class Money
197
201
 
198
202
  it "delegates execution to store, options are a no-op" do
199
203
  expect(subject.store).to receive(:transaction)
200
- subject.export_rates(:yaml, nil, :foo => 1)
204
+ subject.export_rates(:yaml, nil, foo: 1)
201
205
  end
202
206
 
203
207
  end
@@ -239,7 +243,7 @@ class Money
239
243
  it "delegates execution to store#transaction" do
240
244
  expect(subject.store).to receive(:transaction)
241
245
  s = "--- \nUSD_TO_EUR: 1.25\nUSD_TO_JPY: 2.55\n"
242
- subject.import_rates(:yaml, s, :foo => 1)
246
+ subject.import_rates(:yaml, s, foo: 1)
243
247
  end
244
248
 
245
249
  end
@@ -4,159 +4,8 @@ describe Money::Currency::Heuristics do
4
4
  describe "#analyze_string" do
5
5
  let(:it) { Money::Currency }
6
6
 
7
- it "is not affected by blank characters and numbers" do
8
- expect(it.analyze('123')).to eq []
9
- expect(it.analyze('\n123 \t')).to eq []
10
- end
11
-
12
- it "returns nothing when given nothing" do
13
- expect(it.analyze('')).to eq []
14
- expect(it.analyze(nil)).to eq []
15
- end
16
-
17
- it "finds a currency by use of its symbol" do
18
- expect(it.analyze('zł')).to eq ['PLN']
19
- end
20
-
21
- it "is not affected by trailing dot" do
22
- expect(it.analyze('zł.')).to eq ['PLN']
23
- end
24
-
25
- it "finds match even if has numbers after" do
26
- expect(it.analyze('zł 123')).to eq ['PLN']
27
- end
28
-
29
- it "finds match even if has numbers before" do
30
- expect(it.analyze('123 zł')).to eq ['PLN']
31
- end
32
-
33
- it "find match even if symbol is next to number" do
34
- expect(it.analyze('300zł')).to eq ['PLN']
35
- end
36
-
37
- it "finds match even if has numbers with delimiters" do
38
- expect(it.analyze('zł 123,000.50')).to eq ['PLN']
39
- expect(it.analyze('zł123,000.50')).to eq ['PLN']
40
- end
41
-
42
- it "finds currencies with dots in symbols" do
43
- expect(it.analyze('L.E.')).to eq ['EGP']
44
- end
45
-
46
- it "finds by name" do
47
- expect(it.analyze('1900 bulgarian lev')).to eq ['BGN']
48
- expect(it.analyze('Swedish Krona')).to eq ['SEK']
49
- end
50
-
51
- it "Finds several currencies when several match" do
52
- r = it.analyze('$400')
53
- expect(r).to include("ARS")
54
- expect(r).to include("USD")
55
- expect(r).to include("NZD")
56
-
57
- r = it.analyze('9000 £')
58
- expect(r).to include("GBP")
59
- expect(r).to include("SHP")
60
- expect(r).to include("SYP")
61
- end
62
-
63
- it "should use alternate symbols" do
64
- expect(it.analyze('US$')).to eq ['USD']
65
- end
66
-
67
- it "finds a currency by use of its iso code" do
68
- expect(it.analyze('USD 200')).to eq ['USD']
69
- end
70
-
71
- it "finds currencies in the middle of a sentence!" do
72
- expect(it.analyze('It would be nice to have 10000 Albanian lek by tomorrow!')).to eq ['ALL']
73
- end
74
-
75
- it "finds several currencies in the same text!" do
76
- expect(it.analyze("10EUR is less than 100:- but really, I want US$1")).to eq ['EUR', 'SEK', 'USD']
77
- end
78
-
79
- it "find currencies with normal characters in name using unaccent" do
80
- expect(it.analyze("10 Nicaraguan Cordoba")).to eq ["NIO"]
81
- end
82
-
83
- it "find currencies with special characters in name using unaccent" do
84
- expect(it.analyze("10 Nicaraguan Córdoba")).to eq ["NIO"]
85
- end
86
-
87
- it "find currencies with special symbols using unaccent" do
88
- expect(it.analyze("ل.س")).not_to eq []
89
- expect(it.analyze("R₣")).not_to eq []
90
- expect(it.analyze("ரூ")).not_to eq []
91
- expect(it.analyze("රු")).not_to eq []
92
- expect(it.analyze("сом")).not_to eq []
93
- expect(it.analyze("圓")).not_to eq []
94
- expect(it.analyze("円")).not_to eq []
95
- expect(it.analyze("৳")).not_to eq []
96
- expect(it.analyze("૱")).not_to eq []
97
- expect(it.analyze("௹")).not_to eq []
98
- expect(it.analyze("रु")).not_to eq []
99
- expect(it.analyze("ש״ח")).not_to eq []
100
- expect(it.analyze("元")).not_to eq []
101
- expect(it.analyze("¢")).not_to eq []
102
- expect(it.analyze("£")).not_to eq []
103
- expect(it.analyze("€")).not_to eq []
104
- expect(it.analyze("¥")).not_to eq []
105
- expect(it.analyze("د.إ")).not_to eq []
106
- expect(it.analyze("؋")).not_to eq []
107
- expect(it.analyze("դր.")).not_to eq []
108
- expect(it.analyze("ƒ")).not_to eq []
109
- expect(it.analyze("₼")).not_to eq []
110
- expect(it.analyze("৳")).not_to eq []
111
- expect(it.analyze("лв")).not_to eq []
112
- expect(it.analyze("лев")).not_to eq []
113
- expect(it.analyze("дин")).not_to eq []
114
- expect(it.analyze("ب.د")).not_to eq []
115
- expect(it.analyze("₡")).not_to eq []
116
- expect(it.analyze("Kč")).not_to eq []
117
- expect(it.analyze("د.ج")).not_to eq []
118
- expect(it.analyze("ج.م")).not_to eq []
119
- expect(it.analyze("ლ")).not_to eq []
120
- expect(it.analyze("₵")).not_to eq []
121
- expect(it.analyze("₪")).not_to eq []
122
- expect(it.analyze("₹")).not_to eq []
123
- expect(it.analyze("ع.د")).not_to eq []
124
- expect(it.analyze("﷼")).not_to eq []
125
- expect(it.analyze("د.ا")).not_to eq []
126
- expect(it.analyze("៛")).not_to eq []
127
- expect(it.analyze("₩")).not_to eq []
128
- expect(it.analyze("د.ك")).not_to eq []
129
- expect(it.analyze("〒")).not_to eq []
130
- expect(it.analyze("₭")).not_to eq []
131
- expect(it.analyze("ل.ل")).not_to eq []
132
- expect(it.analyze("₨")).not_to eq []
133
- expect(it.analyze("ل.د")).not_to eq []
134
- expect(it.analyze("د.م.")).not_to eq []
135
- expect(it.analyze("ден")).not_to eq []
136
- expect(it.analyze("₮")).not_to eq []
137
- expect(it.analyze("₦")).not_to eq []
138
- expect(it.analyze("C$")).not_to eq []
139
- expect(it.analyze("ر.ع.")).not_to eq []
140
- expect(it.analyze("₱")).not_to eq []
141
- expect(it.analyze("zł")).not_to eq []
142
- expect(it.analyze("₲")).not_to eq []
143
- expect(it.analyze("ر.ق")).not_to eq []
144
- expect(it.analyze("РСД")).not_to eq []
145
- expect(it.analyze("₽")).not_to eq []
146
- expect(it.analyze("ر.س")).not_to eq []
147
- expect(it.analyze("฿")).not_to eq []
148
- expect(it.analyze("د.ت")).not_to eq []
149
- expect(it.analyze("T$")).not_to eq []
150
- expect(it.analyze("₺")).not_to eq []
151
- expect(it.analyze("₴")).not_to eq []
152
- expect(it.analyze("₫")).not_to eq []
153
- expect(it.analyze("B⃦")).not_to eq []
154
- expect(it.analyze("₤")).not_to eq []
155
- expect(it.analyze("ރ")).not_to eq []
156
- end
157
-
158
- it "should function with unicode characters" do
159
- expect(it.analyze("10 դր.")).to eq ["AMD"]
7
+ it "it raises deprecation error" do
8
+ expect{ it.analyze('123') }.to raise_error(StandardError, 'Heuristics deprecated, add `gem "money-heuristics"` to Gemfile')
160
9
  end
161
10
  end
162
11
  end
@@ -6,7 +6,7 @@ class Money
6
6
  FOO = '{ "priority": 1, "iso_code": "FOO", "iso_numeric": "840", "name": "United States Dollar", "symbol": "$", "subunit": "Cent", "subunit_to_unit": 1000, "symbol_first": true, "html_entity": "$", "decimal_mark": ".", "thousands_separator": ",", "smallest_denomination": 1 }'
7
7
 
8
8
  def register_foo(opts={})
9
- foo_attrs = JSON.parse(FOO, :symbolize_names => true)
9
+ foo_attrs = JSON.parse(FOO, symbolize_names: true)
10
10
  # Pass an array of attribute names to 'skip' to remove them from the 'FOO'
11
11
  # json before registering foo as a currency.
12
12
  Array(opts[:skip]).each { |attr| foo_attrs.delete(attr) }
@@ -14,7 +14,7 @@ class Money
14
14
  end
15
15
 
16
16
  def unregister_foo
17
- Currency.unregister(JSON.parse(FOO, :symbolize_names => true))
17
+ Currency.unregister(JSON.parse(FOO, symbolize_names: true))
18
18
  end
19
19
 
20
20
  describe "UnknownCurrency" do
@@ -59,6 +59,11 @@ class Money
59
59
  expect(Currency.find_by_iso_numeric('non iso 4217 numeric code')).to be_nil
60
60
  expect(Currency.find_by_iso_numeric(0)).to be_nil
61
61
  end
62
+
63
+ it "returns nil when given empty input" do
64
+ expect(Currency.find_by_iso_numeric('')).to be_nil
65
+ expect(Currency.find_by_iso_numeric(nil)).to be_nil
66
+ end
62
67
  end
63
68
 
64
69
  describe ".wrap" do
@@ -82,7 +87,7 @@ class Money
82
87
  expect(Currency.all.first.priority).to eq 1
83
88
  end
84
89
  it "raises a MissingAttributeError if any currency has no priority" do
85
- register_foo(:skip => :priority)
90
+ register_foo(skip: :priority)
86
91
 
87
92
  expect{Money::Currency.all}.to \
88
93
  raise_error(Money::Currency::MissingAttributeError, /foo.*priority/)
@@ -115,6 +120,32 @@ class Money
115
120
  end
116
121
 
117
122
 
123
+ describe ".inherit" do
124
+ after do
125
+ Currency.unregister(iso_code: "XXX") if Currency.find("XXX")
126
+ Currency.unregister(iso_code: "YYY") if Currency.find("YYY")
127
+ end
128
+
129
+ it "inherit a new currency" do
130
+ Currency.register(
131
+ iso_code: "XXX",
132
+ name: "Golden Doubloon",
133
+ symbol: "%",
134
+ subunit_to_unit: 100
135
+ )
136
+ Currency.inherit("XXX",
137
+ iso_code: "YYY",
138
+ symbol: "@"
139
+ )
140
+ new_currency = Currency.find("YYY")
141
+ expect(new_currency).not_to be_nil
142
+ expect(new_currency.name).to eq "Golden Doubloon"
143
+ expect(new_currency.symbol).to eq "@"
144
+ expect(new_currency.subunit_to_unit).to eq 100
145
+ end
146
+ end
147
+
148
+
118
149
  describe ".unregister" do
119
150
  it "unregisters a currency" do
120
151
  Currency.register(iso_code: "XXX")
@@ -291,7 +322,17 @@ class Money
291
322
 
292
323
  describe "#inspect" do
293
324
  it "works as documented" do
294
- expect(Currency.new(:usd).inspect).to eq %Q{#<Money::Currency id: usd, priority: 1, symbol_first: true, thousands_separator: ,, html_entity: $, decimal_mark: ., name: United States Dollar, symbol: $, subunit_to_unit: 100, exponent: 2.0, iso_code: USD, iso_numeric: 840, subunit: Cent, smallest_denomination: 1>}
325
+ expect(Currency.new(:usd).inspect).to eq %Q{#<Money::Currency id: usd, priority: 1, symbol_first: true, thousands_separator: ,, html_entity: $, decimal_mark: ., name: United States Dollar, symbol: $, subunit_to_unit: 100, exponent: 2, iso_code: USD, iso_numeric: 840, subunit: Cent, smallest_denomination: 1>}
326
+ end
327
+ end
328
+
329
+ describe "#iso?" do
330
+ it "returns true for iso currency" do
331
+ expect(Money::Currency.new(:eur).iso?).to be true
332
+ end
333
+
334
+ it "returns false if the currency is not iso" do
335
+ expect(Money::Currency.new(:btc).iso?).to be false
295
336
  end
296
337
  end
297
338