eu_central_bank 1.4.2 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 01a35ff1906fc6fcb306bff2804ed3684b8925ab
4
- data.tar.gz: 56e430e88205e5f30248570cacc32d7cfdcd7270
2
+ SHA256:
3
+ metadata.gz: b589452c483bac16b40ab1372829694c9f261b9c802381110ed00fcebb73eaa6
4
+ data.tar.gz: 551614b051eb56ef4c10be19a5e796ebe0194287e1c278d374ae12f9aa5a2b9e
5
5
  SHA512:
6
- metadata.gz: e84505f83a5c70373b71cb0f07325fef8e11c313fa4936e607bc5162531b666a5e04f53a9dedf18270113ce402d3f3c65948cfac7633d1146782c437e64079fe
7
- data.tar.gz: beef6198548c17b60f27e0151cee6df2a59280ebdd7c4261621ca4359274ada0aa6d7043db5e6d25be2e54330e3a150f6aea1dc7b671d4fff0bf96138dcafe30
6
+ metadata.gz: 0774ff56419fa0b24d71d8747259b1464468e3cbdb4b6db52808bf3ab9e6023cdb4a00f92e387df9e27fb9d84a16a933a588f49e7d7355db86704fa0a67ab8d7
7
+ data.tar.gz: 1f2a24bfd29add88ea0c537b2b7be8a245bcd8fdbe8a7bda04474757e3df9b8ebb62271b34e8dadfc029c2918b4d5c80f4a6783f043eb22d9447f29911db2534
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # eu_central_bank changelog
2
2
 
3
+ ## 1.7.0 (Nov 17 2021)
4
+
5
+ * Support historical rates
6
+
7
+ ## 1.6.1 (Mar 3 2021)
8
+
9
+ * Allow a fallback when loading cached rate files to inhibit network fetch.
10
+
11
+ ## 1.5.0 (Dec 7 2019)
12
+
13
+ * Fix issue with money gem incompatibility
14
+ * Fix thread safety of the default rates store
15
+ * Bump money dependency to >= 6.13.6
16
+
3
17
  ## 1.4.2 (Feb 2 2019)
4
18
 
5
19
  * Fix issue with importing exported rates
data/LICENSE CHANGED
@@ -1,20 +1,22 @@
1
+ MIT License
2
+
1
3
  Copyright (c) 2009 Wong Liang Zan
4
+ Copyright (c) 2021 Shane Emmons
2
5
 
3
- Permission is hereby granted, free of charge, to any person obtaining
4
- a copy of this software and associated documentation files (the
5
- "Software"), to deal in the Software without restriction, including
6
- without limitation the rights to use, copy, modify, merge, publish,
7
- distribute, sublicense, and/or sell copies of the Software, and to
8
- permit persons to whom the Software is furnished to do so, subject to
9
- the following conditions:
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
10
12
 
11
- The above copyright notice and this permission notice shall be
12
- included in all copies or substantial portions of the Software.
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
13
15
 
14
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
data/README.md CHANGED
@@ -25,7 +25,7 @@ gem 'nokogiri', '1.6.8'
25
25
 
26
26
  ## Usage
27
27
 
28
- With the gem, you do not need to manually add exchange rates. Calling update_rates will download the rates from the European Central Bank. The API is the same as the money gem. Feel free to use Money objects with the bank.
28
+ With the gem, you do not need to manually add exchange rates. Calling `update_rates` will download the rates from the European Central Bank. The API is the same as the money gem. Feel free to use Money objects with the bank.
29
29
 
30
30
  ``` ruby
31
31
  require 'eu_central_bank'
@@ -0,0 +1,45 @@
1
+ class EuCentralBank < Money::Bank::VariableExchange
2
+ class RatesDocument < Nokogiri::XML::SAX::Document
3
+ attr_reader :rates
4
+ attr_reader :errors
5
+ attr_reader :updated_at
6
+
7
+ def initialize
8
+ @rates = {}
9
+ @errors = []
10
+ @updated_at = nil
11
+ @current_date = nil
12
+ end
13
+
14
+ def error(msg)
15
+ # TODO: remove this workaround after
16
+ # https://github.com/sparklemotion/nokogiri/pull/1872
17
+ # is released
18
+ @errors << msg
19
+ super
20
+ end
21
+
22
+ def start_element(name, attributes=[])
23
+ return if name != 'Cube' || attributes.empty?
24
+ begin
25
+ first_name, first_value = attributes[0]
26
+ case first_name
27
+ when 'time'
28
+ @current_date = Time.parse(first_value).to_date
29
+ @updated_at ||= @current_date
30
+ @rates[@current_date] = []
31
+ when 'currency'
32
+ currency = first_value
33
+ _, rate = attributes[1]
34
+ @rates[@current_date] << [currency, rate]
35
+ end
36
+ rescue StandardError => e
37
+ raise Nokogiri::XML::XPath::SyntaxError, e.message
38
+ end
39
+ end
40
+
41
+ def end_document
42
+ raise Nokogiri::XML::XPath::SyntaxError if @rates.empty? || @updated_at.nil?
43
+ end
44
+ end
45
+ end
@@ -2,6 +2,7 @@ require 'open-uri'
2
2
  require 'nokogiri'
3
3
  require 'money'
4
4
  require 'money/rates_store/store_with_historical_data_support'
5
+ require 'eu_central_bank/rates_document'
5
6
 
6
7
  class InvalidCache < StandardError ; end
7
8
 
@@ -15,38 +16,47 @@ class EuCentralBank < Money::Bank::VariableExchange
15
16
  attr_accessor :historical_rates_updated_at
16
17
 
17
18
  SERIALIZER_DATE_SEPARATOR = '_AT_'
18
-
19
+ DECIMAL_PRECISION = 5
19
20
  CURRENCIES = %w(USD JPY BGN CZK DKK GBP HUF ILS ISK PLN RON SEK CHF NOK HRK RUB TRY AUD BRL CAD CNY HKD IDR INR KRW MXN MYR NZD PHP SGD THB ZAR).map(&:freeze).freeze
20
21
  ECB_RATES_URL = 'https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml'.freeze
21
22
  ECB_90_DAY_URL = 'https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml'.freeze
23
+ ECB_ALL_HIST_URL = 'https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.xml'.freeze
24
+
25
+ LEGACY_CURRENCIES = %w(CYP SIT ROL TRL)
22
26
 
23
27
  def initialize(st = Money::RatesStore::StoreWithHistoricalDataSupport.new, &block)
24
28
  super
25
29
  @currency_string = nil
26
30
  end
27
31
 
28
- def update_rates(cache=nil)
29
- update_parsed_rates(doc(cache))
32
+ def update_rates(cache=nil, url=ECB_RATES_URL)
33
+ update_parsed_rates(doc(cache, url))
30
34
  end
31
35
 
32
- def update_historical_rates(cache=nil)
33
- update_parsed_historical_rates(doc(cache, ECB_90_DAY_URL))
36
+ def update_historical_rates(cache=nil, all=false)
37
+ url = all ? ECB_ALL_HIST_URL : ECB_90_DAY_URL
38
+ update_parsed_historical_rates(doc(cache, url))
34
39
  end
35
40
 
36
41
  def save_rates(cache, url=ECB_RATES_URL)
37
42
  raise InvalidCache unless cache
38
43
  File.open(cache, "w") do |file|
39
- io = open(url);
44
+ io = open_url(url)
40
45
  io.each_line { |line| file.puts line }
41
46
  end
42
47
  end
43
48
 
49
+ def save_historical_rates(cache, all=false)
50
+ url = all ? ECB_ALL_HIST_URL : ECB_90_DAY_URL
51
+ save_rates(cache, url)
52
+ end
53
+
44
54
  def update_rates_from_s(content)
45
- update_parsed_rates(doc_from_s(content))
55
+ update_parsed_rates(parse_rates(content))
46
56
  end
47
57
 
48
58
  def save_rates_to_s(url=ECB_RATES_URL)
49
- open(url).read
59
+ open_url(url).read
50
60
  end
51
61
 
52
62
  def exchange(cents, from_currency, to_currency, date=nil)
@@ -146,7 +156,7 @@ class EuCentralBank < Money::Bank::VariableExchange
146
156
  from, to = key.split(SERIALIZER_SEPARATOR)
147
157
  to, date = to.split(SERIALIZER_DATE_SEPARATOR)
148
158
 
149
- store.add_rate from, to, BigDecimal(rate), date
159
+ store.add_rate from, to, BigDecimal(rate, DECIMAL_PRECISION), date
150
160
  end
151
161
  end
152
162
 
@@ -164,48 +174,53 @@ class EuCentralBank < Money::Bank::VariableExchange
164
174
 
165
175
  def doc(cache, url=ECB_RATES_URL)
166
176
  rates_source = !!cache ? cache : url
167
- Nokogiri::XML(open(rates_source)).tap { |doc| doc.xpath('gesmes:Envelope/xmlns:Cube/xmlns:Cube//xmlns:Cube') }
168
- rescue Nokogiri::XML::XPath::SyntaxError
169
- Nokogiri::XML(open(url))
177
+ begin
178
+ parse_rates(open_url(rates_source))
179
+ rescue Nokogiri::XML::XPath::SyntaxError
180
+ parse_rates(open_url(url))
181
+ end
170
182
  end
171
183
 
172
- def doc_from_s(content)
173
- Nokogiri::XML(content)
174
- end
184
+ def parse_rates(io)
185
+ doc = ::EuCentralBank::RatesDocument.new
186
+ parser = Nokogiri::XML::SAX::Parser.new(doc)
187
+ parser.parse(io)
175
188
 
176
- def update_parsed_rates(doc)
177
- rates = doc.xpath('gesmes:Envelope/xmlns:Cube/xmlns:Cube//xmlns:Cube')
189
+ unless doc.errors.empty?
190
+ # Temporary workaround for jruby until
191
+ # https://github.com/sparklemotion/nokogiri/pull/1872 gets
192
+ # released and we bump nokogiri version to include it.
193
+ # TLDR: jruby version of SAX parser will mask all the exceptions
194
+ # raised in document so we will raise it here if there were errors.
195
+ raise Nokogiri::XML::XPath::SyntaxError, doc.errors.join("\n")
196
+ end
178
197
 
179
- store.transaction true do
180
- rates.each do |exchange_rate|
181
- rate = BigDecimal(exchange_rate.attribute("rate").value)
182
- currency = exchange_rate.attribute("currency").value
183
- set_rate("EUR", currency, rate)
198
+ doc
199
+ end
200
+
201
+ def copy_rates(rates_document, with_date = false)
202
+ rates_document.rates.each do |date, rates|
203
+ rates.each do |currency, rate|
204
+ next if LEGACY_CURRENCIES.include?(currency)
205
+ set_rate('EUR', currency, BigDecimal(rate, DECIMAL_PRECISION), with_date ? date : nil)
184
206
  end
185
- set_rate("EUR", "EUR", 1)
207
+ set_rate('EUR', 'EUR', 1, with_date ? date : nil)
186
208
  end
209
+ end
187
210
 
188
- rates_updated_at = doc.xpath('gesmes:Envelope/xmlns:Cube/xmlns:Cube/@time').first.value
189
- @rates_updated_at = Time.parse(rates_updated_at)
190
-
211
+ def update_parsed_rates(rates_document)
212
+ store.transaction true do
213
+ copy_rates(rates_document)
214
+ end
215
+ @rates_updated_at = rates_document.updated_at
191
216
  @last_updated = Time.now
192
217
  end
193
218
 
194
- def update_parsed_historical_rates(doc)
195
- rates = doc.xpath('gesmes:Envelope/xmlns:Cube/xmlns:Cube//xmlns:Cube')
196
-
219
+ def update_parsed_historical_rates(rates_document)
197
220
  store.transaction true do
198
- rates.each do |exchange_rate|
199
- rate = BigDecimal(exchange_rate.attribute("rate").value)
200
- currency = exchange_rate.attribute("currency").value
201
- date = exchange_rate.parent.attribute("time").value
202
- set_rate("EUR", currency, rate, date)
203
- end
221
+ copy_rates(rates_document, true)
204
222
  end
205
-
206
- rates_updated_at = doc.xpath('gesmes:Envelope/xmlns:Cube/xmlns:Cube/@time').first.value
207
- @historical_rates_updated_at = Time.parse(rates_updated_at)
208
-
223
+ @historical_rates_updated_at = rates_document.updated_at
209
224
  @historical_last_updated = Time.now
210
225
  end
211
226
 
@@ -214,8 +229,16 @@ class EuCentralBank < Money::Bank::VariableExchange
214
229
  def calculate_exchange(from, to_currency, rate)
215
230
  to_currency_money = Money::Currency.wrap(to_currency).subunit_to_unit
216
231
  from_currency_money = from.currency.subunit_to_unit
217
- decimal_money = BigDecimal(to_currency_money) / BigDecimal(from_currency_money)
232
+ decimal_money = BigDecimal(to_currency_money, DECIMAL_PRECISION) / BigDecimal(from_currency_money, DECIMAL_PRECISION)
218
233
  money = (decimal_money * from.cents * rate).round
219
234
  Money.new(money, to_currency)
220
235
  end
236
+
237
+ def open_url(url)
238
+ if RUBY_VERSION >= '2.5.0'
239
+ URI.open(url)
240
+ else
241
+ open(url)
242
+ end
243
+ end
221
244
  end
@@ -3,33 +3,16 @@ module Money::RatesStore
3
3
  INDEX_DATE_SEPARATOR = '_AT_'.freeze
4
4
 
5
5
  def add_rate(currency_iso_from, currency_iso_to, rate, date = nil)
6
- transaction { index[rate_key_for(currency_iso_from, currency_iso_to, date)] = rate }
6
+ transaction { rates[rate_key_for(currency_iso_from, currency_iso_to, date)] = rate }
7
7
  end
8
8
 
9
9
  def get_rate(currency_iso_from, currency_iso_to, date = nil)
10
- transaction { index[rate_key_for(currency_iso_from, currency_iso_to, date)] }
10
+ transaction { rates[rate_key_for(currency_iso_from, currency_iso_to, date)] }
11
11
  end
12
12
 
13
13
  # Wraps block execution in a thread-safe transaction
14
- def transaction(force_sync = false, &block)
15
- # Ruby 1.9.3 does not support @mutex.owned?
16
- if @mutex.respond_to?(:owned?)
17
- force_sync = false if @mutex.locked? && @mutex.owned?
18
- else
19
- # If we allowed this in Ruby 1.9.3, it might possibly cause recursive
20
- # locking within the same thread.
21
- force_sync = false
22
- end
23
- if !force_sync && (@in_transaction || options[:without_mutex])
24
- block.call self
25
- else
26
- @mutex.synchronize do
27
- @in_transaction = true
28
- result = block.call
29
- @in_transaction = false
30
- result
31
- end
32
- end
14
+ def transaction(_force_sync = false, &block)
15
+ super(&block)
33
16
  end
34
17
 
35
18
  # Iterate over rate tuples (iso_from, iso_to, rate)
@@ -47,7 +30,7 @@ module Money::RatesStore
47
30
  # end
48
31
  def each_rate(&block)
49
32
  enum = Enumerator.new do |yielder|
50
- index.each do |key, rate|
33
+ rates.each do |key, rate|
51
34
  iso_from, iso_to = key.split(Memory::INDEX_KEY_SEPARATOR)
52
35
  iso_to, date = iso_to.split(INDEX_DATE_SEPARATOR)
53
36
  date = Date.parse(date) if date
@@ -60,10 +43,10 @@ module Money::RatesStore
60
43
 
61
44
  private
62
45
 
63
- def rate_key_for(currency_iso_from, currency_iso_to, date = nil)
64
- key = [currency_iso_from, currency_iso_to].join(Memory::INDEX_KEY_SEPARATOR)
65
- key = [key, date.to_s].join(INDEX_DATE_SEPARATOR) if date
66
- key.upcase
67
- end
46
+ def rate_key_for(currency_iso_from, currency_iso_to, date = nil)
47
+ key = [currency_iso_from, currency_iso_to].join(Memory::INDEX_KEY_SEPARATOR)
48
+ key = [key, date.to_s].join(INDEX_DATE_SEPARATOR) if date
49
+ key.upcase
50
+ end
68
51
  end
69
52
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eu_central_bank
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.2
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shane Emmons
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-02-02 00:00:00.000000000 Z
11
+ date: 2021-11-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.8'
19
+ version: '1.9'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.8'
26
+ version: '1.9'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: money
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -31,6 +31,9 @@ dependencies:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
33
  version: '6.13'
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 6.13.6
34
37
  type: :runtime
35
38
  prerelease: false
36
39
  version_requirements: !ruby/object:Gem::Requirement
@@ -38,6 +41,9 @@ dependencies:
38
41
  - - "~>"
39
42
  - !ruby/object:Gem::Version
40
43
  version: '6.13'
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 6.13.6
41
47
  - !ruby/object:Gem::Dependency
42
48
  name: rspec
43
49
  requirement: !ruby/object:Gem::Requirement
@@ -64,11 +70,13 @@ files:
64
70
  - LICENSE
65
71
  - README.md
66
72
  - lib/eu_central_bank.rb
73
+ - lib/eu_central_bank/rates_document.rb
67
74
  - lib/money/rates_store/store_with_historical_data_support.rb
68
75
  homepage: https://github.com/RubyMoney/eu_central_bank
69
- licenses: []
76
+ licenses:
77
+ - MIT
70
78
  metadata: {}
71
- post_install_message:
79
+ post_install_message:
72
80
  rdoc_options: []
73
81
  require_paths:
74
82
  - lib
@@ -83,9 +91,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
83
91
  - !ruby/object:Gem::Version
84
92
  version: '0'
85
93
  requirements: []
86
- rubyforge_project:
87
- rubygems_version: 2.6.8
88
- signing_key:
94
+ rubygems_version: 3.2.22
95
+ signing_key:
89
96
  specification_version: 4
90
97
  summary: Calculates exchange rates based on rates from european central bank. Money
91
98
  gem compatible.