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,203 @@
|
|
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
|
+
# The source of rates.
|
7
|
+
attr_accessor :source
|
8
|
+
|
9
|
+
# If true, compute all Rates between rates.
|
10
|
+
# This can be used to aid complex join reports that may assume
|
11
|
+
# c1 as the from currency and c2 as the to currency.
|
12
|
+
attr_accessor :all_rates
|
13
|
+
|
14
|
+
# If true, store identity rates.
|
15
|
+
# This can be used to aid complex join reports.
|
16
|
+
attr_accessor :identity_rates
|
17
|
+
|
18
|
+
# If true, compute and store all reciprocal rates.
|
19
|
+
attr_accessor :reciprocal_rates
|
20
|
+
|
21
|
+
# If set, a set of preferred currencies.
|
22
|
+
attr_accessor :preferred_currencies
|
23
|
+
|
24
|
+
# If set, a list of required currencies.
|
25
|
+
attr_accessor :required_currencies
|
26
|
+
|
27
|
+
# If set, a list of required base currencies.
|
28
|
+
# base currencies must have rates as c1.
|
29
|
+
attr_accessor :base_currencies
|
30
|
+
|
31
|
+
# If set, use this time quantitizer to
|
32
|
+
# manipulate the Rate date_0 date_1 time ranges.
|
33
|
+
# If :default, use the TimeQuantitizer.default.
|
34
|
+
attr_accessor :time_quantitizer
|
35
|
+
|
36
|
+
|
37
|
+
def initialize(opt = { })
|
38
|
+
@all_rates = true
|
39
|
+
@identity_rates = false
|
40
|
+
@reciprocal_rates = true
|
41
|
+
@preferred_currencies = nil
|
42
|
+
@required_currencies = nil
|
43
|
+
@base_currencies = nil
|
44
|
+
@time_quantitizer = nil
|
45
|
+
opt.each_pair{| k, v | self.send("#{k}=", v) }
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
# Returns a list of selected rates from source.
|
50
|
+
def selected_rates
|
51
|
+
# Produce a list of all currencies.
|
52
|
+
currencies = source.currencies
|
53
|
+
|
54
|
+
# $stderr.puts "currencies = #{currencies.join(', ')}"
|
55
|
+
|
56
|
+
selected_rates = [ ]
|
57
|
+
|
58
|
+
# Get list of preferred_currencies.
|
59
|
+
if self.preferred_currencies
|
60
|
+
self.preferred_currencies = self.preferred_currencies.collect do | c |
|
61
|
+
::Currency::Currency.get(c)
|
62
|
+
end
|
63
|
+
currencies = currencies.select do | c |
|
64
|
+
self.preferred_currencies.include?(c)
|
65
|
+
end.uniq
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
# Check for required currencies.
|
70
|
+
if self.required_currencies
|
71
|
+
self.required_currencies = self.required_currencies.collect do | c |
|
72
|
+
::Currency::Currency.get(c)
|
73
|
+
end
|
74
|
+
|
75
|
+
self.required_currencies.each do | c |
|
76
|
+
unless currencies.include?(c)
|
77
|
+
raise("Required currency #{c.inspect} not in #{currencies.inspect}")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
# $stderr.puts "currencies = #{currencies.inspect}"
|
84
|
+
|
85
|
+
deriver = ::Currency::Exchange::Rate::Deriver.new(:source => source)
|
86
|
+
|
87
|
+
# Produce Rates for all pairs of currencies.
|
88
|
+
if all_rates
|
89
|
+
currencies.each do | c1 |
|
90
|
+
currencies.each do | c2 |
|
91
|
+
next if c1 == c2 && ! identity_rates
|
92
|
+
rate = deriver.rate(c1, c2, nil)
|
93
|
+
selected_rates << rate unless selected_rates.include?(rate)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
elsif base_currencies
|
97
|
+
base_currencies.each do | c1 |
|
98
|
+
c1 = ::Currency::Currency.get(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
|
+
else
|
106
|
+
selected_rates = source.rates.select do | r |
|
107
|
+
next if r.c1 == r.c2 && ! identity_rates
|
108
|
+
currencies.include?(r.c1) && currencies.include?(r.c2)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
if identity_rates
|
113
|
+
currencies.each do | c1 |
|
114
|
+
c1 = ::Currency::Currency.get(c1)
|
115
|
+
c2 = c1
|
116
|
+
rate = deriver.rate(c1, c2, nil)
|
117
|
+
selected_rates << rate unless selected_rates.include?(rate)
|
118
|
+
end
|
119
|
+
else
|
120
|
+
selected_rates = selected_rates.select do | r |
|
121
|
+
r.c1 != r.c2
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
if reciprocal_rates
|
126
|
+
selected_rates.clone.each do | r |
|
127
|
+
c1 = r.c2
|
128
|
+
c2 = r.c1
|
129
|
+
rate = deriver.rate(c1, c2, nil)
|
130
|
+
selected_rates << rate unless selected_rates.include?(rate)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# $stderr.puts "selected_rates = #{selected_rates.inspect}\n [#{selected_rates.size}]"
|
135
|
+
|
136
|
+
selected_rates
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
# Returns an Array of Historical::Rate objects that were written.
|
141
|
+
# Avoids writing Rates that already have been written.
|
142
|
+
def write_rates(rates = selected_rates)
|
143
|
+
|
144
|
+
# Create Historical::Rate objects.
|
145
|
+
h_rate_class = ::Currency::Exchange::Rate::Source::Historical::Rate
|
146
|
+
|
147
|
+
# Most Rates from the same Source will probably have the same time,
|
148
|
+
# so cache the computed date_range.
|
149
|
+
date_range_cache = { }
|
150
|
+
rate_0 = nil
|
151
|
+
if time_quantitizer = self.time_quantitizer
|
152
|
+
time_quantitizer = ::Currency::Exchange::TimeQuantitizer.current if time_quantitizer == :current
|
153
|
+
end
|
154
|
+
|
155
|
+
h_rates = rates.collect do | r |
|
156
|
+
rr = h_rate_class.new.from_rate(r)
|
157
|
+
rr.dates_to_localtime!
|
158
|
+
|
159
|
+
if rr.date && time_quantitizer
|
160
|
+
date_range = date_range_cache[rr.date] ||= time_quantitizer.quantitize_time_range(rr.date)
|
161
|
+
rr.date_0 = date_range.begin
|
162
|
+
rr.date_1 = date_range.end
|
163
|
+
end
|
164
|
+
|
165
|
+
rate_0 ||= rr if rr.date_0 && rr.date_1
|
166
|
+
|
167
|
+
rr
|
168
|
+
end
|
169
|
+
|
170
|
+
# Fix any dateless Rates.
|
171
|
+
if rate_0
|
172
|
+
h_rates.each do | rr |
|
173
|
+
rr.date_0 = rate_0.date_0 unless rr.date_0
|
174
|
+
rr.date_1 = rate_0.date_1 unless rr.date_1
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Save them all or none.
|
179
|
+
stored_h_rates = [ ]
|
180
|
+
h_rate_class.transaction do
|
181
|
+
h_rates.each do | rr |
|
182
|
+
# Skip identity rates.
|
183
|
+
next if rr.c1 == rr.c2 && ! identity_rates
|
184
|
+
|
185
|
+
# Skip if already exists.
|
186
|
+
existing_rate = rr.find_matching_this(:first)
|
187
|
+
if existing_rate
|
188
|
+
stored_h_rates << existing_rate # Already existed.
|
189
|
+
else
|
190
|
+
rr.save!
|
191
|
+
stored_h_rates << rr # Written.
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# Return written Historical::Rates.
|
197
|
+
stored_h_rates
|
198
|
+
end
|
199
|
+
|
200
|
+
end # class
|
201
|
+
|
202
|
+
|
203
|
+
|
@@ -0,0 +1,91 @@
|
|
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
|
+
# This is for demonstration purposes.
|
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
|
+
def clear_rates
|
34
|
+
@raw_rates = nil
|
35
|
+
super
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
def raw_rates
|
40
|
+
rates
|
41
|
+
@raw_rates
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
# Parses XML for rates.
|
46
|
+
def parse_rates(data = nil)
|
47
|
+
data = get_page_content unless data
|
48
|
+
|
49
|
+
rates = [ ]
|
50
|
+
|
51
|
+
@raw_rates = { }
|
52
|
+
|
53
|
+
$stderr.puts "#{self}: parse_rates: data =\n#{data}" if @verbose
|
54
|
+
|
55
|
+
doc = REXML::Document.new(data).root
|
56
|
+
doc.elements.to_a('//frbny:Series').each do | series |
|
57
|
+
c1 = series.attributes['UNIT'] # WHAT TO DO WITH @UNIT_MULT?
|
58
|
+
c1 = c1.upcase.intern
|
59
|
+
|
60
|
+
c2 = series.elements.to_a('frbny:Key/frbny:CURR')[0].text
|
61
|
+
c2 = c2.upcase.intern
|
62
|
+
|
63
|
+
rate = series.elements.to_a('frbny:Obs/frbny:OBS_VALUE')[0].text.to_f
|
64
|
+
|
65
|
+
date = series.elements.to_a('frbny:Obs/frbny:TIME_PERIOD')[0].text
|
66
|
+
date = Time.parse("#{date} 12:00:00 -05:00") # USA NY => EST
|
67
|
+
|
68
|
+
rates << new_rate(c1, c2, rate, date)
|
69
|
+
|
70
|
+
(@raw_rates[c1] ||= { })[c2] ||= rate
|
71
|
+
(@raw_rates[c2] ||= { })[c1] ||= 1.0 / rate
|
72
|
+
end
|
73
|
+
|
74
|
+
# $stderr.puts "rates = #{rates.inspect}"
|
75
|
+
|
76
|
+
rates
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
# Return a list of known base rates.
|
81
|
+
def load_rates(time = nil)
|
82
|
+
# $stderr.puts "#{self}: load_rates(#{time})" if @verbose
|
83
|
+
self.date = time
|
84
|
+
parse_rates
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
end # class
|
89
|
+
|
90
|
+
|
91
|
+
|
@@ -0,0 +1,105 @@
|
|
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
|
+
# The URI used to access the rate source.
|
11
|
+
attr_accessor :uri
|
12
|
+
|
13
|
+
# The Time used to query the rate source.
|
14
|
+
# Typically set by #load_rates.
|
15
|
+
attr_accessor :date
|
16
|
+
|
17
|
+
# The name is the same as its #uri.
|
18
|
+
alias :name :uri
|
19
|
+
|
20
|
+
def initialize(*args)
|
21
|
+
super
|
22
|
+
@rates = { }
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns the date to query for rates.
|
26
|
+
# Defaults to yesterday.
|
27
|
+
def date
|
28
|
+
@date || (Time.now - 24 * 60 * 60) # yesterday.
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
# Returns year of query date.
|
33
|
+
def date_YYYY
|
34
|
+
'%04d' % date.year
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
# Return month of query date.
|
39
|
+
def date_MM
|
40
|
+
'%02d' % date.month
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
# Returns day of query date.
|
45
|
+
def date_DD
|
46
|
+
'%02d' % date.day
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
# Returns the URI string as evaluated with this object.
|
51
|
+
def get_uri
|
52
|
+
uri = self.uri
|
53
|
+
uri = "\"#{uri}\""
|
54
|
+
uri = instance_eval(uri)
|
55
|
+
$stderr.puts "#{self}: uri = #{uri.inspect}" if @verbose
|
56
|
+
uri
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
# Returns the URI content.
|
61
|
+
def get_page_content
|
62
|
+
data = open(get_uri) { |data| data.read }
|
63
|
+
|
64
|
+
data
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
# Clear cached rates from this source.
|
69
|
+
def clear_rates
|
70
|
+
@rates.clear
|
71
|
+
super
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
# Returns current base Rates or calls load_rates to load them from the source.
|
76
|
+
def rates(time = nil)
|
77
|
+
time = time && normalize_time(time)
|
78
|
+
@rates["#{time}"] ||= load_rates(time)
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
# Returns an array of base Rates from the rate source.
|
83
|
+
#
|
84
|
+
# Subclasses must define this method.
|
85
|
+
def load_rates(time = nil)
|
86
|
+
raise('Subclass responsiblity')
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
# Return a matching base rate?
|
91
|
+
def get_rate(c1, c2, time)
|
92
|
+
matching_rates = rates.select do | rate |
|
93
|
+
rate.c1 == c1 &&
|
94
|
+
rate.c2 == c2 &&
|
95
|
+
(! time || normalize_time(rate.date) == time)
|
96
|
+
end
|
97
|
+
matching_rates[0]
|
98
|
+
end
|
99
|
+
|
100
|
+
alias :get_rate_base :get_rate
|
101
|
+
|
102
|
+
end # class
|
103
|
+
|
104
|
+
|
105
|
+
|
@@ -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 :EUR.
|
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
|
+
|