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.
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
+