money 6.7.0 → 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 +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