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