currency 0.3.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/COPYING.txt +339 -0
  2. data/LICENSE.txt +62 -0
  3. data/Manifest.txt +37 -14
  4. data/README.txt +8 -0
  5. data/Rakefile +42 -8
  6. data/Releases.txt +26 -0
  7. data/TODO.txt +1 -0
  8. data/examples/ex1.rb +3 -3
  9. data/examples/xe1.rb +3 -2
  10. data/lib/currency.rb +71 -9
  11. data/lib/currency/active_record.rb +138 -21
  12. data/lib/currency/core_extensions.rb +7 -5
  13. data/lib/currency/currency.rb +94 -177
  14. data/lib/currency/{currency_factory.rb → currency/factory.rb} +46 -25
  15. data/lib/currency/currency_version.rb +3 -3
  16. data/lib/currency/exception.rb +14 -14
  17. data/lib/currency/exchange.rb +14 -12
  18. data/lib/currency/exchange/rate.rb +159 -28
  19. data/lib/currency/exchange/rate/deriver.rb +146 -0
  20. data/lib/currency/exchange/rate/source.rb +84 -0
  21. data/lib/currency/exchange/rate/source/base.rb +156 -0
  22. data/lib/currency/exchange/rate/source/failover.rb +57 -0
  23. data/lib/currency/exchange/rate/source/historical.rb +79 -0
  24. data/lib/currency/exchange/rate/source/historical/rate.rb +181 -0
  25. data/lib/currency/exchange/rate/source/historical/writer.rb +203 -0
  26. data/lib/currency/exchange/rate/source/new_york_fed.rb +91 -0
  27. data/lib/currency/exchange/rate/source/provider.rb +105 -0
  28. data/lib/currency/exchange/rate/source/test.rb +50 -0
  29. data/lib/currency/exchange/rate/source/the_financials.rb +190 -0
  30. data/lib/currency/exchange/rate/source/timed_cache.rb +144 -0
  31. data/lib/currency/exchange/rate/source/xe.rb +166 -0
  32. data/lib/currency/exchange/time_quantitizer.rb +111 -0
  33. data/lib/currency/formatter.rb +159 -0
  34. data/lib/currency/macro.rb +321 -0
  35. data/lib/currency/money.rb +90 -64
  36. data/lib/currency/money_helper.rb +6 -5
  37. data/lib/currency/parser.rb +153 -0
  38. data/test/ar_column_test.rb +6 -3
  39. data/test/ar_simple_test.rb +5 -2
  40. data/test/ar_test_base.rb +39 -33
  41. data/test/ar_test_core.rb +64 -0
  42. data/test/formatter_test.rb +81 -0
  43. data/test/historical_writer_test.rb +184 -0
  44. data/test/macro_test.rb +109 -0
  45. data/test/money_test.rb +72 -4
  46. data/test/new_york_fed_test.rb +57 -0
  47. data/test/parser_test.rb +60 -0
  48. data/test/test_base.rb +13 -3
  49. data/test/time_quantitizer_test.rb +136 -0
  50. data/test/xe_test.rb +29 -5
  51. metadata +41 -18
  52. data/lib/currency/exchange/base.rb +0 -84
  53. data/lib/currency/exchange/test.rb +0 -39
  54. 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
+