acvwilson-acvwilson-currency 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) 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 +48 -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 +50 -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 +300 -0
  38. data/lib/currency/macro.rb +321 -0
  39. data/lib/currency/money.rb +296 -0
  40. data/lib/currency/money_helper.rb +13 -0
  41. data/lib/currency/parser.rb +193 -0
  42. data/spec/ar_base_spec.rb +140 -0
  43. data/spec/ar_column_spec.rb +69 -0
  44. data/spec/ar_core_spec.rb +64 -0
  45. data/spec/ar_simple_spec.rb +31 -0
  46. data/spec/config_spec.rb +29 -0
  47. data/spec/federal_reserve_spec.rb +75 -0
  48. data/spec/formatter_spec.rb +72 -0
  49. data/spec/historical_writer_spec.rb +187 -0
  50. data/spec/macro_spec.rb +109 -0
  51. data/spec/money_spec.rb +347 -0
  52. data/spec/new_york_fed_spec.rb +73 -0
  53. data/spec/parser_spec.rb +105 -0
  54. data/spec/spec_helper.rb +25 -0
  55. data/spec/time_quantitizer_spec.rb +115 -0
  56. data/spec/timed_cache_spec.rb +95 -0
  57. data/spec/xe_spec.rb +50 -0
  58. metadata +117 -0
@@ -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&nbsp;+USD&nbsp;=/
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
+
@@ -0,0 +1,111 @@
1
+ # Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
2
+ # See LICENSE.txt for details.
3
+
4
+ # = Currency::Exchange::TimeQuantitizer
5
+ #
6
+ # The Currency::Exchange::TimeQuantitizer quantitizes time values
7
+ # such that money values and rates at a given time
8
+ # can be turned into a hash key, depending
9
+ # on the rate source's temporal accuracy.
10
+ #
11
+ class Currency::Exchange::TimeQuantitizer
12
+
13
+ def self.current; @current ||= self.new; end
14
+ def self.current=(x); @current = x; end
15
+
16
+ # Time quantitization size.
17
+ # Defaults to 1 day.
18
+ attr_accessor :time_quant_size
19
+
20
+ # Time quantization offset in seconds.
21
+ # This is applied to epoch time before quantization.
22
+ # If nil, uses Time#utc_offset.
23
+ # Defaults to nil.
24
+ attr_accessor :time_quant_offset
25
+
26
+ def initialize(*opt)
27
+ @time_quant_size ||= 60 * 60 * 24
28
+ @time_quant_offset ||= nil
29
+ opt = Hash[*opt]
30
+ opt.each_pair{|k,v| self.send("#{k}=", v)}
31
+ end
32
+
33
+
34
+ # Normalizes time to a quantitized value.
35
+ # For example: a time_quant_size of 60 * 60 * 24 will
36
+ # truncate a rate time to a particular day.
37
+ #
38
+ # Subclasses can override this method.
39
+ def quantitize_time(time)
40
+ # If nil, then nil.
41
+ return time unless time
42
+
43
+ # Get bucket parameters.
44
+ was_utc = time.utc?
45
+ quant_offset = time_quant_offset
46
+ quant_offset ||= time.utc_offset
47
+ # $stderr.puts "quant_offset = #{quant_offset}"
48
+ quant_size = time_quant_size.to_i
49
+
50
+ # Get offset from epoch.
51
+ time = time.tv_sec
52
+
53
+ # Remove offset (timezone)
54
+ time += quant_offset
55
+
56
+ # Truncate to quantitize size.
57
+ time = (time.to_i / quant_size) * quant_size
58
+
59
+ # Add offset (timezone)
60
+ time -= quant_offset
61
+
62
+ # Convert back to Time object.
63
+ time = Time.at(time)
64
+
65
+ # Quant to day?
66
+ # NOTE: is this due to a Ruby bug, or
67
+ # some wierd UTC time-flow issue, like leap-seconds.
68
+ if quant_size == 60 * 60 * 24
69
+ time = time + 60 * 60
70
+ if was_utc
71
+ time = time.getutc
72
+ time = Time.utc(time.year, time.month, time.day, 0, 0, 0, 0)
73
+ else
74
+ time = Time.local(time.year, time.month, time.day, 0, 0, 0, 0)
75
+ end
76
+ end
77
+
78
+ # Convert back to UTC?
79
+ time = time.getutc if was_utc
80
+
81
+ time
82
+ end
83
+
84
+ # Returns a Range of Time such that:
85
+ #
86
+ # range.include?(time)
87
+ # ! range.include?(time + time_quant_size)
88
+ # ! range.include?(time - time_quant_size)
89
+ # range.exclude_end?
90
+ #
91
+ # The range.max is end-exclusive to avoid precision issues:
92
+ #
93
+ # t = Time.now
94
+ # => Thu Feb 15 15:32:34 EST 2007
95
+ # x.quantitize_time_range(t)
96
+ # => Thu Feb 15 00:00:00 EST 2007...Fri Feb 16 00:00:00 EST 2007
97
+ #
98
+ def quantitize_time_range(time)
99
+ time_0 = quantitize_time(time)
100
+ time_1 = time_0 + time_quant_size.to_i
101
+ time_0 ... time_1
102
+ end
103
+
104
+ # Returns a simple string rep.
105
+ def to_s
106
+ "#<#{self.class.name} #{quant_offset} #{quant_size}>"
107
+ end
108
+
109
+ end # class
110
+
111
+