-
Notifications
You must be signed in to change notification settings - Fork 129
/
eu_central_bank.rb
248 lines (201 loc) · 6.97 KB
/
eu_central_bank.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
require 'open-uri'
require 'nokogiri'
require 'money'
require 'money/rates_store/store_with_historical_data_support'
require 'eu_central_bank/rates_document'
class InvalidCache < StandardError ; end
class CurrencyUnavailable < StandardError; end
class EuCentralBank < Money::Bank::VariableExchange
attr_accessor :last_updated
attr_accessor :rates_updated_at
attr_accessor :historical_last_updated
attr_accessor :historical_rates_updated_at
SERIALIZER_DATE_SEPARATOR = '_AT_'
DECIMAL_PRECISION = 5
CURRENCIES = %w(USD JPY BGN CZK DKK GBP HUF ILS ISK PLN RON SEK CHF NOK TRY AUD BRL CAD CNY HKD IDR INR KRW MXN MYR NZD PHP SGD THB ZAR).map(&:freeze).freeze
ECB_RATES_URL = 'https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml'.freeze
ECB_90_DAY_URL = 'https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml'.freeze
ECB_ALL_HIST_URL = 'https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.xml'.freeze
LEGACY_CURRENCIES = %w(CYP HRK RUB SIT ROL TRL)
def initialize(st = Money::RatesStore::StoreWithHistoricalDataSupport.new, &block)
super
@currency_string = nil
end
def update_rates(cache=nil, url=ECB_RATES_URL)
update_parsed_rates(doc(cache, url))
end
def update_historical_rates(cache=nil, all=false)
url = all ? ECB_ALL_HIST_URL : ECB_90_DAY_URL
update_parsed_historical_rates(doc(cache, url))
end
def save_rates(cache, url=ECB_RATES_URL)
raise InvalidCache unless cache
File.open(cache, "w") do |file|
io = open_url(url)
io.each_line { |line| file.puts line }
end
end
def save_historical_rates(cache, all=false)
url = all ? ECB_ALL_HIST_URL : ECB_90_DAY_URL
save_rates(cache, url)
end
def update_rates_from_s(content)
update_parsed_rates(parse_rates(content))
end
def save_rates_to_s(url=ECB_RATES_URL)
open_url(url).read
end
def exchange(cents, from_currency, to_currency, date=nil)
exchange_with(Money.new(cents, from_currency), to_currency, date)
end
def exchange_with(from, to_currency, date=nil)
from_base_rate, to_base_rate = nil, nil
rate = get_rate(from.currency, to_currency, date)
unless rate
store.transaction true do
from_base_rate = get_rate("EUR", from.currency.to_s, date)
to_base_rate = get_rate("EUR", to_currency, date)
end
unless from_base_rate && to_base_rate
message = "No conversion rate known for '#{from.currency.iso_code}' -> '#{to_currency}'"
message << " on #{date.to_s}" if date
raise Money::Bank::UnknownRate, message
end
rate = to_base_rate / from_base_rate
end
calculate_exchange(from, to_currency, rate)
end
def get_rate(from, to, date = nil)
return 1 if from == to
check_currency_available(from)
check_currency_available(to)
if date.is_a?(Hash)
# Backwards compatibility for the opts hash
date = date[:date]
end
store.get_rate(::Money::Currency.wrap(from).iso_code, ::Money::Currency.wrap(to).iso_code, date)
end
def set_rate(from, to, rate, date = nil)
if date.is_a?(Hash)
# Backwards compatibility for the opts hash
date = date[:date]
end
store.add_rate(::Money::Currency.wrap(from).iso_code, ::Money::Currency.wrap(to).iso_code, rate, date)
end
def rates
store.each_rate.each_with_object({}) do |(from,to,rate,date),hash|
key = [from, to].join(SERIALIZER_SEPARATOR)
key = [key, date.to_s].join(SERIALIZER_DATE_SEPARATOR) if date
hash[key] = rate
end
end
def export_rates(format, file = nil, opts = {})
raise Money::Bank::UnknownRateFormat unless
RATE_FORMATS.include? format
store.transaction true do
s = case format
when :json
JSON.dump(rates)
when :ruby
Marshal.dump(rates)
when :yaml
YAML.dump(rates)
end
unless file.nil?
File.open(file, "w") {|f| f.write(s) }
end
s
end
end
def import_rates(format, s, opts = {})
raise Money::Bank::UnknownRateFormat unless
RATE_FORMATS.include? format
store.transaction true do
data = case format
when :json
JSON.load(s)
when :ruby
Marshal.load(s)
when :yaml
if Gem::Version.new(Psych::VERSION) >= Gem::Version.new('3.1.0')
YAML.safe_load(s, permitted_classes: [ BigDecimal ])
else
YAML.safe_load(s, [ BigDecimal ], [], true)
end
end
data.each do |key, rate|
from, to = key.split(SERIALIZER_SEPARATOR)
to, date = to.split(SERIALIZER_DATE_SEPARATOR)
store.add_rate from, to, BigDecimal(rate, DECIMAL_PRECISION), date
end
end
self
end
def check_currency_available(currency)
currency_string = currency.to_s
return true if currency_string == "EUR"
return true if CURRENCIES.include?(currency_string)
raise CurrencyUnavailable, "No rates available for #{currency_string}"
end
protected
def doc(cache, url=ECB_RATES_URL)
rates_source = !!cache ? cache : url
begin
parse_rates(open_url(rates_source))
rescue Nokogiri::XML::XPath::SyntaxError
parse_rates(open_url(url))
end
end
def parse_rates(io)
doc = ::EuCentralBank::RatesDocument.new
parser = Nokogiri::XML::SAX::Parser.new(doc)
parser.parse(io)
unless doc.errors.empty?
# Temporary workaround for jruby until
# https://github.com/sparklemotion/nokogiri/pull/1872 gets
# released and we bump nokogiri version to include it.
# TLDR: jruby version of SAX parser will mask all the exceptions
# raised in document so we will raise it here if there were errors.
raise Nokogiri::XML::XPath::SyntaxError, doc.errors.join("\n")
end
doc
end
def copy_rates(rates_document, with_date = false)
rates_document.rates.each do |date, rates|
rates.each do |currency, rate|
next if LEGACY_CURRENCIES.include?(currency)
set_rate('EUR', currency, BigDecimal(rate, DECIMAL_PRECISION), with_date ? date : nil)
end
set_rate('EUR', 'EUR', 1, with_date ? date : nil)
end
end
def update_parsed_rates(rates_document)
store.transaction true do
copy_rates(rates_document)
end
@rates_updated_at = rates_document.updated_at
@last_updated = Time.now
end
def update_parsed_historical_rates(rates_document)
store.transaction true do
copy_rates(rates_document, true)
end
@historical_rates_updated_at = rates_document.updated_at
@historical_last_updated = Time.now
end
private
def calculate_exchange(from, to_currency, rate)
to_currency_money = Money::Currency.wrap(to_currency).subunit_to_unit
from_currency_money = from.currency.subunit_to_unit
decimal_money = BigDecimal(to_currency_money, DECIMAL_PRECISION) / BigDecimal(from_currency_money, DECIMAL_PRECISION)
money = (decimal_money * from.cents * rate).round
Money.new(money, to_currency)
end
def open_url(url)
if RUBY_VERSION >= '2.5.0'
URI.open(url)
else
open(url)
end
end
end