acvwilson-currency 0.5.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 (57) hide show
  1. data/COPYING.txt +339 -0
  2. data/ChangeLog +8 -0
  3. data/LICENSE.txt +65 -0
  4. data/Manifest.txt +58 -0
  5. data/README.txt +51 -0
  6. data/Releases.txt +155 -0
  7. data/TODO.txt +9 -0
  8. data/currency.gemspec +18 -0
  9. data/examples/ex1.rb +13 -0
  10. data/examples/xe1.rb +20 -0
  11. data/lib/currency.rb +143 -0
  12. data/lib/currency/active_record.rb +265 -0
  13. data/lib/currency/config.rb +91 -0
  14. data/lib/currency/core_extensions.rb +83 -0
  15. data/lib/currency/currency.rb +175 -0
  16. data/lib/currency/currency/factory.rb +121 -0
  17. data/lib/currency/currency_version.rb +6 -0
  18. data/lib/currency/exception.rb +119 -0
  19. data/lib/currency/exchange.rb +48 -0
  20. data/lib/currency/exchange/rate.rb +214 -0
  21. data/lib/currency/exchange/rate/deriver.rb +157 -0
  22. data/lib/currency/exchange/rate/source.rb +89 -0
  23. data/lib/currency/exchange/rate/source/base.rb +166 -0
  24. data/lib/currency/exchange/rate/source/failover.rb +63 -0
  25. data/lib/currency/exchange/rate/source/federal_reserve.rb +160 -0
  26. data/lib/currency/exchange/rate/source/historical.rb +79 -0
  27. data/lib/currency/exchange/rate/source/historical/rate.rb +184 -0
  28. data/lib/currency/exchange/rate/source/historical/rate_loader.rb +186 -0
  29. data/lib/currency/exchange/rate/source/historical/writer.rb +220 -0
  30. data/lib/currency/exchange/rate/source/new_york_fed.rb +127 -0
  31. data/lib/currency/exchange/rate/source/provider.rb +120 -0
  32. data/lib/currency/exchange/rate/source/test.rb +50 -0
  33. data/lib/currency/exchange/rate/source/the_financials.rb +191 -0
  34. data/lib/currency/exchange/rate/source/timed_cache.rb +198 -0
  35. data/lib/currency/exchange/rate/source/xe.rb +165 -0
  36. data/lib/currency/exchange/time_quantitizer.rb +111 -0
  37. data/lib/currency/formatter.rb +310 -0
  38. data/lib/currency/macro.rb +321 -0
  39. data/lib/currency/money.rb +298 -0
  40. data/lib/currency/money_helper.rb +13 -0
  41. data/lib/currency/parser.rb +193 -0
  42. data/spec/ar_column_spec.rb +76 -0
  43. data/spec/ar_core_spec.rb +68 -0
  44. data/spec/ar_simple_spec.rb +23 -0
  45. data/spec/config_spec.rb +29 -0
  46. data/spec/federal_reserve_spec.rb +75 -0
  47. data/spec/formatter_spec.rb +72 -0
  48. data/spec/historical_writer_spec.rb +187 -0
  49. data/spec/macro_spec.rb +109 -0
  50. data/spec/money_spec.rb +355 -0
  51. data/spec/new_york_fed_spec.rb +73 -0
  52. data/spec/parser_spec.rb +105 -0
  53. data/spec/spec_helper.rb +25 -0
  54. data/spec/time_quantitizer_spec.rb +115 -0
  55. data/spec/timed_cache_spec.rb +95 -0
  56. data/spec/xe_spec.rb +50 -0
  57. metadata +117 -0
@@ -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,184 @@
1
+ require 'active_support'
2
+ require 'active_record/base'
3
+
4
+ require 'currency/exchange/rate/source/historical'
5
+
6
+ # This class represents a historical Rate in a database.
7
+ # It requires ActiveRecord.
8
+ #
9
+ class Currency::Exchange::Rate::Source::Historical::Rate < ::ActiveRecord::Base
10
+ @@_table_name ||= Currency::Config.current.historical_table_name
11
+ set_table_name @@_table_name
12
+
13
+ # Can create a table and indices for this class
14
+ # when passed a Migration.
15
+ def self.__create_table(m, table_name = @@_table_name)
16
+ table_name = table_name.intern
17
+ m.instance_eval do
18
+ create_table table_name do |t|
19
+ t.column :created_on, :datetime, :null => false
20
+ t.column :updated_on, :datetime
21
+
22
+ t.column :c1, :string, :limit => 3, :null => false
23
+ t.column :c2, :string, :limit => 3, :null => false
24
+
25
+ t.column :source, :string, :limit => 32, :null => false
26
+
27
+ t.column :rate, :float, :null => false
28
+
29
+ t.column :rate_avg, :float
30
+ t.column :rate_samples, :integer
31
+ t.column :rate_lo, :float
32
+ t.column :rate_hi, :float
33
+ t.column :rate_date_0, :float
34
+ t.column :rate_date_1, :float
35
+
36
+ t.column :date, :datetime, :null => false
37
+ t.column :date_0, :datetime
38
+ t.column :date_1, :datetime
39
+
40
+ t.column :derived, :string, :limit => 64
41
+ end
42
+
43
+ add_index table_name, :c1
44
+ add_index table_name, :c2
45
+ add_index table_name, :source
46
+ add_index table_name, :date
47
+ add_index table_name, :date_0
48
+ add_index table_name, :date_1
49
+ add_index table_name, [:c1, :c2, :source, :date_0, :date_1], :name => 'c1_c2_src_date_range', :unique => true
50
+ end
51
+ end
52
+
53
+
54
+ # Initializes this object from a Currency::Exchange::Rate object.
55
+ def from_rate(rate)
56
+ self.c1 = rate.c1.code.to_s
57
+ self.c2 = rate.c2.code.to_s
58
+ self.rate = rate.rate
59
+ self.rate_avg = rate.rate_avg
60
+ self.rate_samples = rate.rate_samples
61
+ self.rate_lo = rate.rate_lo
62
+ self.rate_hi = rate.rate_hi
63
+ self.rate_date_0 = rate.rate_date_0
64
+ self.rate_date_1 = rate.rate_date_1
65
+ self.source = rate.source
66
+ self.derived = rate.derived
67
+ self.date = rate.date
68
+ self.date_0 = rate.date_0
69
+ self.date_1 = rate.date_1
70
+ self
71
+ end
72
+
73
+
74
+ # Convert all dates to localtime.
75
+ def dates_to_localtime!
76
+ self.date = self.date && self.date.clone.localtime
77
+ self.date_0 = self.date_0 && self.date_0.clone.localtime
78
+ self.date_1 = self.date_1 && self.date_1.clone.localtime
79
+ end
80
+
81
+
82
+ # Creates a new Currency::Exchange::Rate object.
83
+ def to_rate(cls = ::Currency::Exchange::Rate)
84
+ cls.
85
+ new(
86
+ ::Currency::Currency.get(self.c1),
87
+ ::Currency::Currency.get(self.c2),
88
+ self.rate,
89
+ "historical #{self.source}",
90
+ self.date,
91
+ self.derived,
92
+ {
93
+ :rate_avg => self.rate_avg,
94
+ :rate_samples => self.rate_samples,
95
+ :rate_lo => self.rate_lo,
96
+ :rate_hi => self.rate_hi,
97
+ :rate_date_0 => self.rate_date_0,
98
+ :rate_date_1 => self.rate_date_1,
99
+ :date_0 => self.date_0,
100
+ :date_1 => self.date_1
101
+ })
102
+ end
103
+
104
+
105
+ # Various defaults.
106
+ def before_validation
107
+ self.rate_avg = self.rate unless self.rate_avg
108
+ self.rate_samples = 1 unless self.rate_samples
109
+ self.rate_lo = self.rate unless self.rate_lo
110
+ self.rate_hi = self.rate unless self.rate_hi
111
+ self.rate_date_0 = self.rate unless self.rate_date_0
112
+ self.rate_date_1 = self.rate unless self.rate_date_1
113
+
114
+ #self.date_0 = self.date unless self.date_0
115
+ #self.date_1 = self.date unless self.date_1
116
+ self.date = self.date_0 + (self.date_1 - self.date_0) * 0.5 if ! self.date && self.date_0 && self.date_1
117
+ self.date = self.date_0 unless self.date
118
+ self.date = self.date_1 unless self.date
119
+ end
120
+
121
+
122
+ # Returns a ActiveRecord::Base#find :conditions value
123
+ # to locate any rates that will match this one.
124
+ #
125
+ # source may be a list of sources.
126
+ # date will match inside date_0 ... date_1 or exactly.
127
+ #
128
+ def find_matching_this_conditions
129
+ sql = [ ]
130
+ values = [ ]
131
+
132
+ if self.c1
133
+ sql << 'c1 = ?'
134
+ values.push(self.c1.to_s)
135
+ end
136
+
137
+ if self.c2
138
+ sql << 'c2 = ?'
139
+ values.push(self.c2.to_s)
140
+ end
141
+
142
+ if self.source
143
+ if self.source.kind_of?(Array)
144
+ sql << 'source IN ?'
145
+ else
146
+ sql << 'source = ?'
147
+ end
148
+ values.push(self.source)
149
+ end
150
+
151
+ if self.date
152
+ sql << '(((date_0 IS NULL) OR (date_0 <= ?)) AND ((date_1 IS NULL) OR (date_1 > ?))) OR date = ?'
153
+ values.push(self.date, self.date, self.date)
154
+ end
155
+
156
+ if self.date_0
157
+ sql << 'date_0 = ?'
158
+ values.push(self.date_0)
159
+ end
160
+
161
+ if self.date_1
162
+ sql << 'date_1 = ?'
163
+ values.push(self.date_1)
164
+ end
165
+
166
+ sql << '1 = 1' if sql.empty?
167
+
168
+ values.unshift(sql.collect{|x| "(#{x})"}.join(' AND '))
169
+
170
+ # $stderr.puts "values = #{values.inspect}"
171
+
172
+ values
173
+ end
174
+
175
+
176
+ # Shorthand.
177
+ def find_matching_this(opt1 = :all, *opts)
178
+ self.class.find(opt1, :conditions => find_matching_this_conditions, *opts)
179
+ end
180
+
181
+ end # class
182
+
183
+
184
+
@@ -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
+