mumboe-currency 0.5

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 (37) hide show
  1. data/bin/currency_historical_rate_load +105 -0
  2. data/examples/ex1.rb +13 -0
  3. data/examples/xe1.rb +20 -0
  4. data/lib/currency/active_record.rb +265 -0
  5. data/lib/currency/config.rb +91 -0
  6. data/lib/currency/core_extensions.rb +41 -0
  7. data/lib/currency/currency/factory.rb +228 -0
  8. data/lib/currency/currency.rb +175 -0
  9. data/lib/currency/currency_version.rb +6 -0
  10. data/lib/currency/exception.rb +119 -0
  11. data/lib/currency/exchange/rate/deriver.rb +157 -0
  12. data/lib/currency/exchange/rate/source/base.rb +166 -0
  13. data/lib/currency/exchange/rate/source/failover.rb +63 -0
  14. data/lib/currency/exchange/rate/source/federal_reserve.rb +160 -0
  15. data/lib/currency/exchange/rate/source/historical/rate.rb +184 -0
  16. data/lib/currency/exchange/rate/source/historical/rate_loader.rb +186 -0
  17. data/lib/currency/exchange/rate/source/historical/writer.rb +220 -0
  18. data/lib/currency/exchange/rate/source/historical.rb +79 -0
  19. data/lib/currency/exchange/rate/source/new_york_fed.rb +127 -0
  20. data/lib/currency/exchange/rate/source/provider.rb +120 -0
  21. data/lib/currency/exchange/rate/source/test.rb +50 -0
  22. data/lib/currency/exchange/rate/source/the_financials.rb +191 -0
  23. data/lib/currency/exchange/rate/source/timed_cache.rb +198 -0
  24. data/lib/currency/exchange/rate/source/xe.rb +165 -0
  25. data/lib/currency/exchange/rate/source.rb +89 -0
  26. data/lib/currency/exchange/rate.rb +214 -0
  27. data/lib/currency/exchange/time_quantitizer.rb +111 -0
  28. data/lib/currency/exchange.rb +50 -0
  29. data/lib/currency/formatter.rb +290 -0
  30. data/lib/currency/macro.rb +321 -0
  31. data/lib/currency/money.rb +295 -0
  32. data/lib/currency/money_helper.rb +13 -0
  33. data/lib/currency/parser.rb +151 -0
  34. data/lib/currency.rb +143 -0
  35. data/test/string_test.rb +54 -0
  36. data/test/test_base.rb +44 -0
  37. metadata +90 -0
@@ -0,0 +1,186 @@
1
+ require 'currency/exchange/rate/source/historical'
2
+ require 'currency/exchange/rate/source/historical/rate'
3
+ require 'currency/exchange/rate/source/historical/writer'
4
+
5
+
6
+ # Currency::Config.current.historical_table_name = 'currency_rates'
7
+ # opts['uri_path'] ||= 'syndicated/cnusa/fxrates.xml'
8
+
9
+ # Loads rates from multiple sources and will store them
10
+ # as historical rates in a database.
11
+
12
+ class ::Currency::Exchange::Rate::Source::Historical::RateLoader
13
+ attr_accessor :options
14
+ attr_accessor :source_options
15
+ attr_accessor :required_currencies
16
+ attr_accessor :rate_sources
17
+ attr_accessor :rate_source_options
18
+ attr_accessor :verbose
19
+ attr_accessor :preferred_summary_source
20
+ attr_accessor :base_currencies
21
+ attr_accessor :summary_rate_src
22
+ attr_reader :writer
23
+
24
+ def initialize(opts = { })
25
+ self.summary_rate_src = 'summary'
26
+ self.source_options = { }
27
+ self.options = opts.dup.freeze
28
+ self.base_currencies = [ :USD ]
29
+ self.required_currencies =
30
+ [
31
+ :USD,
32
+ :GBP,
33
+ :CAD,
34
+ :EUR,
35
+ # :MXP,
36
+ ]
37
+ self.verbose = ! ! ENV['CURRENCY_VERBOSE']
38
+ opts.each do | k, v |
39
+ setter = "#{k}="
40
+ send(setter, v) if respond_to?(setter)
41
+ end
42
+ end
43
+
44
+
45
+ def initialize_writer(writer = Currency::Exchange::Rate::Source::Historical::Writer.new)
46
+ @writer = writer
47
+
48
+ writer.time_quantitizer = :current
49
+ writer.required_currencies = required_currencies
50
+ writer.base_currencies = base_currencies
51
+ writer.preferred_currencies = writer.required_currencies
52
+ writer.reciprocal_rates = true
53
+ writer.all_rates = true
54
+ writer.identity_rates = false
55
+
56
+ options.each do | k, v |
57
+ setter = "#{k}="
58
+ writer.send(setter, v) if writer.respond_to?(setter)
59
+ end
60
+
61
+ writer
62
+ end
63
+
64
+
65
+ def run
66
+ rate_sources.each do | src |
67
+ # Create a historical rate writer.
68
+ initialize_writer
69
+
70
+ # Handle creating a summary rates called 'summary'.
71
+ if src == summary_rate_src
72
+ summary_rates(src)
73
+ else
74
+ require "currency/exchange/rate/source/#{src}"
75
+ src_cls_name = src.gsub(/(^|_)([a-z])/) { | m | $2.upcase }
76
+ src_cls = Currency::Exchange::Rate::Source.const_get(src_cls_name)
77
+ src = src_cls.new(source_options)
78
+
79
+ writer.source = src
80
+
81
+ writer.write_rates
82
+ end
83
+ end
84
+ ensure
85
+ @writer = nil
86
+ end
87
+
88
+
89
+ def summary_rates(src)
90
+ # A list of summary rates.
91
+ summary_rates = [ ]
92
+
93
+ # Get a list of all rate time ranges before today,
94
+ # that do not have a 'cnu' summary rate.
95
+ h_rate_cls = Currency::Exchange::Rate::Source::Historical::Rate
96
+ conn = h_rate_cls.connection
97
+
98
+ # Select only rates from yesterday or before back till 30 days.
99
+ date_1 = Time.now - (0 * 24 * 60 * 60)
100
+ date_0 = date_1 - (30 * 24 * 60 * 60)
101
+
102
+ date_0 = conn.quote(date_0)
103
+ date_1 = conn.quote(date_1)
104
+
105
+ query =
106
+ "SELECT
107
+ DISTINCT a.date_0, a.date_1
108
+ FROM
109
+ #{h_rate_cls.table_name} AS a
110
+ WHERE
111
+ a.source <> '#{src}'
112
+ AND a.date_1 >= #{date_0} AND a.date_1 < #{date_1}
113
+ AND (SELECT COUNT(b.id) FROM #{h_rate_cls.table_name} AS b
114
+ WHERE
115
+ b.c1 = a.c1 AND b.c2 = a.c2
116
+ AND b.date_0 = a.date_0 AND b.date_1 = a.date_1
117
+ AND b.source = '#{src}') = 0
118
+ ORDER BY
119
+ date_0"
120
+ STDERR.puts "query = \n#{query.split("\n").join(' ')}" if verbose
121
+
122
+ dates = conn.query(query)
123
+
124
+ dates.each do | date_range |
125
+ STDERR.puts "\n=============================================\n" if verbose
126
+ STDERR.puts "date_range = #{date_range.inspect}" if verbose
127
+
128
+ # Query for all rates that have the same date range.
129
+ q_rate = h_rate_cls.new(:date_0 => date_range[0], :date_1 => date_range[1])
130
+ available_rates = q_rate.find_matching_this(:all)
131
+
132
+ # Collect all the currency pairs and rates.
133
+ currency_pair = { }
134
+ available_rates.each do | h_rate |
135
+ rate = h_rate.to_rate
136
+ (currency_pair[ [ rate.c1, rate.c2 ] ] ||= [ ]) << [ h_rate, rate ]
137
+ # STDERR.puts "rate = #{rate} #{h_rate.date_0} #{h_rate.date_1}" if verbose
138
+ end
139
+
140
+ currency_pair.each_pair do | currency_pair, rates |
141
+ STDERR.puts "\n =============================================\n" if verbose
142
+ STDERR.puts " currency_pair = #{currency_pair}" if verbose
143
+
144
+ # Create a summary rate for the currency pair.
145
+ selected_rates = [ ]
146
+
147
+ rates.each do | h_rates |
148
+ h_rate, rate = *h_rates
149
+
150
+ # Sanity check!
151
+ next if h_rate.source == src
152
+
153
+ # Found perferred source?
154
+ if h_rate.source == preferred_summary_source
155
+ selected_rates = [ h_rates ]
156
+ break
157
+ end
158
+
159
+ selected_rates << h_rates
160
+ end
161
+
162
+ unless selected_rates.empty?
163
+ summary_rate = Currency::Exchange::Rate::Writable.new(currency_pair[0], currency_pair[1], 0.0)
164
+ selected_rates.each do | h_rates |
165
+ h_rate, rate = *h_rates
166
+ STDERR.puts " rate = #{rate.inspect}" if verbose
167
+ summary_rate.collect_rate(rate)
168
+ end
169
+
170
+ # Save the rate.
171
+ summary_rate.rate = summary_rate.rate_avg
172
+ summary_rate.source = src
173
+ summary_rate.derived = 'summary(' + selected_rates.collect{|r| r[0].id}.sort.join(',') + ')'
174
+ STDERR.puts " summary_rate = #{summary_rate} #{summary_rate.rate_samples}" if verbose
175
+
176
+ summary_rates << summary_rate
177
+ end
178
+ end
179
+ end
180
+
181
+ writer.write_rates(summary_rates)
182
+ end
183
+
184
+ end
185
+
186
+
@@ -0,0 +1,220 @@
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
+
7
+ # Error during handling of historical rates.
8
+ class Error < ::Currency::Exception::Base; end
9
+
10
+ # The source of rates.
11
+ attr_accessor :source
12
+
13
+ # If true, compute all Rates between rates.
14
+ # This can be used to aid complex join reports that may assume
15
+ # c1 as the from currency and c2 as the to currency.
16
+ attr_accessor :all_rates
17
+
18
+ # If true, store identity rates.
19
+ # This can be used to aid complex join reports.
20
+ attr_accessor :identity_rates
21
+
22
+ # If true, compute and store all reciprocal rates.
23
+ attr_accessor :reciprocal_rates
24
+
25
+ # If set, a set of preferred currencies.
26
+ attr_accessor :preferred_currencies
27
+
28
+ # If set, a list of required currencies.
29
+ attr_accessor :required_currencies
30
+
31
+ # If set, a list of required base currencies.
32
+ # base currencies must have rates as c1.
33
+ attr_accessor :base_currencies
34
+
35
+ # If set, use this time quantitizer to
36
+ # manipulate the Rate date_0 date_1 time ranges.
37
+ # If :default, use the TimeQuantitizer.default.
38
+ attr_accessor :time_quantitizer
39
+
40
+
41
+ def initialize(opt = { })
42
+ @all_rates = true
43
+ @identity_rates = false
44
+ @reciprocal_rates = true
45
+ @preferred_currencies = nil
46
+ @required_currencies = nil
47
+ @base_currencies = nil
48
+ @time_quantitizer = nil
49
+ opt.each_pair{| k, v | self.send("#{k}=", v) }
50
+ end
51
+
52
+
53
+ # Returns a list of selected rates from source.
54
+ def selected_rates
55
+ # Produce a list of all currencies.
56
+ currencies = source.currencies
57
+
58
+ # $stderr.puts "currencies = #{currencies.join(', ')}"
59
+
60
+ selected_rates = [ ]
61
+
62
+ # Get list of preferred_currencies.
63
+ if self.preferred_currencies
64
+ self.preferred_currencies = self.preferred_currencies.collect do | c |
65
+ ::Currency::Currency.get(c)
66
+ end
67
+ currencies = currencies.select do | c |
68
+ self.preferred_currencies.include?(c)
69
+ end.uniq
70
+ end
71
+
72
+
73
+ # Check for required currencies.
74
+ if self.required_currencies
75
+ self.required_currencies = self.required_currencies.collect do | c |
76
+ ::Currency::Currency.get(c)
77
+ end
78
+
79
+ self.required_currencies.each do | c |
80
+ unless currencies.include?(c)
81
+ raise ::Currency::Exception::MissingCurrency,
82
+ [
83
+ "Required currency #{c.inspect} not in #{currencies.inspect}",
84
+ :currency, c,
85
+ :required_currency, currencies,
86
+ ]
87
+ end
88
+ end
89
+ end
90
+
91
+
92
+ # $stderr.puts "currencies = #{currencies.inspect}"
93
+
94
+ deriver = ::Currency::Exchange::Rate::Deriver.new(:source => source)
95
+
96
+ # Produce Rates for all pairs of currencies.
97
+ if all_rates
98
+ currencies.each do | 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
+ elsif base_currencies
106
+ base_currencies.each do | c1 |
107
+ c1 = ::Currency::Currency.get(c1)
108
+ currencies.each do | c2 |
109
+ next if c1 == c2 && ! identity_rates
110
+ rate = deriver.rate(c1, c2, nil)
111
+ selected_rates << rate unless selected_rates.include?(rate)
112
+ end
113
+ end
114
+ else
115
+ selected_rates = source.rates.select do | r |
116
+ next if r.c1 == r.c2 && ! identity_rates
117
+ currencies.include?(r.c1) && currencies.include?(r.c2)
118
+ end
119
+ end
120
+
121
+ if identity_rates
122
+ currencies.each do | c1 |
123
+ c1 = ::Currency::Currency.get(c1)
124
+ c2 = c1
125
+ rate = deriver.rate(c1, c2, nil)
126
+ selected_rates << rate unless selected_rates.include?(rate)
127
+ end
128
+ else
129
+ selected_rates = selected_rates.select do | r |
130
+ r.c1 != r.c2
131
+ end
132
+ end
133
+
134
+ if reciprocal_rates
135
+ selected_rates.clone.each do | r |
136
+ c1 = r.c2
137
+ c2 = r.c1
138
+ rate = deriver.rate(c1, c2, nil)
139
+ selected_rates << rate unless selected_rates.include?(rate)
140
+ end
141
+ end
142
+
143
+ # $stderr.puts "selected_rates = #{selected_rates.inspect}\n [#{selected_rates.size}]"
144
+
145
+ selected_rates
146
+ end
147
+
148
+
149
+ # Returns an Array of Historical::Rate objects that were written.
150
+ # Avoids writing Rates that already have been written.
151
+ def write_rates(rates = selected_rates)
152
+
153
+ # Create Historical::Rate objects.
154
+ h_rate_class = ::Currency::Exchange::Rate::Source::Historical::Rate
155
+
156
+ # Most Rates from the same Source will probably have the same time,
157
+ # so cache the computed date_range.
158
+ date_range_cache = { }
159
+ rate_0 = nil
160
+ if time_quantitizer = self.time_quantitizer
161
+ time_quantitizer = ::Currency::Exchange::TimeQuantitizer.current if time_quantitizer == :current
162
+ end
163
+
164
+ h_rates = rates.collect do | r |
165
+ rr = h_rate_class.new.from_rate(r)
166
+ rr.dates_to_localtime!
167
+
168
+ if rr.date && time_quantitizer
169
+ date_range = date_range_cache[rr.date] ||= time_quantitizer.quantitize_time_range(rr.date)
170
+ rr.date_0 = date_range.begin
171
+ rr.date_1 = date_range.end
172
+ end
173
+
174
+ rate_0 ||= rr if rr.date_0 && rr.date_1
175
+
176
+ rr
177
+ end
178
+
179
+ # Fix any dateless Rates.
180
+ if rate_0
181
+ h_rates.each do | rr |
182
+ rr.date_0 = rate_0.date_0 unless rr.date_0
183
+ rr.date_1 = rate_0.date_1 unless rr.date_1
184
+ end
185
+ end
186
+
187
+ # Save them all or none.
188
+ stored_h_rates = [ ]
189
+ h_rate_class.transaction do
190
+ h_rates.each do | rr |
191
+ # Skip identity rates.
192
+ next if rr.c1 == rr.c2 && ! identity_rates
193
+
194
+ # Skip if already exists.
195
+ existing_rate = rr.find_matching_this(:first)
196
+ if existing_rate
197
+ stored_h_rates << existing_rate # Already existed.
198
+ else
199
+ begin
200
+ rr.save!
201
+ rescue Object => err
202
+ raise ::Currency::Exception::Generic,
203
+ [
204
+ "During save of #{rr.inspect}",
205
+ :error, err,
206
+ ]
207
+ end
208
+ stored_h_rates << rr # Written.
209
+ end
210
+ end
211
+ end
212
+
213
+ # Return written Historical::Rates.
214
+ stored_h_rates
215
+ end
216
+
217
+ end # class
218
+
219
+
220
+
@@ -0,0 +1,79 @@
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
+ # Gets historical rates from database using Active::Record.
7
+ # Rates are retrieved using Currency::Exchange::Rate::Source::Historical::Rate as
8
+ # a database record proxy.
9
+ #
10
+ # See Currency::Exchange::Rate::Source::Historical::Writer for a rate archiver.
11
+ #
12
+ class Currency::Exchange::Rate::Source::Historical < Currency::Exchange::Rate::Source::Base
13
+
14
+ # Select specific rate source.
15
+ # Defaults to nil
16
+ attr_accessor :source
17
+
18
+ def initialize
19
+ @source = nil # any
20
+ super
21
+ end
22
+
23
+
24
+ def source_key
25
+ @source ? @source.join(',') : ''
26
+ end
27
+
28
+
29
+ # This Exchange's name is the same as its #uri.
30
+ def name
31
+ "historical #{source_key}"
32
+ end
33
+
34
+
35
+ def initialize(*opt)
36
+ super
37
+ @rates_cache = { }
38
+ @raw_rates_cache = { }
39
+ end
40
+
41
+
42
+ def clear_rates
43
+ @rates_cache.clear
44
+ @raw_rates_cache.clear
45
+ super
46
+ end
47
+
48
+
49
+ # Returns a Rate.
50
+ def get_rate(c1, c2, time)
51
+ # rate =
52
+ get_rates(time).select{ | r | r.c1 == c1 && r.c2 == c2 }[0]
53
+ # $stderr.puts "#{self}.get_rate(#{c1}, #{c2}, #{time.inspect}) => #{rate.inspect}"
54
+ # rate
55
+ end
56
+
57
+
58
+ # Return a list of base Rates.
59
+ def get_rates(time = nil)
60
+ @rates_cache["#{source_key}:#{time}"] ||=
61
+ get_raw_rates(time).collect do | rr |
62
+ rr.to_rate
63
+ end
64
+ end
65
+
66
+
67
+ # Return a list of raw rates.
68
+ def get_raw_rates(time = nil)
69
+ @raw_rates_cache["#{source_key}:#{time}"] ||=
70
+ ::Currency::Exchange::Rate::Source::Historical::Rate.new(:c1 => nil, :c2 => nil, :date => time, :source => source).
71
+ find_matching_this(:all)
72
+ end
73
+
74
+ end # class
75
+
76
+
77
+ require 'currency/exchange/rate/source/historical/rate'
78
+
79
+
@@ -0,0 +1,127 @@
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
+ # No rates are available on Saturday and Sunday.
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
+ # New York Fed rates are not available on Saturday and Sunday.
34
+ def available?(time = nil)
35
+ time ||= Time.now
36
+ ! [0, 6].include?(time.wday) ? true : false
37
+ end
38
+
39
+
40
+ def clear_rates
41
+ @raw_rates = nil
42
+ super
43
+ end
44
+
45
+
46
+ def raw_rates
47
+ rates
48
+ @raw_rates
49
+ end
50
+
51
+
52
+ # The fed swaps rates on some currency pairs!
53
+ # See http://www.newyorkfed.org/markets/fxrates/noon.cfm (LISTS AUD!)
54
+ # http://www.newyorkfed.org/xml/fx.html (DOES NOT LIST AUD!)
55
+ @@swap_units = {
56
+ :AUD => true,
57
+ :EUR => true,
58
+ :NZD => true,
59
+ :GBP => true,
60
+ }
61
+
62
+
63
+ # Parses XML for rates.
64
+ def parse_rates(data = nil)
65
+ data = get_page_content unless data
66
+
67
+ rates = [ ]
68
+
69
+ @raw_rates = { }
70
+
71
+ $stderr.puts "#{self}: parse_rates: data =\n#{data}" if @verbose
72
+
73
+ doc = REXML::Document.new(data).root
74
+ x_series = doc.elements.to_a('//frbny:Series')
75
+ raise ParserError, "no UNIT attribute" unless x_series
76
+ x_series.each do | series |
77
+ c1 = series.attributes['UNIT'] # WHAT TO DO WITH @UNIT_MULT?
78
+ raise ParserError, "no UNIT attribute" unless c1
79
+ c1 = c1.upcase.intern
80
+
81
+ c2 = series.elements.to_a('frbny:Key/frbny:CURR')[0].text
82
+ raise ParserError, "no frbny:CURR element" unless c2
83
+ c2 = c2.upcase.intern
84
+
85
+ rate = series.elements.to_a('frbny:Obs/frbny:OBS_VALUE')[0]
86
+ raise ParserError, 'no frbny:OBS_VALUE' unless rate
87
+ rate = rate.text.to_f
88
+
89
+ date = series.elements.to_a('frbny:Obs/frbny:TIME_PERIOD')[0]
90
+ raise ParserError, 'no frbny:TIME_PERIOD' unless date
91
+ date = date.text
92
+ date = Time.parse("#{date} 12:00:00 -05:00") # USA NY => EST
93
+
94
+ # Handle arbitrary rate reciprocals!
95
+ if @@swap_units[c1] || @@swap_units[c2]
96
+ c1, c2 = c2, c1
97
+ end
98
+
99
+ rates << new_rate(c1, c2, rate, date)
100
+
101
+ (@raw_rates[c1] ||= { })[c2] ||= rate
102
+ (@raw_rates[c2] ||= { })[c1] ||= 1.0 / rate
103
+ end
104
+
105
+ # $stderr.puts "rates = #{rates.inspect}"
106
+ raise ::Currency::Exception::UnavailableRates,
107
+ [
108
+ "No rates found in #{get_uri.inspect}",
109
+ :uri, get_uri,
110
+ ] if rates.empty?
111
+
112
+ rates
113
+ end
114
+
115
+
116
+ # Return a list of known base rates.
117
+ def load_rates(time = nil)
118
+ # $stderr.puts "#{self}: load_rates(#{time})" if @verbose
119
+ self.date = time
120
+ parse_rates
121
+ end
122
+
123
+
124
+ end # class
125
+
126
+
127
+