currency 0.3.3 → 0.4.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 (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
+