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