currency 0.3.3 → 0.4.0
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/COPYING.txt +339 -0
- data/LICENSE.txt +62 -0
- data/Manifest.txt +37 -14
- data/README.txt +8 -0
- data/Rakefile +42 -8
- data/Releases.txt +26 -0
- data/TODO.txt +1 -0
- data/examples/ex1.rb +3 -3
- data/examples/xe1.rb +3 -2
- data/lib/currency.rb +71 -9
- data/lib/currency/active_record.rb +138 -21
- data/lib/currency/core_extensions.rb +7 -5
- data/lib/currency/currency.rb +94 -177
- data/lib/currency/{currency_factory.rb → currency/factory.rb} +46 -25
- data/lib/currency/currency_version.rb +3 -3
- data/lib/currency/exception.rb +14 -14
- data/lib/currency/exchange.rb +14 -12
- data/lib/currency/exchange/rate.rb +159 -28
- data/lib/currency/exchange/rate/deriver.rb +146 -0
- data/lib/currency/exchange/rate/source.rb +84 -0
- data/lib/currency/exchange/rate/source/base.rb +156 -0
- data/lib/currency/exchange/rate/source/failover.rb +57 -0
- data/lib/currency/exchange/rate/source/historical.rb +79 -0
- data/lib/currency/exchange/rate/source/historical/rate.rb +181 -0
- data/lib/currency/exchange/rate/source/historical/writer.rb +203 -0
- data/lib/currency/exchange/rate/source/new_york_fed.rb +91 -0
- data/lib/currency/exchange/rate/source/provider.rb +105 -0
- data/lib/currency/exchange/rate/source/test.rb +50 -0
- data/lib/currency/exchange/rate/source/the_financials.rb +190 -0
- data/lib/currency/exchange/rate/source/timed_cache.rb +144 -0
- data/lib/currency/exchange/rate/source/xe.rb +166 -0
- data/lib/currency/exchange/time_quantitizer.rb +111 -0
- data/lib/currency/formatter.rb +159 -0
- data/lib/currency/macro.rb +321 -0
- data/lib/currency/money.rb +90 -64
- data/lib/currency/money_helper.rb +6 -5
- data/lib/currency/parser.rb +153 -0
- data/test/ar_column_test.rb +6 -3
- data/test/ar_simple_test.rb +5 -2
- data/test/ar_test_base.rb +39 -33
- data/test/ar_test_core.rb +64 -0
- data/test/formatter_test.rb +81 -0
- data/test/historical_writer_test.rb +184 -0
- data/test/macro_test.rb +109 -0
- data/test/money_test.rb +72 -4
- data/test/new_york_fed_test.rb +57 -0
- data/test/parser_test.rb +60 -0
- data/test/test_base.rb +13 -3
- data/test/time_quantitizer_test.rb +136 -0
- data/test/xe_test.rb +29 -5
- metadata +41 -18
- data/lib/currency/exchange/base.rb +0 -84
- data/lib/currency/exchange/test.rb +0 -39
- data/lib/currency/exchange/xe.rb +0 -250
@@ -0,0 +1,190 @@
|
|
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
|
+
self.uri = 'http://www.thefinancials.com/XXXXXXX'
|
21
|
+
@raw_rates = nil
|
22
|
+
super(*opt)
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
# Returns 'thefinancials.com'.
|
27
|
+
def name
|
28
|
+
'thefinancials.org'
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
def get_page_content
|
33
|
+
test_content
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
def clear_rates
|
38
|
+
@raw_rates = nil
|
39
|
+
super
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
def raw_rates
|
44
|
+
rates
|
45
|
+
@raw_rates
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
# Parses XML for rates.
|
50
|
+
def parse_rates(data = nil)
|
51
|
+
data = get_page_content unless data
|
52
|
+
|
53
|
+
rates = [ ]
|
54
|
+
|
55
|
+
@raw_rates = { }
|
56
|
+
|
57
|
+
# $stderr.puts "parse_rates: data = #{data}"
|
58
|
+
|
59
|
+
doc = REXML::Document.new(data).root
|
60
|
+
doc.elements.to_a('//record').each do | record |
|
61
|
+
c1_c2 = record.elements.to_a('symbol')[0].text
|
62
|
+
md = /([A-Z][A-Z][A-Z]).*?([A-Z][A-Z][A-Z])/.match(c1_c2)
|
63
|
+
c1, c2 = md[1], md[2]
|
64
|
+
|
65
|
+
c1 = c1.upcase.intern
|
66
|
+
c2 = c2.upcase.intern
|
67
|
+
|
68
|
+
rate = record.elements.to_a('last')[0].text.to_f
|
69
|
+
|
70
|
+
date = record.elements.to_a('date')[0].text
|
71
|
+
date = Time.parse("#{date} 12:00:00 -05:00") # USA NY => EST
|
72
|
+
|
73
|
+
rates << new_rate(c1, c2, rate, date)
|
74
|
+
|
75
|
+
(@raw_rates[c1] ||= { })[c2] ||= rate
|
76
|
+
end
|
77
|
+
|
78
|
+
rates
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
# Return a list of known base rates.
|
83
|
+
def load_rates(time = nil)
|
84
|
+
self.date = time
|
85
|
+
parse_rates
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
def test_content
|
90
|
+
<<EOF
|
91
|
+
<?xml version="1.0" ?>
|
92
|
+
<TFCRecords>
|
93
|
+
<record>
|
94
|
+
<symbol>USD/EUR</symbol>
|
95
|
+
<date>10/25/2001</date>
|
96
|
+
<last>1.115822</last>
|
97
|
+
</record>
|
98
|
+
<record>
|
99
|
+
<symbol>USD/AUD</symbol>
|
100
|
+
<date>10/25/2001</date>
|
101
|
+
<last>1.975114</last>
|
102
|
+
</record>
|
103
|
+
<record>
|
104
|
+
<symbol>USD/CAD</symbol>
|
105
|
+
<date>10/25/2001</date>
|
106
|
+
<last>1.57775</last>
|
107
|
+
</record>
|
108
|
+
<record>
|
109
|
+
<symbol>USD/CNY</symbol>
|
110
|
+
<date>10/25/2001</date>
|
111
|
+
<last>8.2769</last>
|
112
|
+
</record>
|
113
|
+
<record>
|
114
|
+
<symbol>USD/ESP</symbol>
|
115
|
+
<date>10/25/2001</date>
|
116
|
+
<last>185.65725</last>
|
117
|
+
</record>
|
118
|
+
<record>
|
119
|
+
<symbol>USD/GBP</symbol>
|
120
|
+
<date>10/25/2001</date>
|
121
|
+
<last>0.698849867830019</last>
|
122
|
+
</record>
|
123
|
+
<record>
|
124
|
+
<symbol>USD/HKD</symbol>
|
125
|
+
<date>10/25/2001</date>
|
126
|
+
<last>7.7999</last>
|
127
|
+
</record>
|
128
|
+
<record>
|
129
|
+
<symbol>USD/IDR</symbol>
|
130
|
+
<date>10/25/2001</date>
|
131
|
+
<last>10265</last>
|
132
|
+
</record>
|
133
|
+
<record>
|
134
|
+
<symbol>USD/INR</symbol>
|
135
|
+
<date>10/25/2001</date>
|
136
|
+
<last>48.01</last>
|
137
|
+
</record>
|
138
|
+
<record>
|
139
|
+
<symbol>USD/JPY</symbol>
|
140
|
+
<date>10/25/2001</date>
|
141
|
+
<last>122.68</last>
|
142
|
+
</record>
|
143
|
+
<record>
|
144
|
+
<symbol>USD/KRW</symbol>
|
145
|
+
<date>10/25/2001</date>
|
146
|
+
<last>1293.5</last>
|
147
|
+
</record>
|
148
|
+
<record>
|
149
|
+
<symbol>USD/MYR</symbol>
|
150
|
+
<date>10/25/2001</date>
|
151
|
+
<last>3.8</last>
|
152
|
+
</record>
|
153
|
+
<record>
|
154
|
+
<symbol>USD/NZD</symbol>
|
155
|
+
<date>10/25/2001</date>
|
156
|
+
<last>2.41485</last>
|
157
|
+
</record>
|
158
|
+
<record>
|
159
|
+
<symbol>USD/PHP</symbol>
|
160
|
+
<date>10/25/2001</date>
|
161
|
+
<last>52.05</last>
|
162
|
+
</record>
|
163
|
+
<record>
|
164
|
+
<symbol>USD/PKR</symbol>
|
165
|
+
<date>10/25/2001</date>
|
166
|
+
<last>61.6</last>
|
167
|
+
</record>
|
168
|
+
<record>
|
169
|
+
<symbol>USD/SGD</symbol>
|
170
|
+
<date>10/25/2001</date>
|
171
|
+
<last>1.82615</last>
|
172
|
+
</record>
|
173
|
+
<record>
|
174
|
+
<symbol>USD/THB</symbol>
|
175
|
+
<date>10/25/2001</date>
|
176
|
+
<last>44.88</last>
|
177
|
+
</record>
|
178
|
+
<record>
|
179
|
+
<symbol>USD/TWD</symbol>
|
180
|
+
<date>10/25/2001</date>
|
181
|
+
<last>34.54</last>
|
182
|
+
</record>
|
183
|
+
</TFCRecords>
|
184
|
+
EOF
|
185
|
+
end
|
186
|
+
|
187
|
+
end # class
|
188
|
+
|
189
|
+
|
190
|
+
|
@@ -0,0 +1,144 @@
|
|
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 source's name.
|
30
|
+
def name
|
31
|
+
source.name
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
def initialize(*opt)
|
36
|
+
self.time_to_live = 600
|
37
|
+
self.time_to_live_fudge = 30
|
38
|
+
@rate_timestamp = nil
|
39
|
+
@processing_rates = false
|
40
|
+
super(*opt)
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
# Clears current rates.
|
45
|
+
def clear_rates
|
46
|
+
@cached_rates.clear
|
47
|
+
@source.clear_rates
|
48
|
+
super
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
# Returns true if the cache of Rates
|
53
|
+
# is expired.
|
54
|
+
def expired?
|
55
|
+
if @time_to_live &&
|
56
|
+
@rates_renew_time &&
|
57
|
+
(Time.now > @rates_renew_time)
|
58
|
+
|
59
|
+
if @cached_rates
|
60
|
+
$stderr.puts "#{self}: rates expired on #{@rates_renew_time}" if @verbose
|
61
|
+
|
62
|
+
@cached_rates_old ||= @cashed_rates
|
63
|
+
|
64
|
+
@cached_rates = nil
|
65
|
+
end
|
66
|
+
|
67
|
+
true
|
68
|
+
else
|
69
|
+
false
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
# Check expired? before returning a Rate.
|
75
|
+
def rate(c1, c2, time)
|
76
|
+
if expired?
|
77
|
+
clear_rates
|
78
|
+
end
|
79
|
+
super(c1, c2, time)
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
# Returns an array of all the cached Rates.
|
84
|
+
def rates(time = nil)
|
85
|
+
load_rates(time)
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
# Returns an array of all the cached Rates.
|
90
|
+
def load_rates(time = nil)
|
91
|
+
# Check expiration.
|
92
|
+
expired?
|
93
|
+
|
94
|
+
# Return rates, if cached.
|
95
|
+
return @cached_rates if @cashed_rates
|
96
|
+
|
97
|
+
# Force load of rates
|
98
|
+
@cached_rates = _load_rates_from_source(time)
|
99
|
+
|
100
|
+
# Flush old rates.
|
101
|
+
@cached_rates_old = nil
|
102
|
+
|
103
|
+
# Update expiration.
|
104
|
+
if time_to_live
|
105
|
+
@rates_renew_time = @rate_timestamp + (time_to_live + (time_to_live_fudge || 0))
|
106
|
+
$stderr.puts "#{self}: rates expire on #{@rates_renew_time}" if @verbose
|
107
|
+
end
|
108
|
+
|
109
|
+
@rates
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
def _load_rates_from_source(time = nil) # :nodoc:
|
114
|
+
# Do not allow re-entrancy
|
115
|
+
raise "Reentry!" if @processing_rates
|
116
|
+
|
117
|
+
# Begin processing new rate request.
|
118
|
+
@processing_rates = true
|
119
|
+
|
120
|
+
# Clear cached Rates.
|
121
|
+
clear_rates
|
122
|
+
|
123
|
+
# Load rates from the source.
|
124
|
+
rates = source.load_rates(time)
|
125
|
+
|
126
|
+
unless rates
|
127
|
+
# FIXME: raise Exception::???
|
128
|
+
return rates
|
129
|
+
end
|
130
|
+
|
131
|
+
# Compute new rate timestamp.
|
132
|
+
@rate_timestamp = Time.now
|
133
|
+
|
134
|
+
# End processsing new rate request.
|
135
|
+
@processing_rates = false
|
136
|
+
|
137
|
+
rates
|
138
|
+
end
|
139
|
+
|
140
|
+
|
141
|
+
end # class
|
142
|
+
|
143
|
+
|
144
|
+
|
@@ -0,0 +1,166 @@
|
|
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
|
+
|
9
|
+
# Connects to http://xe.com and parses "XE.com Quick Cross Rates"
|
10
|
+
# from home page HTML.
|
11
|
+
#
|
12
|
+
# This is for demonstration purposes.
|
13
|
+
#
|
14
|
+
class Currency::Exchange::Rate::Source::Xe < ::Currency::Exchange::Rate::Source::Provider
|
15
|
+
# Defines the pivot currency for http://xe.com/.
|
16
|
+
PIVOT_CURRENCY = :USD
|
17
|
+
|
18
|
+
def initialize(*opt)
|
19
|
+
self.uri = 'http://xe.com/'
|
20
|
+
self.pivot_currency = PIVOT_CURRENCY
|
21
|
+
@raw_rates = nil
|
22
|
+
super(*opt)
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
# Returns 'xe.com'.
|
27
|
+
def name
|
28
|
+
'xe.com'
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
def clear_rates
|
33
|
+
@raw_rates = nil
|
34
|
+
super
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
# Returns a cached Hash of rates:
|
39
|
+
#
|
40
|
+
# xe.xe_rates[:USD][:CAD] => 1.0134
|
41
|
+
#
|
42
|
+
def raw_rates
|
43
|
+
# Force load of rates
|
44
|
+
@raw_rates ||= parse_page_rates
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
|
49
|
+
# Parses http://xe.com homepage HTML for
|
50
|
+
# quick rates of 10 currencies.
|
51
|
+
def parse_page_rates(data = nil)
|
52
|
+
data = get_page_content unless data
|
53
|
+
|
54
|
+
data = data.split(/[\r\n]/)
|
55
|
+
|
56
|
+
@rate_timestamp = nil
|
57
|
+
|
58
|
+
# Chomp after
|
59
|
+
until data.empty?
|
60
|
+
line = data.pop
|
61
|
+
break if line =~ /Need More currencies\?/
|
62
|
+
end
|
63
|
+
|
64
|
+
# Chomp before Quick Cross Rates
|
65
|
+
until data.empty?
|
66
|
+
line = data.shift
|
67
|
+
break if line =~ /XE.com Quick Cross Rates/
|
68
|
+
end
|
69
|
+
|
70
|
+
# Look for date.
|
71
|
+
md = nil
|
72
|
+
until data.empty?
|
73
|
+
line = data.shift
|
74
|
+
break if md = /rates as of (\d\d\d\d)\.(\d\d)\.(\d\d)\s+(\d\d):(\d\d).*GMT/.match(line)
|
75
|
+
end
|
76
|
+
if md
|
77
|
+
yyyy, mm, dd, h, m = md[1].to_i, md[2].to_i, md[3].to_i, md[4].to_i, md[5].to_i
|
78
|
+
@rate_timestamp = Time.gm(yyyy, mm, dd, h, m, 0, 0) rescue nil
|
79
|
+
#$stderr.puts "parsed #{md[0].inspect} => #{yyyy}, #{mm}, #{dd}, #{h}, #{m}"
|
80
|
+
#$stderr.puts " => #{@rate_timestamp && @rate_timestamp.xmlschema}"
|
81
|
+
end
|
82
|
+
|
83
|
+
until data.empty?
|
84
|
+
line = data.shift
|
85
|
+
break if line =~ /Confused about how to use the rates/i
|
86
|
+
end
|
87
|
+
|
88
|
+
until data.empty?
|
89
|
+
line = data.shift
|
90
|
+
break if line =~ /^\s*<\/tr>/i
|
91
|
+
end
|
92
|
+
# $stderr.puts "#{data[0..4].inspect}"
|
93
|
+
|
94
|
+
# Read first table row to get position for each currency
|
95
|
+
currency = [ ]
|
96
|
+
until data.empty?
|
97
|
+
line = data.shift
|
98
|
+
break if line =~ /^\s*<\/tr>/i
|
99
|
+
if md = /<td><IMG .+ ALT="([A-Z][A-Z][A-Z])"/i.match(line) #"
|
100
|
+
cur = md[1].intern
|
101
|
+
cur_i = currency.size
|
102
|
+
currency.push(cur)
|
103
|
+
# $stderr.puts "Found currency header: #{cur.inspect} at #{cur_i}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# $stderr.puts "#{data[0..4].inspect}"
|
108
|
+
|
109
|
+
# Skip blank <tr>
|
110
|
+
until data.empty?
|
111
|
+
line = data.shift
|
112
|
+
break if line =~ /^\s*<td>.+1.+USD.+=/
|
113
|
+
end
|
114
|
+
|
115
|
+
until data.empty?
|
116
|
+
line = data.shift
|
117
|
+
break if line =~ /^\s*<\/tr>/i
|
118
|
+
end
|
119
|
+
|
120
|
+
# $stderr.puts "#{data[0..4].inspect}"
|
121
|
+
|
122
|
+
# Read first row of 1 USD = ...
|
123
|
+
|
124
|
+
rate = { }
|
125
|
+
cur_i = -1
|
126
|
+
until data.empty?
|
127
|
+
line = data.shift
|
128
|
+
break if cur_i < 0 && line =~ /^\s*<\/tr>/i
|
129
|
+
if md = /<td>\s+(\d+\.\d+)\s+<\/td>/.match(line)
|
130
|
+
usd_to_cur = md[1].to_f
|
131
|
+
cur_i = cur_i + 1
|
132
|
+
cur = currency[cur_i]
|
133
|
+
next unless cur
|
134
|
+
next if cur.to_s == PIVOT_CURRENCY.to_s
|
135
|
+
(rate[PIVOT_CURRENCY] ||= {})[cur] = usd_to_cur
|
136
|
+
(rate[cur] ||= { })[PIVOT_CURRENCY] ||= 1.0 / usd_to_cur
|
137
|
+
$stderr.puts "#{cur.inspect} => #{usd_to_cur}" if @verbose
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
rate
|
142
|
+
end
|
143
|
+
|
144
|
+
|
145
|
+
# Return a list of known base rates.
|
146
|
+
def load_rates(time = nil)
|
147
|
+
if time
|
148
|
+
$stderr.puts "#{self}: WARNING CANNOT SUPPLY HISTORICAL RATES" unless @time_warning
|
149
|
+
@time_warning = true
|
150
|
+
end
|
151
|
+
|
152
|
+
rates = raw_rates # Load rates
|
153
|
+
rates_pivot = rates[PIVOT_CURRENCY]
|
154
|
+
raise ::Currency::Exception::UnknownRate.new("#{self}: cannot get base rate #{PIVOT_CURRENCY.inspect}") unless rates_pivot
|
155
|
+
|
156
|
+
result = rates_pivot.keys.collect do | c2 |
|
157
|
+
new_rate(PIVOT_CURRENCY, c2, rates_pivot[c2], @rate_timestamp)
|
158
|
+
end
|
159
|
+
|
160
|
+
result
|
161
|
+
end
|
162
|
+
|
163
|
+
|
164
|
+
end # class
|
165
|
+
|
166
|
+
|