mumboe-currency 0.5
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.
- data/bin/currency_historical_rate_load +105 -0
- data/examples/ex1.rb +13 -0
- data/examples/xe1.rb +20 -0
- data/lib/currency/active_record.rb +265 -0
- data/lib/currency/config.rb +91 -0
- data/lib/currency/core_extensions.rb +41 -0
- data/lib/currency/currency/factory.rb +228 -0
- data/lib/currency/currency.rb +175 -0
- data/lib/currency/currency_version.rb +6 -0
- data/lib/currency/exception.rb +119 -0
- data/lib/currency/exchange/rate/deriver.rb +157 -0
- data/lib/currency/exchange/rate/source/base.rb +166 -0
- data/lib/currency/exchange/rate/source/failover.rb +63 -0
- data/lib/currency/exchange/rate/source/federal_reserve.rb +160 -0
- data/lib/currency/exchange/rate/source/historical/rate.rb +184 -0
- data/lib/currency/exchange/rate/source/historical/rate_loader.rb +186 -0
- data/lib/currency/exchange/rate/source/historical/writer.rb +220 -0
- data/lib/currency/exchange/rate/source/historical.rb +79 -0
- data/lib/currency/exchange/rate/source/new_york_fed.rb +127 -0
- data/lib/currency/exchange/rate/source/provider.rb +120 -0
- data/lib/currency/exchange/rate/source/test.rb +50 -0
- data/lib/currency/exchange/rate/source/the_financials.rb +191 -0
- data/lib/currency/exchange/rate/source/timed_cache.rb +198 -0
- data/lib/currency/exchange/rate/source/xe.rb +165 -0
- data/lib/currency/exchange/rate/source.rb +89 -0
- data/lib/currency/exchange/rate.rb +214 -0
- data/lib/currency/exchange/time_quantitizer.rb +111 -0
- data/lib/currency/exchange.rb +50 -0
- data/lib/currency/formatter.rb +290 -0
- data/lib/currency/macro.rb +321 -0
- data/lib/currency/money.rb +295 -0
- data/lib/currency/money_helper.rb +13 -0
- data/lib/currency/parser.rb +151 -0
- data/lib/currency.rb +143 -0
- data/test/string_test.rb +54 -0
- data/test/test_base.rb +44 -0
- metadata +90 -0
@@ -0,0 +1,120 @@
|
|
1
|
+
# Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
|
2
|
+
# See LICENSE.txt for details.
|
3
|
+
|
4
|
+
require 'currency/exchange/rate/source'
|
5
|
+
|
6
|
+
|
7
|
+
# Base class for rate data providers.
|
8
|
+
# Assumes that rate sources provide more than one rate per query.
|
9
|
+
class Currency::Exchange::Rate::Source::Provider < Currency::Exchange::Rate::Source::Base
|
10
|
+
|
11
|
+
# Error during parsing of rates.
|
12
|
+
class ParserError < ::Currency::Exception::RateSourceError; end
|
13
|
+
|
14
|
+
# The URI used to access the rate source.
|
15
|
+
attr_accessor :uri
|
16
|
+
|
17
|
+
# The URI path relative to uri used to access the rate source.
|
18
|
+
attr_accessor :uri_path
|
19
|
+
|
20
|
+
# The Time used to query the rate source.
|
21
|
+
# Typically set by #load_rates.
|
22
|
+
attr_accessor :date
|
23
|
+
|
24
|
+
# The name is the same as its #uri.
|
25
|
+
alias :name :uri
|
26
|
+
|
27
|
+
def initialize(*args)
|
28
|
+
super
|
29
|
+
@rates = { }
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the date to query for rates.
|
33
|
+
# Defaults to yesterday.
|
34
|
+
def date
|
35
|
+
@date || (Time.now - 24 * 60 * 60) # yesterday.
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
# Returns year of query date.
|
40
|
+
def date_YYYY
|
41
|
+
'%04d' % date.year
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
# Return month of query date.
|
46
|
+
def date_MM
|
47
|
+
'%02d' % date.month
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
# Returns day of query date.
|
52
|
+
def date_DD
|
53
|
+
'%02d' % date.day
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
# Returns the URI string as evaluated with this object.
|
58
|
+
def get_uri
|
59
|
+
uri = self.uri
|
60
|
+
uri = "\"#{uri}\""
|
61
|
+
uri = instance_eval(uri)
|
62
|
+
$stderr.puts "#{self}: uri = #{uri.inspect}" if @verbose
|
63
|
+
uri
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
# Returns the URI content.
|
68
|
+
def get_page_content
|
69
|
+
data = open(get_uri) { |data| data.read }
|
70
|
+
|
71
|
+
data
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
# Clear cached rates from this source.
|
76
|
+
def clear_rates
|
77
|
+
@rates.clear
|
78
|
+
super
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
# Returns current base Rates or calls load_rates to load them from the source.
|
83
|
+
def rates(time = nil)
|
84
|
+
time = time && normalize_time(time)
|
85
|
+
@rates["#{time}"] ||= load_rates(time)
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
# Returns an array of base Rates from the rate source.
|
90
|
+
#
|
91
|
+
# Subclasses must define this method.
|
92
|
+
def load_rates(time = nil)
|
93
|
+
raise Currency::Exception::SubclassResponsibility, :load_rates
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
# Return a matching base rate.
|
98
|
+
def get_rate(c1, c2, time)
|
99
|
+
rates.each do | rate |
|
100
|
+
return rate if
|
101
|
+
rate.c1 == c1 &&
|
102
|
+
rate.c2 == c2 &&
|
103
|
+
(! time || normalize_time(rate.date) == time)
|
104
|
+
end
|
105
|
+
|
106
|
+
nil
|
107
|
+
end
|
108
|
+
|
109
|
+
alias :get_rate_base :get_rate
|
110
|
+
|
111
|
+
|
112
|
+
# Returns true if a rate provider is available.
|
113
|
+
def available?(time = nil)
|
114
|
+
true
|
115
|
+
end
|
116
|
+
|
117
|
+
end # class
|
118
|
+
|
119
|
+
|
120
|
+
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
|
2
|
+
# See LICENSE.txt for details.
|
3
|
+
|
4
|
+
require 'currency/exchange/rate/source/provider'
|
5
|
+
|
6
|
+
# This class is a test Rate Source.
|
7
|
+
# It can provide only fixed rates between USD, CAD and EUR.
|
8
|
+
# Used only for test purposes.
|
9
|
+
# DO NOT USE THESE RATES FOR A REAL APPLICATION.
|
10
|
+
class Currency::Exchange::Rate::Source::Test < Currency::Exchange::Rate::Source::Provider
|
11
|
+
@@instance = nil
|
12
|
+
|
13
|
+
# Returns a singleton instance.
|
14
|
+
def self.instance(*opts)
|
15
|
+
@@instance ||= self.new(*opts)
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def initialize(*opts)
|
20
|
+
self.uri = 'none://localhost/Test'
|
21
|
+
super(*opts)
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def name
|
26
|
+
'Test'
|
27
|
+
end
|
28
|
+
|
29
|
+
# Test rate from :USD to :CAD.
|
30
|
+
def self.USD_CAD; 1.1708; end
|
31
|
+
|
32
|
+
|
33
|
+
# Test rate from :USD to :EUR.
|
34
|
+
def self.USD_EUR; 0.7737; end
|
35
|
+
|
36
|
+
|
37
|
+
# Test rate from :USD to :GBP.
|
38
|
+
def self.USD_GBP; 0.5098; end
|
39
|
+
|
40
|
+
|
41
|
+
# Returns test Rate for USD to [ CAD, EUR, GBP ].
|
42
|
+
def rates
|
43
|
+
[ new_rate(:USD, :CAD, self.class.USD_CAD),
|
44
|
+
new_rate(:USD, :EUR, self.class.USD_EUR),
|
45
|
+
new_rate(:USD, :GBP, self.class.USD_GBP) ]
|
46
|
+
end
|
47
|
+
|
48
|
+
end # class
|
49
|
+
|
50
|
+
|
@@ -0,0 +1,191 @@
|
|
1
|
+
# Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
|
2
|
+
# See LICENSE.txt for details.
|
3
|
+
|
4
|
+
require 'currency/exchange/rate/source/base'
|
5
|
+
|
6
|
+
require 'net/http'
|
7
|
+
require 'open-uri'
|
8
|
+
require 'rexml/document'
|
9
|
+
|
10
|
+
|
11
|
+
# Connects to http://www.thefinancials.com and parses XML.
|
12
|
+
#
|
13
|
+
# This is for demonstration purposes.
|
14
|
+
#
|
15
|
+
class Currency::Exchange::Rate::Source::TheFinancials < ::Currency::Exchange::Rate::Source::Provider
|
16
|
+
# Defines the pivot currency for http://thefinancials.com/.
|
17
|
+
PIVOT_CURRENCY = :USD
|
18
|
+
|
19
|
+
def initialize(*opt)
|
20
|
+
@raw_rates = nil
|
21
|
+
self.uri_path = 'syndicated/UNKNOWN/fxrates.xml'
|
22
|
+
super(*opt)
|
23
|
+
self.uri = "http://www.thefinancials.com/#{self.uri_path}"
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
# Returns 'thefinancials.com'.
|
28
|
+
def name
|
29
|
+
'thefinancials.org'
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
# def get_page_content
|
34
|
+
# test_content
|
35
|
+
# end
|
36
|
+
|
37
|
+
|
38
|
+
def clear_rates
|
39
|
+
@raw_rates = nil
|
40
|
+
super
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def raw_rates
|
45
|
+
rates
|
46
|
+
@raw_rates
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
# Parses XML for rates.
|
51
|
+
def parse_rates(data = nil)
|
52
|
+
data = get_page_content unless data
|
53
|
+
|
54
|
+
rates = [ ]
|
55
|
+
|
56
|
+
@raw_rates = { }
|
57
|
+
|
58
|
+
# $stderr.puts "parse_rates: data = #{data}"
|
59
|
+
|
60
|
+
doc = REXML::Document.new(data).root
|
61
|
+
doc.elements.to_a('//record').each do | record |
|
62
|
+
c1_c2 = record.elements.to_a('symbol')[0].text
|
63
|
+
md = /([A-Z][A-Z][A-Z]).*?([A-Z][A-Z][A-Z])/.match(c1_c2)
|
64
|
+
c1, c2 = md[1], md[2]
|
65
|
+
|
66
|
+
c1 = c1.upcase.intern
|
67
|
+
c2 = c2.upcase.intern
|
68
|
+
|
69
|
+
rate = record.elements.to_a('last')[0].text.to_f
|
70
|
+
|
71
|
+
date = record.elements.to_a('date')[0].text
|
72
|
+
date = Time.parse("#{date} 12:00:00 -05:00") # USA NY => EST
|
73
|
+
|
74
|
+
rates << new_rate(c1, c2, rate, date)
|
75
|
+
|
76
|
+
(@raw_rates[c1] ||= { })[c2] ||= rate
|
77
|
+
end
|
78
|
+
|
79
|
+
rates
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
# Return a list of known base rates.
|
84
|
+
def load_rates(time = nil)
|
85
|
+
self.date = time
|
86
|
+
parse_rates
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
def test_content
|
91
|
+
<<EOF
|
92
|
+
<?xml version="1.0" ?>
|
93
|
+
<TFCRecords>
|
94
|
+
<record>
|
95
|
+
<symbol>USD/EUR</symbol>
|
96
|
+
<date>10/25/2001</date>
|
97
|
+
<last>1.115822</last>
|
98
|
+
</record>
|
99
|
+
<record>
|
100
|
+
<symbol>USD/AUD</symbol>
|
101
|
+
<date>10/25/2001</date>
|
102
|
+
<last>1.975114</last>
|
103
|
+
</record>
|
104
|
+
<record>
|
105
|
+
<symbol>USD/CAD</symbol>
|
106
|
+
<date>10/25/2001</date>
|
107
|
+
<last>1.57775</last>
|
108
|
+
</record>
|
109
|
+
<record>
|
110
|
+
<symbol>USD/CNY</symbol>
|
111
|
+
<date>10/25/2001</date>
|
112
|
+
<last>8.2769</last>
|
113
|
+
</record>
|
114
|
+
<record>
|
115
|
+
<symbol>USD/ESP</symbol>
|
116
|
+
<date>10/25/2001</date>
|
117
|
+
<last>185.65725</last>
|
118
|
+
</record>
|
119
|
+
<record>
|
120
|
+
<symbol>USD/GBP</symbol>
|
121
|
+
<date>10/25/2001</date>
|
122
|
+
<last>0.698849867830019</last>
|
123
|
+
</record>
|
124
|
+
<record>
|
125
|
+
<symbol>USD/HKD</symbol>
|
126
|
+
<date>10/25/2001</date>
|
127
|
+
<last>7.7999</last>
|
128
|
+
</record>
|
129
|
+
<record>
|
130
|
+
<symbol>USD/IDR</symbol>
|
131
|
+
<date>10/25/2001</date>
|
132
|
+
<last>10265</last>
|
133
|
+
</record>
|
134
|
+
<record>
|
135
|
+
<symbol>USD/INR</symbol>
|
136
|
+
<date>10/25/2001</date>
|
137
|
+
<last>48.01</last>
|
138
|
+
</record>
|
139
|
+
<record>
|
140
|
+
<symbol>USD/JPY</symbol>
|
141
|
+
<date>10/25/2001</date>
|
142
|
+
<last>122.68</last>
|
143
|
+
</record>
|
144
|
+
<record>
|
145
|
+
<symbol>USD/KRW</symbol>
|
146
|
+
<date>10/25/2001</date>
|
147
|
+
<last>1293.5</last>
|
148
|
+
</record>
|
149
|
+
<record>
|
150
|
+
<symbol>USD/MYR</symbol>
|
151
|
+
<date>10/25/2001</date>
|
152
|
+
<last>3.8</last>
|
153
|
+
</record>
|
154
|
+
<record>
|
155
|
+
<symbol>USD/NZD</symbol>
|
156
|
+
<date>10/25/2001</date>
|
157
|
+
<last>2.41485</last>
|
158
|
+
</record>
|
159
|
+
<record>
|
160
|
+
<symbol>USD/PHP</symbol>
|
161
|
+
<date>10/25/2001</date>
|
162
|
+
<last>52.05</last>
|
163
|
+
</record>
|
164
|
+
<record>
|
165
|
+
<symbol>USD/PKR</symbol>
|
166
|
+
<date>10/25/2001</date>
|
167
|
+
<last>61.6</last>
|
168
|
+
</record>
|
169
|
+
<record>
|
170
|
+
<symbol>USD/SGD</symbol>
|
171
|
+
<date>10/25/2001</date>
|
172
|
+
<last>1.82615</last>
|
173
|
+
</record>
|
174
|
+
<record>
|
175
|
+
<symbol>USD/THB</symbol>
|
176
|
+
<date>10/25/2001</date>
|
177
|
+
<last>44.88</last>
|
178
|
+
</record>
|
179
|
+
<record>
|
180
|
+
<symbol>USD/TWD</symbol>
|
181
|
+
<date>10/25/2001</date>
|
182
|
+
<last>34.54</last>
|
183
|
+
</record>
|
184
|
+
</TFCRecords>
|
185
|
+
EOF
|
186
|
+
end
|
187
|
+
|
188
|
+
end # class
|
189
|
+
|
190
|
+
|
191
|
+
|
@@ -0,0 +1,198 @@
|
|
1
|
+
# Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
|
2
|
+
# See LICENSE.txt for details.
|
3
|
+
|
4
|
+
require 'currency/exchange/rate/source/base'
|
5
|
+
|
6
|
+
# A timed cache for rate sources.
|
7
|
+
#
|
8
|
+
# This class should be used at the top-level of a rate source change,
|
9
|
+
# to correctly check rate dates.
|
10
|
+
#
|
11
|
+
class Currency::Exchange::Rate::Source::TimedCache < ::Currency::Exchange::Rate::Source::Base
|
12
|
+
# The rate source.
|
13
|
+
attr_accessor :source
|
14
|
+
|
15
|
+
# Defines the number of seconds rates until rates
|
16
|
+
# become invalid, causing a request of new rates.
|
17
|
+
#
|
18
|
+
# Defaults to 600 seconds.
|
19
|
+
attr_accessor :time_to_live
|
20
|
+
|
21
|
+
|
22
|
+
# Defines the number of random seconds to add before
|
23
|
+
# rates become invalid.
|
24
|
+
#
|
25
|
+
# Defaults to 30 seconds.
|
26
|
+
attr_accessor :time_to_live_fudge
|
27
|
+
|
28
|
+
|
29
|
+
# Returns the time of the last load.
|
30
|
+
attr_reader :rate_load_time
|
31
|
+
|
32
|
+
|
33
|
+
# Returns the time of the next load.
|
34
|
+
attr_reader :rate_reload_time
|
35
|
+
|
36
|
+
|
37
|
+
# Returns source's name.
|
38
|
+
def name
|
39
|
+
source.name
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
def initialize(*opt)
|
44
|
+
self.time_to_live = 600
|
45
|
+
self.time_to_live_fudge = 30
|
46
|
+
@rate_load_time = nil
|
47
|
+
@rate_reload_time = nil
|
48
|
+
@processing_rates = false
|
49
|
+
@cached_rates = { }
|
50
|
+
@cached_rates_old = nil
|
51
|
+
super(*opt)
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
# Clears current rates.
|
56
|
+
def clear_rates
|
57
|
+
@cached_rates = { }
|
58
|
+
@source.clear_rates
|
59
|
+
super
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
# Returns true if the cache of Rates
|
64
|
+
# is expired.
|
65
|
+
def expired?
|
66
|
+
if @time_to_live &&
|
67
|
+
@rate_reload_time &&
|
68
|
+
(Time.now > @rate_reload_time)
|
69
|
+
|
70
|
+
if @cached_rates
|
71
|
+
$stderr.puts "#{self}: rates expired on #{@rate_reload_time}" if @verbose
|
72
|
+
|
73
|
+
@cached_rates_old = @cached_rates
|
74
|
+
end
|
75
|
+
|
76
|
+
clear_rates
|
77
|
+
|
78
|
+
true
|
79
|
+
else
|
80
|
+
false
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
# Check expired? before returning a Rate.
|
86
|
+
def rate(c1, c2, time)
|
87
|
+
if expired?
|
88
|
+
clear_rates
|
89
|
+
end
|
90
|
+
super(c1, c2, time)
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
def get_rate(c1, c2, time)
|
95
|
+
# STDERR.puts "get_rate #{c1} #{c2} #{time}"
|
96
|
+
rates = load_rates(time)
|
97
|
+
# STDERR.puts "rates = #{rates.inspect}"
|
98
|
+
rate = rates && (rates.select{|x| x.c1 == c1 && x.c2 == c2}[0])
|
99
|
+
# STDERR.puts "rate = #{rate.inspect}"
|
100
|
+
rate
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
# Returns an array of all the cached Rates.
|
105
|
+
def rates(time = nil)
|
106
|
+
load_rates(time)
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
# Returns an array of all the cached Rates.
|
111
|
+
def load_rates(time = nil)
|
112
|
+
# Check expiration.
|
113
|
+
expired?
|
114
|
+
|
115
|
+
# Return rates, if cached.
|
116
|
+
return rates if rates = @cached_rates["#{time}"]
|
117
|
+
|
118
|
+
# Force load of rates.
|
119
|
+
rates = @cached_rates["#{time}"] = _load_rates_from_source(time)
|
120
|
+
|
121
|
+
# Update expiration.
|
122
|
+
_calc_rate_reload_time
|
123
|
+
|
124
|
+
return nil unless rates
|
125
|
+
|
126
|
+
# Flush old rates.
|
127
|
+
@cached_rates_old = nil
|
128
|
+
|
129
|
+
rates
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
def time_to_live=(x)
|
134
|
+
@time_to_live = x
|
135
|
+
_calc_rate_reload_time
|
136
|
+
x
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
def time_to_live_fudge=(x)
|
141
|
+
@time_to_live_fudge = x
|
142
|
+
_calc_rate_reload_time
|
143
|
+
x
|
144
|
+
end
|
145
|
+
|
146
|
+
|
147
|
+
def _calc_rate_reload_time
|
148
|
+
if @time_to_live && @rate_load_time
|
149
|
+
@rate_reload_time = @rate_load_time + (@time_to_live + (@time_to_live_fudge || 0))
|
150
|
+
$stderr.puts "#{self}: rates expire on #{@rate_reload_time}" if @verbose
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
|
156
|
+
|
157
|
+
def _load_rates_from_source(time = nil) # :nodoc:
|
158
|
+
rates = nil
|
159
|
+
|
160
|
+
begin
|
161
|
+
# Do not allow re-entrancy
|
162
|
+
raise Currency::Exception::InvalidReentrancy, "Reentry!" if @processing_rates
|
163
|
+
|
164
|
+
# Begin processing new rate request.
|
165
|
+
@processing_rates = true
|
166
|
+
|
167
|
+
# Clear cached Rates.
|
168
|
+
clear_rates
|
169
|
+
|
170
|
+
# Load rates from the source.
|
171
|
+
rates = source.load_rates(time)
|
172
|
+
|
173
|
+
# Compute new rate timestamp.
|
174
|
+
@rate_load_time = Time.now
|
175
|
+
|
176
|
+
# STDERR.puts "rate_load_time = #{@rate_load_time}"
|
177
|
+
ensure
|
178
|
+
# End processsing new rate request.
|
179
|
+
@processing_rates = false
|
180
|
+
|
181
|
+
end
|
182
|
+
|
183
|
+
# STDERR.puts "_load_rates => #{rates.inspect}"
|
184
|
+
|
185
|
+
rates
|
186
|
+
end
|
187
|
+
|
188
|
+
|
189
|
+
# Returns true if the underlying rate provider is available.
|
190
|
+
def available?(time = nil)
|
191
|
+
source.available?(time)
|
192
|
+
end
|
193
|
+
|
194
|
+
|
195
|
+
end # class
|
196
|
+
|
197
|
+
|
198
|
+
|
@@ -0,0 +1,165 @@
|
|
1
|
+
# Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
|
2
|
+
# See LICENSE.txt for details.
|
3
|
+
|
4
|
+
require 'currency/exchange/rate/source/base'
|
5
|
+
|
6
|
+
require 'net/http'
|
7
|
+
require 'open-uri'
|
8
|
+
# Cant use REXML because of missing </form> tags -- 2007/03/11
|
9
|
+
# require 'rexml/document'
|
10
|
+
|
11
|
+
# Connects to http://xe.com and parses "XE.com Quick Cross Rates"
|
12
|
+
# from home page HTML.
|
13
|
+
#
|
14
|
+
class Currency::Exchange::Rate::Source::Xe < ::Currency::Exchange::Rate::Source::Provider
|
15
|
+
|
16
|
+
# Defines the pivot currency for http://xe.com/.
|
17
|
+
PIVOT_CURRENCY = :USD
|
18
|
+
|
19
|
+
def initialize(*opt)
|
20
|
+
self.uri = 'http://xe.com/'
|
21
|
+
self.pivot_currency = PIVOT_CURRENCY
|
22
|
+
@raw_rates = nil
|
23
|
+
super(*opt)
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
# Returns 'xe.com'.
|
28
|
+
def name
|
29
|
+
'xe.com'
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def clear_rates
|
34
|
+
@raw_rates = nil
|
35
|
+
super
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
# Returns a cached Hash of rates:
|
40
|
+
#
|
41
|
+
# xe.raw_rates[:USD][:CAD] => 1.0134
|
42
|
+
#
|
43
|
+
def raw_rates
|
44
|
+
# Force load of rates
|
45
|
+
@raw_rates ||= parse_page_rates
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
|
50
|
+
# Parses http://xe.com homepage HTML for
|
51
|
+
# quick rates of 10 currencies.
|
52
|
+
def parse_page_rates(data = nil)
|
53
|
+
data = get_page_content unless data
|
54
|
+
|
55
|
+
@lines = data = data.split(/\n/);
|
56
|
+
|
57
|
+
# xe.com no longer gives date/time.
|
58
|
+
# Remove usecs.
|
59
|
+
time = Time.at(Time.new.to_i).getutc
|
60
|
+
@rate_timestamp = time
|
61
|
+
|
62
|
+
eat_lines_until /More currencies\.\.\.<\/a>/i
|
63
|
+
eat_lines_until /^\s*<tr>/i
|
64
|
+
eat_lines_until /^\s*<tr>/i
|
65
|
+
|
66
|
+
# Read first table row to get position for each currency
|
67
|
+
currency = [ ]
|
68
|
+
eat_lines_until /^\s*<\/tr>/i do
|
69
|
+
if md = /<td[^>]+?>.*?\/> ([A-Z][A-Z][A-Z])<\/td>/.match(@line)
|
70
|
+
cur = md[1].intern
|
71
|
+
cur_i = currency.size
|
72
|
+
currency.push(cur)
|
73
|
+
$stderr.puts "Found currency header: #{cur.inspect} at #{cur_i}" if @verbose
|
74
|
+
end
|
75
|
+
end
|
76
|
+
raise ParserError, "Currencies header not found" if currency.empty?
|
77
|
+
|
78
|
+
|
79
|
+
# Skip until "1 USD ="
|
80
|
+
eat_lines_until /^\s*<td[^>]+?> 1 +USD =/
|
81
|
+
|
82
|
+
# Read first row of 1 USD = ...
|
83
|
+
rate = { }
|
84
|
+
cur_i = -1
|
85
|
+
eat_lines_until /^\s*<\/tr>/i do
|
86
|
+
# Grok:
|
87
|
+
#
|
88
|
+
# <td align="center" class="cur2 currencyA">114.676</td>\n
|
89
|
+
#
|
90
|
+
# AND
|
91
|
+
#
|
92
|
+
# <td align="center" class="cur2 currencyA"><div id="positionImage">0.9502\n
|
93
|
+
#
|
94
|
+
if md = /<td[^>]+?>\s*(<div[^>]+?>\s*)?(\d+\.\d+)\s*(<\/td>)?/i.match(@line)
|
95
|
+
usd_to_cur = md[2].to_f
|
96
|
+
cur_i = cur_i + 1
|
97
|
+
cur = currency[cur_i]
|
98
|
+
raise ParserError, "Currency not found at column #{cur_i}" unless cur
|
99
|
+
next if cur.to_s == PIVOT_CURRENCY.to_s
|
100
|
+
(rate[PIVOT_CURRENCY] ||= {})[cur] = usd_to_cur
|
101
|
+
(rate[cur] ||= { })[PIVOT_CURRENCY] ||= 1.0 / usd_to_cur
|
102
|
+
$stderr.puts "#{cur.inspect} => #{usd_to_cur}" if @verbose
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
raise ::Currency::Exception::UnavailableRates, "No rates found in #{get_uri.inspect}" if rate.keys.empty?
|
107
|
+
|
108
|
+
raise ParserError,
|
109
|
+
[
|
110
|
+
"Not all currencies found",
|
111
|
+
:expected_currences, currency,
|
112
|
+
:found_currencies, rate.keys,
|
113
|
+
:missing_currencies, currency - rate.keys,
|
114
|
+
] if rate.keys.size != currency.size
|
115
|
+
|
116
|
+
@lines = @line = nil
|
117
|
+
|
118
|
+
raise ParserError, "Rate date not found" unless @rate_timestamp
|
119
|
+
|
120
|
+
rate
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
def eat_lines_until(rx)
|
125
|
+
until @lines.empty?
|
126
|
+
@line = @lines.shift
|
127
|
+
if md = rx.match(@line)
|
128
|
+
$stderr.puts "\nMATCHED #{@line.inspect} WITH #{rx.inspect} AT LINES:\n#{@lines[0..4].inspect}" if @verbose
|
129
|
+
return md
|
130
|
+
end
|
131
|
+
yield @line if block_given?
|
132
|
+
end
|
133
|
+
|
134
|
+
raise ParserError, [ 'eat_lines_until failed', :rx, rx ]
|
135
|
+
|
136
|
+
false
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
# Return a list of known base rates.
|
141
|
+
def load_rates(time = nil)
|
142
|
+
if time
|
143
|
+
$stderr.puts "#{self}: WARNING CANNOT SUPPLY HISTORICAL RATES" unless @time_warning
|
144
|
+
@time_warning = true
|
145
|
+
end
|
146
|
+
|
147
|
+
rates = raw_rates # Load rates
|
148
|
+
rates_pivot = rates[PIVOT_CURRENCY]
|
149
|
+
raise ::Currency::Exception::UnknownRate,
|
150
|
+
[
|
151
|
+
"Cannot get base rate #{PIVOT_CURRENCY.inspect}",
|
152
|
+
:pivot_currency, PIVOT_CURRENCY,
|
153
|
+
] unless rates_pivot
|
154
|
+
|
155
|
+
result = rates_pivot.keys.collect do | c2 |
|
156
|
+
new_rate(PIVOT_CURRENCY, c2, rates_pivot[c2], @rate_timestamp)
|
157
|
+
end
|
158
|
+
|
159
|
+
result
|
160
|
+
end
|
161
|
+
|
162
|
+
|
163
|
+
end # class
|
164
|
+
|
165
|
+
|