acvwilson-currency 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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,89 @@
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
+ # require 'currency'
45
+ # require 'currency/exchange/rate/deriver'
46
+ # require 'currency/exchange/rate/source/xe'
47
+ # require 'currency/exchange/rate/source/timed_cache'
48
+ #
49
+ # provider = Currency::Exchange::Rate::Source::Xe.new
50
+ # deriver = Currency::Exchange::Rate::Deriver.new(:source => provider)
51
+ # cache = Currency::Exchange::Rate::Source::TimedCache.new(:source => deriver)
52
+ # Currency::Exchange::Rate::Source.default = cache
53
+ #
54
+ module Currency::Exchange::Rate::Source
55
+
56
+ @@default = nil
57
+ @@current = nil
58
+
59
+ # Returns the default Currency::Exchange::Rate::Source::Base object.
60
+ #
61
+ # If one is not specfied an instance of Currency::Exchange::Rate::Source::Base is
62
+ # created. Currency::Exchange::Rate::Source::Base cannot service any
63
+ # conversion rate requests.
64
+ def self.default
65
+ @@default ||= Base.new
66
+ end
67
+
68
+ # Sets the default Currency::Exchange object.
69
+ def self.default=(x)
70
+ @@default = x
71
+ end
72
+
73
+ # Returns the current Currency::Exchange object used during
74
+ # explicit and implicit Money conversions.
75
+ #
76
+ # If #current= has not been called and #default= has not been called,
77
+ # then UndefinedExchange is raised.
78
+ def self.current
79
+ @@current || self.default || (raise ::Currency::Exception::UndefinedExchange, "Currency::Exchange.current not defined")
80
+ end
81
+
82
+ # Sets the current Currency::Exchange object used during
83
+ # explicit and implicit Money conversions.
84
+ def self.current=(x)
85
+ @@current = x
86
+ end
87
+ end
88
+
89
+ require 'currency/exchange/rate/source/base'
@@ -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
+