acvwilson-currency 0.5.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/ChangeLog +8 -0
- data/LICENSE.txt +65 -0
- data/Manifest.txt +58 -0
- data/README.txt +51 -0
- data/Releases.txt +155 -0
- data/TODO.txt +9 -0
- data/currency.gemspec +18 -0
- data/examples/ex1.rb +13 -0
- data/examples/xe1.rb +20 -0
- data/lib/currency.rb +143 -0
- data/lib/currency/active_record.rb +265 -0
- data/lib/currency/config.rb +91 -0
- data/lib/currency/core_extensions.rb +83 -0
- data/lib/currency/currency.rb +175 -0
- data/lib/currency/currency/factory.rb +121 -0
- data/lib/currency/currency_version.rb +6 -0
- data/lib/currency/exception.rb +119 -0
- data/lib/currency/exchange.rb +48 -0
- data/lib/currency/exchange/rate.rb +214 -0
- data/lib/currency/exchange/rate/deriver.rb +157 -0
- data/lib/currency/exchange/rate/source.rb +89 -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.rb +79 -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/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/time_quantitizer.rb +111 -0
- data/lib/currency/formatter.rb +310 -0
- data/lib/currency/macro.rb +321 -0
- data/lib/currency/money.rb +298 -0
- data/lib/currency/money_helper.rb +13 -0
- data/lib/currency/parser.rb +193 -0
- data/spec/ar_column_spec.rb +76 -0
- data/spec/ar_core_spec.rb +68 -0
- data/spec/ar_simple_spec.rb +23 -0
- data/spec/config_spec.rb +29 -0
- data/spec/federal_reserve_spec.rb +75 -0
- data/spec/formatter_spec.rb +72 -0
- data/spec/historical_writer_spec.rb +187 -0
- data/spec/macro_spec.rb +109 -0
- data/spec/money_spec.rb +355 -0
- data/spec/new_york_fed_spec.rb +73 -0
- data/spec/parser_spec.rb +105 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/time_quantitizer_spec.rb +115 -0
- data/spec/timed_cache_spec.rb +95 -0
- data/spec/xe_spec.rb +50 -0
- metadata +117 -0
@@ -0,0 +1,220 @@
|
|
1
|
+
|
2
|
+
require 'currency/exchange/rate/source/historical'
|
3
|
+
|
4
|
+
# Responsible for writing historical rates from a rate source.
|
5
|
+
class Currency::Exchange::Rate::Source::Historical::Writer
|
6
|
+
|
7
|
+
# Error during handling of historical rates.
|
8
|
+
class Error < ::Currency::Exception::Base; end
|
9
|
+
|
10
|
+
# The source of rates.
|
11
|
+
attr_accessor :source
|
12
|
+
|
13
|
+
# If true, compute all Rates between rates.
|
14
|
+
# This can be used to aid complex join reports that may assume
|
15
|
+
# c1 as the from currency and c2 as the to currency.
|
16
|
+
attr_accessor :all_rates
|
17
|
+
|
18
|
+
# If true, store identity rates.
|
19
|
+
# This can be used to aid complex join reports.
|
20
|
+
attr_accessor :identity_rates
|
21
|
+
|
22
|
+
# If true, compute and store all reciprocal rates.
|
23
|
+
attr_accessor :reciprocal_rates
|
24
|
+
|
25
|
+
# If set, a set of preferred currencies.
|
26
|
+
attr_accessor :preferred_currencies
|
27
|
+
|
28
|
+
# If set, a list of required currencies.
|
29
|
+
attr_accessor :required_currencies
|
30
|
+
|
31
|
+
# If set, a list of required base currencies.
|
32
|
+
# base currencies must have rates as c1.
|
33
|
+
attr_accessor :base_currencies
|
34
|
+
|
35
|
+
# If set, use this time quantitizer to
|
36
|
+
# manipulate the Rate date_0 date_1 time ranges.
|
37
|
+
# If :default, use the TimeQuantitizer.default.
|
38
|
+
attr_accessor :time_quantitizer
|
39
|
+
|
40
|
+
|
41
|
+
def initialize(opt = { })
|
42
|
+
@all_rates = true
|
43
|
+
@identity_rates = false
|
44
|
+
@reciprocal_rates = true
|
45
|
+
@preferred_currencies = nil
|
46
|
+
@required_currencies = nil
|
47
|
+
@base_currencies = nil
|
48
|
+
@time_quantitizer = nil
|
49
|
+
opt.each_pair{| k, v | self.send("#{k}=", v) }
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
# Returns a list of selected rates from source.
|
54
|
+
def selected_rates
|
55
|
+
# Produce a list of all currencies.
|
56
|
+
currencies = source.currencies
|
57
|
+
|
58
|
+
# $stderr.puts "currencies = #{currencies.join(', ')}"
|
59
|
+
|
60
|
+
selected_rates = [ ]
|
61
|
+
|
62
|
+
# Get list of preferred_currencies.
|
63
|
+
if self.preferred_currencies
|
64
|
+
self.preferred_currencies = self.preferred_currencies.collect do | c |
|
65
|
+
::Currency::Currency.get(c)
|
66
|
+
end
|
67
|
+
currencies = currencies.select do | c |
|
68
|
+
self.preferred_currencies.include?(c)
|
69
|
+
end.uniq
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
# Check for required currencies.
|
74
|
+
if self.required_currencies
|
75
|
+
self.required_currencies = self.required_currencies.collect do | c |
|
76
|
+
::Currency::Currency.get(c)
|
77
|
+
end
|
78
|
+
|
79
|
+
self.required_currencies.each do | c |
|
80
|
+
unless currencies.include?(c)
|
81
|
+
raise ::Currency::Exception::MissingCurrency,
|
82
|
+
[
|
83
|
+
"Required currency #{c.inspect} not in #{currencies.inspect}",
|
84
|
+
:currency, c,
|
85
|
+
:required_currency, currencies,
|
86
|
+
]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
# $stderr.puts "currencies = #{currencies.inspect}"
|
93
|
+
|
94
|
+
deriver = ::Currency::Exchange::Rate::Deriver.new(:source => source)
|
95
|
+
|
96
|
+
# Produce Rates for all pairs of currencies.
|
97
|
+
if all_rates
|
98
|
+
currencies.each do | c1 |
|
99
|
+
currencies.each do | c2 |
|
100
|
+
next if c1 == c2 && ! identity_rates
|
101
|
+
rate = deriver.rate(c1, c2, nil)
|
102
|
+
selected_rates << rate unless selected_rates.include?(rate)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
elsif base_currencies
|
106
|
+
base_currencies.each do | c1 |
|
107
|
+
c1 = ::Currency::Currency.get(c1)
|
108
|
+
currencies.each do | c2 |
|
109
|
+
next if c1 == c2 && ! identity_rates
|
110
|
+
rate = deriver.rate(c1, c2, nil)
|
111
|
+
selected_rates << rate unless selected_rates.include?(rate)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
else
|
115
|
+
selected_rates = source.rates.select do | r |
|
116
|
+
next if r.c1 == r.c2 && ! identity_rates
|
117
|
+
currencies.include?(r.c1) && currencies.include?(r.c2)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
if identity_rates
|
122
|
+
currencies.each do | c1 |
|
123
|
+
c1 = ::Currency::Currency.get(c1)
|
124
|
+
c2 = c1
|
125
|
+
rate = deriver.rate(c1, c2, nil)
|
126
|
+
selected_rates << rate unless selected_rates.include?(rate)
|
127
|
+
end
|
128
|
+
else
|
129
|
+
selected_rates = selected_rates.select do | r |
|
130
|
+
r.c1 != r.c2
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
if reciprocal_rates
|
135
|
+
selected_rates.clone.each do | r |
|
136
|
+
c1 = r.c2
|
137
|
+
c2 = r.c1
|
138
|
+
rate = deriver.rate(c1, c2, nil)
|
139
|
+
selected_rates << rate unless selected_rates.include?(rate)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# $stderr.puts "selected_rates = #{selected_rates.inspect}\n [#{selected_rates.size}]"
|
144
|
+
|
145
|
+
selected_rates
|
146
|
+
end
|
147
|
+
|
148
|
+
|
149
|
+
# Returns an Array of Historical::Rate objects that were written.
|
150
|
+
# Avoids writing Rates that already have been written.
|
151
|
+
def write_rates(rates = selected_rates)
|
152
|
+
|
153
|
+
# Create Historical::Rate objects.
|
154
|
+
h_rate_class = ::Currency::Exchange::Rate::Source::Historical::Rate
|
155
|
+
|
156
|
+
# Most Rates from the same Source will probably have the same time,
|
157
|
+
# so cache the computed date_range.
|
158
|
+
date_range_cache = { }
|
159
|
+
rate_0 = nil
|
160
|
+
if time_quantitizer = self.time_quantitizer
|
161
|
+
time_quantitizer = ::Currency::Exchange::TimeQuantitizer.current if time_quantitizer == :current
|
162
|
+
end
|
163
|
+
|
164
|
+
h_rates = rates.collect do | r |
|
165
|
+
rr = h_rate_class.new.from_rate(r)
|
166
|
+
rr.dates_to_localtime!
|
167
|
+
|
168
|
+
if rr.date && time_quantitizer
|
169
|
+
date_range = date_range_cache[rr.date] ||= time_quantitizer.quantitize_time_range(rr.date)
|
170
|
+
rr.date_0 = date_range.begin
|
171
|
+
rr.date_1 = date_range.end
|
172
|
+
end
|
173
|
+
|
174
|
+
rate_0 ||= rr if rr.date_0 && rr.date_1
|
175
|
+
|
176
|
+
rr
|
177
|
+
end
|
178
|
+
|
179
|
+
# Fix any dateless Rates.
|
180
|
+
if rate_0
|
181
|
+
h_rates.each do | rr |
|
182
|
+
rr.date_0 = rate_0.date_0 unless rr.date_0
|
183
|
+
rr.date_1 = rate_0.date_1 unless rr.date_1
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Save them all or none.
|
188
|
+
stored_h_rates = [ ]
|
189
|
+
h_rate_class.transaction do
|
190
|
+
h_rates.each do | rr |
|
191
|
+
# Skip identity rates.
|
192
|
+
next if rr.c1 == rr.c2 && ! identity_rates
|
193
|
+
|
194
|
+
# Skip if already exists.
|
195
|
+
existing_rate = rr.find_matching_this(:first)
|
196
|
+
if existing_rate
|
197
|
+
stored_h_rates << existing_rate # Already existed.
|
198
|
+
else
|
199
|
+
begin
|
200
|
+
rr.save!
|
201
|
+
rescue Object => err
|
202
|
+
raise ::Currency::Exception::Generic,
|
203
|
+
[
|
204
|
+
"During save of #{rr.inspect}",
|
205
|
+
:error, err,
|
206
|
+
]
|
207
|
+
end
|
208
|
+
stored_h_rates << rr # Written.
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# Return written Historical::Rates.
|
214
|
+
stored_h_rates
|
215
|
+
end
|
216
|
+
|
217
|
+
end # class
|
218
|
+
|
219
|
+
|
220
|
+
|
@@ -0,0 +1,127 @@
|
|
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.newyorkfed.org/markets/fxrates/FXtoXML.cfm
|
12
|
+
# ?FEXdate=2007%2D02%2D14%2000%3A00%3A00%2E0&FEXtime=1200 and parses XML.
|
13
|
+
#
|
14
|
+
# No rates are available on Saturday and Sunday.
|
15
|
+
#
|
16
|
+
class Currency::Exchange::Rate::Source::NewYorkFed < ::Currency::Exchange::Rate::Source::Provider
|
17
|
+
# Defines the pivot currency for http://xe.com/.
|
18
|
+
PIVOT_CURRENCY = :USD
|
19
|
+
|
20
|
+
def initialize(*opt)
|
21
|
+
self.uri = 'http://www.newyorkfed.org/markets/fxrates/FXtoXML.cfm?FEXdate=#{date_YYYY}%2D#{date_MM}%2D#{date_DD}%2000%3A00%3A00%2E0&FEXTIME=1200'
|
22
|
+
@raw_rates = nil
|
23
|
+
super(*opt)
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
# Returns 'newyorkfed.org'.
|
28
|
+
def name
|
29
|
+
'newyorkfed.org'
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
# New York Fed rates are not available on Saturday and Sunday.
|
34
|
+
def available?(time = nil)
|
35
|
+
time ||= Time.now
|
36
|
+
! [0, 6].include?(time.wday) ? true : false
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
def clear_rates
|
41
|
+
@raw_rates = nil
|
42
|
+
super
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
def raw_rates
|
47
|
+
rates
|
48
|
+
@raw_rates
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
# The fed swaps rates on some currency pairs!
|
53
|
+
# See http://www.newyorkfed.org/markets/fxrates/noon.cfm (LISTS AUD!)
|
54
|
+
# http://www.newyorkfed.org/xml/fx.html (DOES NOT LIST AUD!)
|
55
|
+
@@swap_units = {
|
56
|
+
:AUD => true,
|
57
|
+
:EUR => true,
|
58
|
+
:NZD => true,
|
59
|
+
:GBP => true,
|
60
|
+
}
|
61
|
+
|
62
|
+
|
63
|
+
# Parses XML for rates.
|
64
|
+
def parse_rates(data = nil)
|
65
|
+
data = get_page_content unless data
|
66
|
+
|
67
|
+
rates = [ ]
|
68
|
+
|
69
|
+
@raw_rates = { }
|
70
|
+
|
71
|
+
$stderr.puts "#{self}: parse_rates: data =\n#{data}" if @verbose
|
72
|
+
|
73
|
+
doc = REXML::Document.new(data).root
|
74
|
+
x_series = doc.elements.to_a('//frbny:Series')
|
75
|
+
raise ParserError, "no UNIT attribute" unless x_series
|
76
|
+
x_series.each do | series |
|
77
|
+
c1 = series.attributes['UNIT'] # WHAT TO DO WITH @UNIT_MULT?
|
78
|
+
raise ParserError, "no UNIT attribute" unless c1
|
79
|
+
c1 = c1.upcase.intern
|
80
|
+
|
81
|
+
c2 = series.elements.to_a('frbny:Key/frbny:CURR')[0].text
|
82
|
+
raise ParserError, "no frbny:CURR element" unless c2
|
83
|
+
c2 = c2.upcase.intern
|
84
|
+
|
85
|
+
rate = series.elements.to_a('frbny:Obs/frbny:OBS_VALUE')[0]
|
86
|
+
raise ParserError, 'no frbny:OBS_VALUE' unless rate
|
87
|
+
rate = rate.text.to_f
|
88
|
+
|
89
|
+
date = series.elements.to_a('frbny:Obs/frbny:TIME_PERIOD')[0]
|
90
|
+
raise ParserError, 'no frbny:TIME_PERIOD' unless date
|
91
|
+
date = date.text
|
92
|
+
date = Time.parse("#{date} 12:00:00 -05:00") # USA NY => EST
|
93
|
+
|
94
|
+
# Handle arbitrary rate reciprocals!
|
95
|
+
if @@swap_units[c1] || @@swap_units[c2]
|
96
|
+
c1, c2 = c2, c1
|
97
|
+
end
|
98
|
+
|
99
|
+
rates << new_rate(c1, c2, rate, date)
|
100
|
+
|
101
|
+
(@raw_rates[c1] ||= { })[c2] ||= rate
|
102
|
+
(@raw_rates[c2] ||= { })[c1] ||= 1.0 / rate
|
103
|
+
end
|
104
|
+
|
105
|
+
# $stderr.puts "rates = #{rates.inspect}"
|
106
|
+
raise ::Currency::Exception::UnavailableRates,
|
107
|
+
[
|
108
|
+
"No rates found in #{get_uri.inspect}",
|
109
|
+
:uri, get_uri,
|
110
|
+
] if rates.empty?
|
111
|
+
|
112
|
+
rates
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
# Return a list of known base rates.
|
117
|
+
def load_rates(time = nil)
|
118
|
+
# $stderr.puts "#{self}: load_rates(#{time})" if @verbose
|
119
|
+
self.date = time
|
120
|
+
parse_rates
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
end # class
|
125
|
+
|
126
|
+
|
127
|
+
|
@@ -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
|
+
|