currency 0.3.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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,84 @@
1
+ # Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
2
+ # See LICENSE.txt for details.
3
+
4
+ require 'currency/exchange/rate'
5
+
6
+ #
7
+ # The Currency::Exchange::Rate::Source package is responsible for
8
+ # providing rates between currencies at a given time.
9
+ #
10
+ # It is not responsible for purchasing or selling actual money.
11
+ # See Currency::Exchange.
12
+ #
13
+ # Currency::Exchange::Rate::Source::Provider subclasses are true rate data
14
+ # providers. See the #load_rates method. They provide groups of rates
15
+ # at a given time.
16
+ #
17
+ # Other Currency::Exchange::Rate::Source::Base subclasses
18
+ # are chained to provide additional rate source behavior,
19
+ # such as caching and derived rates. They provide individual rates between
20
+ # currencies at a given time. See the #rate method. An application
21
+ # will interface directly to a Currency::Exchange::Rate::Source::Base.
22
+ # A rate aggregator like Currency::Exchange::Rate::Historical::Writer will
23
+ # interface directly to a Currency::Exchange::Rate::Source::Provider.
24
+ #
25
+ # == IMPORTANT
26
+ #
27
+ # Rates sources should *never* install themselves
28
+ # as a Currency::Exchange::Rate::Source.current or
29
+ # Currency::Exchange::Rate::Source.default. The application itself is
30
+ # responsible setting up the default rate source.
31
+ # The old auto-installation behavior of rate sources,
32
+ # like Currency::Exchange::Xe, is no longer supported.
33
+ #
34
+ # == Initialization of Rate Sources
35
+ #
36
+ # A typical application will use the following rate source chain:
37
+ #
38
+ # * Currency::Exchange::Rate::Source::TimedCache
39
+ # * Currency::Exchange::Rate::Deriver
40
+ # * a Currency::Exchange::Rate::Source::Provider subclass, like Currency::Exchange::Rate::Source::Xe.
41
+ #
42
+ # Somewhere at initialization of application:
43
+ #
44
+ # provider = Currency::Exchange::Rate::Source::Xe.new
45
+ # deriver = Currency::Exchange::Rate::Deriver.new(:source => provider)
46
+ # cache = Currency::Exchange::Rate::Source::TimedCache.new(:source => deriver)
47
+ # Currency::Exchange::Rate::Source.default = cache
48
+ #
49
+ module Currency::Exchange::Rate::Source
50
+
51
+ @@default = nil
52
+ @@current = nil
53
+
54
+ # Returns the default Currency::Exchange::Rate::Source::Base object.
55
+ #
56
+ # If one is not specfied an instance of Currency::Exchange::Rate::Source::Base is
57
+ # created. Currency::Exchange::Rate::Source::Base cannot service any
58
+ # conversion rate requests.
59
+ def self.default
60
+ @@default ||= Base.new
61
+ end
62
+
63
+ # Sets the default Currency::Exchange object.
64
+ def self.default=(x)
65
+ @@default = x
66
+ end
67
+
68
+ # Returns the current Currency::Exchange object used during
69
+ # explicit and implicit Money conversions.
70
+ #
71
+ # If #current= has not been called and #default= has not been called,
72
+ # then UndefinedExchange is raised.
73
+ def self.current
74
+ @@current || self.default || (raise ::Currency::Exception::UndefinedExchange.new("Currency::Exchange.current not defined"))
75
+ end
76
+
77
+ # Sets the current Currency::Exchange object used during
78
+ # explicit and implicit Money conversions.
79
+ def self.current=(x)
80
+ @@current = x
81
+ end
82
+ end
83
+
84
+ require 'currency/exchange/rate/source/base'
@@ -0,0 +1,156 @@
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
+ # Converts Money m in Currency c1 to a new
45
+ # Money value in Currency c2.
46
+ def convert(m, c2, time = nil, c1 = nil)
47
+ c1 = m.currency if c1 == nil
48
+ time = m.time if time == nil
49
+ time = normalize_time(time)
50
+ if c1 == c2 && normalize_time(m.time) == time
51
+ m
52
+ else
53
+ rate = rate(c1, c2, time)
54
+ # raise ::Currency::Exception::UnknownRate, "#{c1} #{c2} #{time}" unless rate
55
+
56
+ rate && ::Currency::Money(rate.convert(m, c1), c2, time)
57
+ end
58
+ end
59
+
60
+
61
+ # Flush all cached Rate.
62
+ def clear_rates
63
+ @rate.clear
64
+ @currencies = nil
65
+ end
66
+
67
+
68
+ # Flush any cached Rate between Currency c1 and c2.
69
+ def clear_rate(c1, c2, time, recip = true)
70
+ time = time && normalize_time(time)
71
+ @rate["#{c1}:#{c2}:#{time}"] = nil
72
+ @rate["#{c2}:#{c1}:#{time}"] = nil if recip
73
+ time
74
+ end
75
+
76
+
77
+ # Returns the cached Rate between Currency c1 and c2 at a given time.
78
+ #
79
+ # Time is normalized using #normalize_time(time)
80
+ #
81
+ # Subclasses can override this method to implement
82
+ # rate expiration rules.
83
+ #
84
+ def rate(c1, c2, time)
85
+ time = time && normalize_time(time)
86
+ @rate["#{c1}:#{c2}:#{time}"] ||= get_rate(c1, c2, time)
87
+ end
88
+
89
+
90
+ # Gets all rates available by this source.
91
+ #
92
+ def rates(time = nil)
93
+ raise ::Currency::Exception::SubclassResponsibility, "#{self.class}#rate"
94
+ end
95
+
96
+
97
+ # Returns a list of Currencies that the rate source provides.
98
+ #
99
+ # Subclasses can override this method.
100
+ def currencies
101
+ @currencies ||= rates.collect{| r | [ r.c1, r.c2 ]}.flatten.uniq
102
+ end
103
+
104
+
105
+ # Determines and creates the Rate between Currency c1 and c2.
106
+ #
107
+ # May attempt to use a pivot currency to bridge between
108
+ # rates.
109
+ #
110
+ def get_rate(c1, c2, time)
111
+ raise ::Currency::Exception::SubclassResponsibility, "#{self.class}#get_rate"
112
+ end
113
+
114
+ # Returns a base Rate.
115
+ #
116
+ # Subclasses are required to implement this method.
117
+ def get_rate_base(c1, c2, time)
118
+ raise ::Currency::Exception::SubclassResponsibility, "#{self.class}#get_rate_base"
119
+ end
120
+
121
+
122
+ # Returns a list of all available rates.
123
+ #
124
+ # Subclasses must override this method.
125
+ def get_rates(time = nil)
126
+ raise ::Currency::Exception::SubclassResponsibility, "#{self.class}#get_rate"
127
+ end
128
+
129
+
130
+ # Called by implementors to construct new Rate objects.
131
+ def new_rate(c1, c2, c1_to_c2_rate, time = nil, derived = nil)
132
+ c1 = ::Currency::Currency.get(c1)
133
+ c2 = ::Currency::Currency.get(c2)
134
+ rate = ::Currency::Exchange::Rate.new(c1, c2, c1_to_c2_rate, name, time, derived)
135
+ # $stderr.puts "new_rate = #{rate}"
136
+ rate
137
+ end
138
+
139
+
140
+ # Normalizes rate time to a quantitized value.
141
+ #
142
+ # Subclasses can override this method.
143
+ def normalize_time(time)
144
+ time && (time_quantitizer || ::Currency::Exchange::TimeQuantitizer.current).quantitize_time(time)
145
+ end
146
+
147
+
148
+ # Returns a simple string rep.
149
+ def to_s
150
+ "#<#{self.class.name} #{self.name && self.name.inspect}>"
151
+ end
152
+ alias :inspect :to_s
153
+
154
+ end # class
155
+
156
+
@@ -0,0 +1,57 @@
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.put "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("Failover: secondary failed for get_rate(#{c1}, #{c2}, #{time})")
48
+ end
49
+
50
+ rate
51
+ end
52
+
53
+
54
+ end # class
55
+
56
+
57
+
@@ -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 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,181 @@
1
+ require 'active_record/base'
2
+
3
+ require 'currency/exchange/rate/source/historical'
4
+
5
+ # This class represents a historical Rate in a database.
6
+ # It requires ActiveRecord.
7
+ #
8
+ class Currency::Exchange::Rate::Source::Historical::Rate < ::ActiveRecord::Base
9
+ TABLE_NAME = 'currency_historical_rates'
10
+ set_table_name TABLE_NAME
11
+
12
+ # Can create a table and indices for this class
13
+ # when passed a Migration.
14
+ def self.__create_table(m, table_name = TABLE_NAME)
15
+ table_name = table_name.intern
16
+ m.instance_eval do
17
+ create_table table_name do |t|
18
+ t.column :created_on, :datetime, :null => false
19
+ t.column :updated_on, :datetime
20
+
21
+ t.column :c1, :string, :limit => 3, :null => false
22
+ t.column :c2, :string, :limit => 3, :null => false
23
+
24
+ t.column :source, :string, :limit => 32, :null => false
25
+
26
+ t.column :rate, :float, :null => false
27
+
28
+ t.column :rate_avg, :float
29
+ t.column :rate_samples, :integer
30
+ t.column :rate_lo, :float
31
+ t.column :rate_hi, :float
32
+ t.column :rate_date_0, :float
33
+ t.column :rate_date_1, :float
34
+
35
+ t.column :date, :datetime, :null => false
36
+ t.column :date_0, :datetime
37
+ t.column :date_1, :datetime
38
+
39
+ t.column :derived, :string, :limit => 64
40
+ end
41
+
42
+ add_index table_name, :c1
43
+ add_index table_name, :c2
44
+ add_index table_name, :source
45
+ add_index table_name, :date
46
+ add_index table_name, :date_0
47
+ add_index table_name, :date_1
48
+ add_index table_name, [:c1, :c2, :source, :date_0, :date_1], :name => 'c1_c2_src_date_range', :unique => true
49
+ end
50
+ end
51
+
52
+
53
+ # Initializes this object from a Currency::Exchange::Rate object.
54
+ def from_rate(rate)
55
+ self.c1 = rate.c1.code.to_s
56
+ self.c2 = rate.c2.code.to_s
57
+ self.rate = rate.rate
58
+ self.rate_avg = rate.rate_avg
59
+ self.rate_lo = rate.rate_lo
60
+ self.rate_hi = rate.rate_hi
61
+ self.rate_date_0 = rate.rate_date_0
62
+ self.rate_date_1 = rate.rate_date_1
63
+ self.source = rate.source
64
+ self.derived = rate.derived
65
+ self.date = rate.date
66
+ self.date_0 = rate.date_0
67
+ self.date_1 = rate.date_1
68
+ self
69
+ end
70
+
71
+
72
+ # Convert all dates to localtime.
73
+ def dates_to_localtime!
74
+ self.date = self.date && self.date.clone.localtime
75
+ self.date_0 = self.date_0 && self.date_0.clone.localtime
76
+ self.date_1 = self.date_1 && self.date_1.clone.localtime
77
+ end
78
+
79
+
80
+ # Creates a new Currency::Exchange::Rate object.
81
+ def to_rate
82
+ Currency::Exchange::Rate.new(
83
+ ::Currency::Currency.get(self.c1),
84
+ ::Currency::Currency.get(self.c2),
85
+ self.rate,
86
+ "historical #{self.source}",
87
+ self.date,
88
+ self.derived,
89
+ {
90
+ :rate_avg => self.rate_avg,
91
+ :rate_samples => self.rate_samples,
92
+ :rate_lo => self.rate_lo,
93
+ :rate_hi => self.rate_hi,
94
+ :rate_date_0 => self.rate_date_0,
95
+ :rate_date_1 => self.rate_date_1,
96
+ :date_0 => self.date_0,
97
+ :date_1 => self.date_1
98
+ })
99
+ end
100
+
101
+
102
+ # Various defaults.
103
+ def before_validation
104
+ self.rate_avg = self.rate unless self.rate_avg
105
+ self.rate_samples = 1 unless self.rate_samples
106
+ self.rate_lo = self.rate unless self.rate_lo
107
+ self.rate_hi = self.rate unless self.rate_hi
108
+ self.rate_date_0 = self.rate unless self.rate_date_0
109
+ self.rate_date_1 = self.rate unless self.rate_date_1
110
+
111
+ #self.date_0 = self.date unless self.date_0
112
+ #self.date_1 = self.date unless self.date_1
113
+ self.date = self.date_0 + (self.date_1 - self.date_0) * 0.5 if ! self.date && self.date_0 && self.date_1
114
+ self.date = self.date_0 unless self.date
115
+ self.date = self.date_1 unless self.date
116
+ end
117
+
118
+
119
+ # Returns a ActiveRecord::Base#find :conditions value
120
+ # to locate any rates that will match this one.
121
+ #
122
+ # source may be a list of sources.
123
+ # date will match inside date_0 ... date_1 or exactly.
124
+ #
125
+ def find_matching_this_conditions
126
+ sql = [ ]
127
+ values = [ ]
128
+
129
+ if self.c1
130
+ sql << 'c1 = ?'
131
+ values.push(self.c1.to_s)
132
+ end
133
+
134
+ if self.c2
135
+ sql << 'c2 = ?'
136
+ values.push(self.c2.to_s)
137
+ end
138
+
139
+ if self.source
140
+ if self.source.kind_of?(Array)
141
+ sql << 'source IN ?'
142
+ else
143
+ sql << 'source = ?'
144
+ end
145
+ values.push(self.source)
146
+ end
147
+
148
+ if self.date
149
+ sql << '(((date_0 IS NULL) OR (date_0 <= ?)) AND ((date_1 IS NULL) OR (date_1 > ?))) OR date = ?'
150
+ values.push(self.date, self.date, self.date)
151
+ end
152
+
153
+ if self.date_0
154
+ sql << 'date_0 = ?'
155
+ values.push(self.date_0)
156
+ end
157
+
158
+ if self.date_1
159
+ sql << 'date_1 = ?'
160
+ values.push(self.date_1)
161
+ end
162
+
163
+ sql << '1 = 1' if sql.empty?
164
+
165
+ values.unshift(sql.collect{|x| "(#{x})"}.join(' AND '))
166
+
167
+ # $stderr.puts "values = #{values.inspect}"
168
+
169
+ values
170
+ end
171
+
172
+
173
+ # Shorthand.
174
+ def find_matching_this(opt1 = :all, *opts)
175
+ self.class.find(opt1, :conditions => find_matching_this_conditions, *opts)
176
+ end
177
+
178
+ end # class
179
+
180
+
181
+