mumboe-currency 0.5

Sign up to get free protection for your applications and to get access to all the features.
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,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'
5
+
6
+ # = Currency::Exchange::Rate::Source::Base
7
+ #
8
+ # The Currency::Exchange::Rate::Source::Base class is the base class for
9
+ # currency exchange rate providers.
10
+ #
11
+ # Currency::Exchange::Rate::Source::Base subclasses are Currency::Exchange::Rate
12
+ # factories.
13
+ #
14
+ # Represents a method of converting between two currencies.
15
+ #
16
+ # See Currency;:Exchange::Rate::source for more details.
17
+ #
18
+ class Currency::Exchange::Rate::Source::Base
19
+
20
+ # The name of this Exchange.
21
+ attr_accessor :name
22
+
23
+ # Currency to use as pivot for deriving rate pairs.
24
+ # Defaults to :USD.
25
+ attr_accessor :pivot_currency
26
+
27
+ # If true, this Exchange will log information.
28
+ attr_accessor :verbose
29
+
30
+ attr_accessor :time_quantitizer
31
+
32
+
33
+ def initialize(opt = { })
34
+ @name = nil
35
+ @verbose = nil unless defined? @verbose
36
+ @pivot_currency ||= :USD
37
+
38
+ @rate = { }
39
+ @currencies = nil
40
+ opt.each_pair{|k,v| self.send("#{k}=", v)}
41
+ end
42
+
43
+
44
+ def __subclass_responsibility(meth)
45
+ raise ::Currency::Exception::SubclassResponsibility,
46
+ [
47
+ "#{self.class}#\#{meth}",
48
+ :class, self.class,
49
+ :method, method,
50
+ ]
51
+ end
52
+
53
+
54
+ # Converts Money m in Currency c1 to a new
55
+ # Money value in Currency c2.
56
+ def convert(m, c2, time = nil, c1 = nil)
57
+ c1 = m.currency if c1 == nil
58
+ time = m.time if time == nil
59
+ time = normalize_time(time)
60
+ if c1 == c2 && normalize_time(m.time) == time
61
+ m
62
+ else
63
+ rate = rate(c1, c2, time)
64
+ # raise ::Currency::Exception::UnknownRate, "#{c1} #{c2} #{time}" unless rate
65
+
66
+ rate && ::Currency::Money(rate.convert(m, c1), c2, time)
67
+ end
68
+ end
69
+
70
+
71
+ # Flush all cached Rate.
72
+ def clear_rates
73
+ @rate.clear
74
+ @currencies = nil
75
+ end
76
+
77
+
78
+ # Flush any cached Rate between Currency c1 and c2.
79
+ def clear_rate(c1, c2, time, recip = true)
80
+ time = time && normalize_time(time)
81
+ @rate["#{c1}:#{c2}:#{time}"] = nil
82
+ @rate["#{c2}:#{c1}:#{time}"] = nil if recip
83
+ time
84
+ end
85
+
86
+
87
+ # Returns the cached Rate between Currency c1 and c2 at a given time.
88
+ #
89
+ # Time is normalized using #normalize_time(time)
90
+ #
91
+ # Subclasses can override this method to implement
92
+ # rate expiration rules.
93
+ #
94
+ def rate(c1, c2, time)
95
+ time = time && normalize_time(time)
96
+ @rate["#{c1}:#{c2}:#{time}"] ||= get_rate(c1, c2, time)
97
+ end
98
+
99
+
100
+ # Gets all rates available by this source.
101
+ #
102
+ def rates(time = nil)
103
+ __subclass_responsibility(:rates)
104
+ end
105
+
106
+
107
+ # Returns a list of Currencies that the rate source provides.
108
+ #
109
+ # Subclasses can override this method.
110
+ def currencies
111
+ @currencies ||= rates.collect{| r | [ r.c1, r.c2 ]}.flatten.uniq
112
+ end
113
+
114
+
115
+ # Determines and creates the Rate between Currency c1 and c2.
116
+ #
117
+ # May attempt to use a pivot currency to bridge between
118
+ # rates.
119
+ #
120
+ def get_rate(c1, c2, time)
121
+ __subclass_responsibility(:get_rate)
122
+ end
123
+
124
+ # Returns a base Rate.
125
+ #
126
+ # Subclasses are required to implement this method.
127
+ def get_rate_base(c1, c2, time)
128
+ __subclass_responsibility(:get_rate_base)
129
+ end
130
+
131
+
132
+ # Returns a list of all available rates.
133
+ #
134
+ # Subclasses must override this method.
135
+ def get_rates(time = nil)
136
+ __subclass_responsibility(:get_rates)
137
+ end
138
+
139
+
140
+ # Called by implementors to construct new Rate objects.
141
+ def new_rate(c1, c2, c1_to_c2_rate, time = nil, derived = nil)
142
+ c1 = ::Currency::Currency.get(c1)
143
+ c2 = ::Currency::Currency.get(c2)
144
+ rate = ::Currency::Exchange::Rate.new(c1, c2, c1_to_c2_rate, name, time, derived)
145
+ # $stderr.puts "new_rate = #{rate}"
146
+ rate
147
+ end
148
+
149
+
150
+ # Normalizes rate time to a quantitized value.
151
+ #
152
+ # Subclasses can override this method.
153
+ def normalize_time(time)
154
+ time && (time_quantitizer || ::Currency::Exchange::TimeQuantitizer.current).quantitize_time(time)
155
+ end
156
+
157
+
158
+ # Returns a simple string rep.
159
+ def to_s
160
+ "#<#{self.class.name} #{self.name && self.name.inspect}>"
161
+ end
162
+ alias :inspect :to_s
163
+
164
+ end # class
165
+
166
+
@@ -0,0 +1,63 @@
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
+ # Gets Rates from primary source, if primary fails, attempts secondary source.
8
+ #
9
+ class Currency::Exchange::Rate::Source::Failover < ::Currency::Exchange::Base
10
+ # Primary rate source.
11
+ attr_accessor :primary
12
+
13
+ # Secondary rate source if primary fails.
14
+ attr_accessor :secondary
15
+
16
+ def name
17
+ "failover(#{primary.name}, #{secondary.name})"
18
+ end
19
+
20
+
21
+ def clear_rates
22
+ @primary.clear_rates
23
+ @secondary.clear_rates
24
+ super
25
+ end
26
+
27
+
28
+ def get_rate(c1, c2, time)
29
+ rate = nil
30
+
31
+ # Try primary.
32
+ err = nil
33
+ begin
34
+ rate = @primary.get_rate(c1, c2, time)
35
+ rescue Object => e
36
+ err = e
37
+ end
38
+
39
+
40
+ if rate == nil || err
41
+ $stderr.puts "Failover: primary failed for get_rate(#{c1}, #{c2}, #{time}) : #{err.inspect}"
42
+ rate = @secondary.get_rate(c1, c2, time)
43
+ end
44
+
45
+
46
+ unless rate
47
+ raise Currency::Exception::UnknownRate,
48
+ [
49
+ "Failover: secondary failed for get_rate(#{c1}, #{c2}, #{time})",
50
+ :c1, c1,
51
+ :c2, c2,
52
+ :time, time,
53
+ ]
54
+ end
55
+
56
+ rate
57
+ end
58
+
59
+
60
+ end # class
61
+
62
+
63
+
@@ -0,0 +1,160 @@
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
+ require 'net/http'
7
+ require 'open-uri'
8
+
9
+
10
+ # Connects to http://www.federalreserve.gov/releases/H10/hist/dat00_<country>.txtb
11
+ # Parses all known currency files.
12
+ #
13
+ class Currency::Exchange::Rate::Source::FederalReserve < ::Currency::Exchange::Rate::Source::Provider
14
+ # Defines the pivot currency for http://www.federalreserve.gov/releases/H10/hist/dat00_#{country_code}.txt data files.
15
+ PIVOT_CURRENCY = :USD
16
+
17
+ # Arbitrary currency code used by www.federalreserve.gov for
18
+ # naming historical data files.
19
+ # Used internally.
20
+ attr_accessor :country_code
21
+
22
+ def initialize(*opt)
23
+ self.uri = 'http://www.federalreserve.gov/releases/H10/hist/dat00_#{country_code}.txt'
24
+ self.country_code = ''
25
+ @raw_rates = nil
26
+ super(*opt)
27
+ end
28
+
29
+
30
+ # Returns 'federalreserve.gov'.
31
+ def name
32
+ 'federalreserve.gov'
33
+ end
34
+
35
+
36
+ # FIXME?
37
+ #def available?(time = nil)
38
+ # time ||= Time.now
39
+ # ! [0, 6].include?(time.wday) ? true : false
40
+ #end
41
+
42
+
43
+ def clear_rates
44
+ @raw_rates = nil
45
+ super
46
+ end
47
+
48
+
49
+ def raw_rates
50
+ rates
51
+ @raw_rates
52
+ end
53
+
54
+ # Maps bizzare federalreserve.gov country codes to ISO currency codes.
55
+ # May only work for the dat00_XX.txt data files.
56
+ # See http://www.jhall.demon.co.uk/currency/by_country.html
57
+ #
58
+ # Some data files list reciprocal rates!
59
+ @@country_to_currency =
60
+ {
61
+ 'al' => [ :AUD, :USD ],
62
+ # 'au' => :ASH, # AUSTRIAN SHILLING: pre-EUR?
63
+ 'bz' => [ :USD, :BRL ],
64
+ 'ca' => [ :USD, :CAD ],
65
+ 'ch' => [ :USD, :CNY ],
66
+ 'dn' => [ :USD, :DKK ],
67
+ 'eu' => [ :EUR, :USD ],
68
+ # 'gr' => :XXX, # Greece Drachma: pre-EUR?
69
+ 'hk' => [ :USD, :HKD ],
70
+ 'in' => [ :USD, :INR ],
71
+ 'ja' => [ :USD, :JPY ],
72
+ 'ma' => [ :USD, :MYR ],
73
+ 'mx' => [ :USD, :MXN ], # OR MXP?
74
+ 'nz' => [ :NZD, :USD ],
75
+ 'no' => [ :USD, :NOK ],
76
+ 'ko' => [ :USD, :KRW ],
77
+ 'sf' => [ :USD, :ZAR ],
78
+ 'sl' => [ :USD, :LKR ],
79
+ 'sd' => [ :USD, :SEK ],
80
+ 'sz' => [ :USD, :CHF ],
81
+ 'ta' => [ :USD, :TWD ], # New Taiwan Dollar.
82
+ 'th' => [ :USD, :THB ],
83
+ 'uk' => [ :GBP, :USD ],
84
+ 've' => [ :USD, :VEB ],
85
+ }
86
+
87
+
88
+ # Parses text file for rates.
89
+ def parse_rates(data = nil)
90
+ data = get_page_content unless data
91
+
92
+ rates = [ ]
93
+
94
+ @raw_rates ||= { }
95
+
96
+ $stderr.puts "#{self}: parse_rates: data =\n#{data}" if @verbose
97
+
98
+ # Rates are USD/currency so
99
+ # c1 = currency
100
+ # c2 = :USD
101
+ c1, c2 = @@country_to_currency[country_code]
102
+
103
+ unless c1 && c2
104
+ raise ::Currency::Exception::UnavailableRates, "Cannot determine currency code for federalreserve.gov country code #{country_code.inspect}"
105
+ end
106
+
107
+ data.split(/\r?\n\r?/).each do | line |
108
+ # day month yy rate
109
+ m = /^\s*(\d\d?)-([A-Z][a-z][a-z])-(\d\d)\s+([\d\.]+)/.match(line)
110
+ next unless m
111
+
112
+ day = m[1].to_i
113
+ month = m[2]
114
+ year = m[3].to_i
115
+ if year >= 50 and year < 100
116
+ year += 1900
117
+ elsif year < 50
118
+ year += 2000
119
+ end
120
+
121
+ date = Time.parse("#{day}-#{month}-#{year} 12:00:00 -05:00") # USA NY => EST
122
+
123
+ rate = m[4].to_f
124
+
125
+ STDERR.puts "#{c1} #{c2} #{rate}\t#{date}" if @verbose
126
+
127
+ rates << new_rate(c1, c2, rate, date)
128
+
129
+ ((@raw_rates[date] ||= { })[c1] ||= { })[c2] ||= rate
130
+ ((@raw_rates[date] ||= { })[c2] ||= { })[c1] ||= 1.0 / rate
131
+ end
132
+
133
+ # Put most recent rate first.
134
+ # See Provider#get_rate.
135
+ rates.reverse!
136
+
137
+ # $stderr.puts "rates = #{rates.inspect}"
138
+ raise ::Currency::Exception::UnavailableRates, "No rates found in #{get_uri.inspect}" if rates.empty?
139
+
140
+ rates
141
+ end
142
+
143
+
144
+ # Return a list of known base rates.
145
+ def load_rates(time = nil)
146
+ # $stderr.puts "#{self}: load_rates(#{time})" if @verbose
147
+ self.date = time
148
+ rates = [ ]
149
+ @@country_to_currency.keys.each do | cc |
150
+ self.country_code = cc
151
+ rates.push(*parse_rates)
152
+ end
153
+ rates
154
+ end
155
+
156
+
157
+ end # class
158
+
159
+
160
+
@@ -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
+