money 6.7.1 → 6.13.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 +4 -4
  2. data/.rspec +2 -1
  3. data/.travis.yml +15 -6
  4. data/AUTHORS +5 -0
  5. data/CHANGELOG.md +98 -3
  6. data/Gemfile +13 -4
  7. data/LICENSE +2 -0
  8. data/README.md +64 -44
  9. data/config/currency_backwards_compatible.json +30 -0
  10. data/config/currency_iso.json +135 -59
  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 +33 -39
  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 +106 -139
  22. data/lib/money/money/allocation.rb +37 -0
  23. data/lib/money/money/arithmetic.rb +31 -28
  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 +44 -3
  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 +184 -90
  38. data/spec/money/constructors_spec.rb +0 -12
  39. data/spec/money/formatting_spec.rb +296 -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 -40
  46. data/lib/money/money/formatting.rb +0 -418
@@ -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.1"
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")
@@ -295,6 +326,16 @@ class Money
295
326
  end
296
327
  end
297
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
336
+ end
337
+ end
338
+
298
339
  describe "#to_s" do
299
340
  it "works as documented" do
300
341
  expect(Currency.new(:usd).to_s).to eq("USD")
@@ -0,0 +1,62 @@
1
+ # encoding: utf-8
2
+
3
+ describe Money::LocaleBackend::I18n do
4
+ describe '#initialize' do
5
+ it 'raises an error when I18n is not defined' do
6
+ hide_const('I18n')
7
+
8
+ expect { described_class.new }.to raise_error(Money::LocaleBackend::NotSupported)
9
+ end
10
+ end
11
+
12
+ describe '#lookup' do
13
+ after do
14
+ reset_i18n
15
+ I18n.locale = :en
16
+ end
17
+
18
+ subject { described_class.new }
19
+
20
+ context 'with number.currency.format defined' do
21
+ before do
22
+ I18n.locale = :de
23
+ I18n.backend.store_translations(:de, number: {
24
+ currency: { format: { delimiter: '.', separator: ',' } }
25
+ })
26
+ end
27
+
28
+ it 'returns thousands_separator based on the current locale' do
29
+ expect(subject.lookup(:thousands_separator, nil)).to eq('.')
30
+ end
31
+
32
+ it 'returns decimal_mark based on the current locale' do
33
+ expect(subject.lookup(:decimal_mark, nil)).to eq(',')
34
+ end
35
+ end
36
+
37
+ context 'with number.format defined' do
38
+ before do
39
+ I18n.locale = :de
40
+ I18n.backend.store_translations(:de, number: { format: { delimiter: '.', separator: ',' } })
41
+ end
42
+
43
+ it 'returns thousands_separator based on the current locale' do
44
+ expect(subject.lookup(:thousands_separator, nil)).to eq('.')
45
+ end
46
+
47
+ it 'returns decimal_mark based on the current locale' do
48
+ expect(subject.lookup(:decimal_mark, nil)).to eq(',')
49
+ end
50
+ end
51
+
52
+ context 'with no translation defined' do
53
+ it 'returns thousands_separator based on the current locale' do
54
+ expect(subject.lookup(:thousands_separator, nil)).to eq(nil)
55
+ end
56
+
57
+ it 'returns decimal_mark based on the current locale' do
58
+ expect(subject.lookup(:decimal_mark, nil)).to eq(nil)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,74 @@
1
+ # encoding: utf-8
2
+
3
+ describe Money::LocaleBackend::Legacy do
4
+ after { Money.use_i18n = true }
5
+
6
+ describe '#initialize' do
7
+ it 'raises an error when use_i18n is true and I18n is not defined' do
8
+ Money.use_i18n = true
9
+ hide_const('I18n')
10
+
11
+ expect { described_class.new }.to raise_error(Money::LocaleBackend::NotSupported)
12
+ end
13
+
14
+ it 'does not raise error when use_i18n is false and I18n is not defined' do
15
+ Money.use_i18n = false
16
+ hide_const('I18n')
17
+
18
+ expect { described_class.new }.not_to raise_error
19
+ end
20
+ end
21
+
22
+ describe '#lookup' do
23
+ subject { described_class.new }
24
+ let(:currency) { Money::Currency.new('USD') }
25
+
26
+ context 'use_i18n is true and i18n lookup is successful' do
27
+ before do
28
+ allow(subject.send(:i18n_backend))
29
+ .to receive(:lookup)
30
+ .with(:thousands_separator, nil)
31
+ .and_return('.')
32
+
33
+ allow(subject.send(:i18n_backend))
34
+ .to receive(:lookup)
35
+ .with(:decimal_mark, nil)
36
+ .and_return(',')
37
+ end
38
+
39
+ it 'returns thousands_separator from I18n' do
40
+ expect(subject.lookup(:thousands_separator, currency)).to eq('.')
41
+ end
42
+
43
+ it 'returns decimal_mark based from I18n' do
44
+ expect(subject.lookup(:decimal_mark, currency)).to eq(',')
45
+ end
46
+ end
47
+
48
+ context 'use_i18n is true but i18n lookup is unsuccessful' do
49
+ before do
50
+ allow(subject.send(:i18n_backend)).to receive(:lookup).and_return(nil)
51
+ end
52
+
53
+ it 'returns thousands_separator as defined in currency' do
54
+ expect(subject.lookup(:thousands_separator, currency)).to eq(',')
55
+ end
56
+
57
+ it 'returns decimal_mark based as defined in currency' do
58
+ expect(subject.lookup(:decimal_mark, currency)).to eq('.')
59
+ end
60
+ end
61
+
62
+ context 'use_i18n is false' do
63
+ before { Money.use_i18n = false }
64
+
65
+ it 'returns thousands_separator as defined in currency' do
66
+ expect(subject.lookup(:thousands_separator, currency)).to eq(',')
67
+ end
68
+
69
+ it 'returns decimal_mark based as defined in currency' do
70
+ expect(subject.lookup(:decimal_mark, currency)).to eq('.')
71
+ end
72
+ end
73
+ end
74
+ end