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