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.
Files changed (57) hide show
  1. data/COPYING.txt +339 -0
  2. data/ChangeLog +8 -0
  3. data/LICENSE.txt +65 -0
  4. data/Manifest.txt +58 -0
  5. data/README.txt +51 -0
  6. data/Releases.txt +155 -0
  7. data/TODO.txt +9 -0
  8. data/currency.gemspec +18 -0
  9. data/examples/ex1.rb +13 -0
  10. data/examples/xe1.rb +20 -0
  11. data/lib/currency.rb +143 -0
  12. data/lib/currency/active_record.rb +265 -0
  13. data/lib/currency/config.rb +91 -0
  14. data/lib/currency/core_extensions.rb +83 -0
  15. data/lib/currency/currency.rb +175 -0
  16. data/lib/currency/currency/factory.rb +121 -0
  17. data/lib/currency/currency_version.rb +6 -0
  18. data/lib/currency/exception.rb +119 -0
  19. data/lib/currency/exchange.rb +48 -0
  20. data/lib/currency/exchange/rate.rb +214 -0
  21. data/lib/currency/exchange/rate/deriver.rb +157 -0
  22. data/lib/currency/exchange/rate/source.rb +89 -0
  23. data/lib/currency/exchange/rate/source/base.rb +166 -0
  24. data/lib/currency/exchange/rate/source/failover.rb +63 -0
  25. data/lib/currency/exchange/rate/source/federal_reserve.rb +160 -0
  26. data/lib/currency/exchange/rate/source/historical.rb +79 -0
  27. data/lib/currency/exchange/rate/source/historical/rate.rb +184 -0
  28. data/lib/currency/exchange/rate/source/historical/rate_loader.rb +186 -0
  29. data/lib/currency/exchange/rate/source/historical/writer.rb +220 -0
  30. data/lib/currency/exchange/rate/source/new_york_fed.rb +127 -0
  31. data/lib/currency/exchange/rate/source/provider.rb +120 -0
  32. data/lib/currency/exchange/rate/source/test.rb +50 -0
  33. data/lib/currency/exchange/rate/source/the_financials.rb +191 -0
  34. data/lib/currency/exchange/rate/source/timed_cache.rb +198 -0
  35. data/lib/currency/exchange/rate/source/xe.rb +165 -0
  36. data/lib/currency/exchange/time_quantitizer.rb +111 -0
  37. data/lib/currency/formatter.rb +310 -0
  38. data/lib/currency/macro.rb +321 -0
  39. data/lib/currency/money.rb +298 -0
  40. data/lib/currency/money_helper.rb +13 -0
  41. data/lib/currency/parser.rb +193 -0
  42. data/spec/ar_column_spec.rb +76 -0
  43. data/spec/ar_core_spec.rb +68 -0
  44. data/spec/ar_simple_spec.rb +23 -0
  45. data/spec/config_spec.rb +29 -0
  46. data/spec/federal_reserve_spec.rb +75 -0
  47. data/spec/formatter_spec.rb +72 -0
  48. data/spec/historical_writer_spec.rb +187 -0
  49. data/spec/macro_spec.rb +109 -0
  50. data/spec/money_spec.rb +355 -0
  51. data/spec/new_york_fed_spec.rb +73 -0
  52. data/spec/parser_spec.rb +105 -0
  53. data/spec/spec_helper.rb +25 -0
  54. data/spec/time_quantitizer_spec.rb +115 -0
  55. data/spec/timed_cache_spec.rb +95 -0
  56. data/spec/xe_spec.rb +50 -0
  57. 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
+