acvwilson-currency 0.5.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 (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,6 @@
1
+ module Currency
2
+ CurrencyVersion = '0.4.11'
3
+ end
4
+ # DO NOT EDIT
5
+ # This file is auto-generated by build scripts.
6
+ # See: rake update_version
@@ -0,0 +1,119 @@
1
+ # Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
2
+ # See LICENSE.txt for details.
3
+
4
+ module Currency::Exception
5
+ # Base class for all Currency::Exception objects.
6
+ #
7
+ # raise Currency::Exception [ "msg", :opt1, 1, :opt2, 2 ]
8
+ #
9
+ class Base < ::Exception
10
+ EMPTY_HASH = { }.freeze
11
+
12
+ def initialize(arg1, *args)
13
+ case arg1
14
+ # [ description, ... ]
15
+ when Array
16
+ @opts = arg1
17
+ arg1 = arg1.shift
18
+ else
19
+ @opts = nil
20
+ end
21
+
22
+ case @opts
23
+ when Array
24
+ if @opts.size == 1 && @opts.first.kind_of?(Hash)
25
+ # [ description, { ... } ]
26
+ @opts = @opts.first
27
+ else
28
+ # [ description, :key, value, ... ]
29
+ @opts = Hash[*@opts]
30
+ end
31
+ end
32
+
33
+ case @opts
34
+ when Hash
35
+ @opts = @opts.dup.freeze
36
+ else
37
+ @opts = { :info => @opts }.freeze
38
+ end
39
+
40
+ @opts ||= EMPTY_HASH
41
+
42
+ super(arg1, *args)
43
+ end
44
+
45
+
46
+ def method_missing(sel, *args, &blk)
47
+ sel = sel.to_sym
48
+ if args.empty? && ! block_given? && @opts.key?(sel)
49
+ return @opts[sel]
50
+ end
51
+ super
52
+ end
53
+
54
+ def to_s
55
+ super + ": #{@opts.inspect}"
56
+ end
57
+
58
+ end
59
+
60
+ # Generic Error.
61
+ class Generic < Base
62
+ end
63
+
64
+ # Error during parsing of Money values from String.
65
+ class InvalidMoneyString < Base
66
+ end
67
+
68
+ # Error during coercion of external Money values.
69
+ class InvalidMoneyValue < Base
70
+ end
71
+
72
+ # Error in Currency code formeat.
73
+ class InvalidCurrencyCode < Base
74
+ end
75
+
76
+ # Error during conversion between currencies.
77
+ class IncompatibleCurrency < Base
78
+ end
79
+
80
+ # Error during locating currencies.
81
+ class MissingCurrency < Base
82
+ end
83
+
84
+ # Error if an Exchange is not defined.
85
+ class UndefinedExchange < Base
86
+ end
87
+
88
+ # Error if a Currency is unknown.
89
+ class UnknownCurrency < Base
90
+ end
91
+
92
+ # Error if an Exchange Rate Source cannot provide an Exchange::Rate.
93
+ class UnknownRate < Base
94
+ end
95
+
96
+ # Error if an Exchange Rate Source.
97
+ class RateSourceError < Base
98
+ end
99
+
100
+ # Error if an Exchange Rate Source cannot supply any rates.
101
+ class UnavailableRates < Base
102
+ end
103
+
104
+ # Error if an Exchange::Rate is not valid.
105
+ class InvalidRate < Base
106
+ end
107
+
108
+ # Error if a subclass is responsible for implementing a method.
109
+ class SubclassResponsibility < Base
110
+ end
111
+
112
+ # Error if some functionality is unimplemented
113
+ class Unimplemented < Base
114
+ end
115
+
116
+ # Error if reentrantancy is d
117
+ class InvalidReentrancy < Base
118
+ end
119
+ end # module
@@ -0,0 +1,48 @@
1
+ # Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
2
+ # See LICENSE.txt for details.
3
+
4
+ # The Currency::Exchange package is responsible for
5
+ # the buying and selling of currencies.
6
+ #
7
+ # This feature is currently unimplemented.
8
+ #
9
+ # Exchange rate sources are configured via Currency::Exchange::Rate::Source.
10
+ #
11
+ module Currency::Exchange
12
+ @@default = nil
13
+ @@current = nil
14
+
15
+ # Returns the default Currency::Exchange object.
16
+ #
17
+ # If one is not specfied an instance of Currency::Exchange::Base is
18
+ # created. Currency::Exchange::Base cannot service any
19
+ # conversion rate requests.
20
+ def self.default
21
+ @@default ||= raise :Currency::Exception::Unimplemented, :default
22
+ end
23
+
24
+ # Sets the default Currency::Exchange object.
25
+ def self.default=(x)
26
+ @@default = x
27
+ end
28
+
29
+ # Returns the current Currency::Exchange object used during
30
+ # explicit and implicit Money trading.
31
+ #
32
+ # If #current= has not been called and #default= has not been called,
33
+ # then UndefinedExchange is raised.
34
+ def self.current
35
+ @@current || self.default || (raise ::Currency::Exception::UndefinedExchange, "Currency::Exchange.current not defined")
36
+ end
37
+
38
+ # Sets the current Currency::Exchange object used during
39
+ # explicit and implicit Money conversions.
40
+ def self.current=(x)
41
+ @@current = x
42
+ end
43
+
44
+ end # module
45
+
46
+ require 'currency/exchange/rate'
47
+ require 'currency/exchange/rate/source'
48
+
@@ -0,0 +1,214 @@
1
+ # Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
2
+ # See LICENSE.txt for details.
3
+
4
+ require 'currency/exchange'
5
+
6
+ # Represents a convertion rate between two currencies at a given period of time.
7
+ class Currency::Exchange::Rate
8
+ # The first Currency.
9
+ attr_reader :c1
10
+
11
+ # The second Currency.
12
+ attr_reader :c2
13
+
14
+ # The rate between c1 and c2.
15
+ #
16
+ # To convert between m1 in c1 and m2 in c2:
17
+ #
18
+ # m2 = m1 * rate.
19
+ attr_reader :rate
20
+
21
+ # The source of the rate.
22
+ attr_reader :source
23
+
24
+ # The Time of the rate.
25
+ attr_reader :date
26
+
27
+ # If the rate is derived from other rates, this describes from where it was derived.
28
+ attr_reader :derived
29
+
30
+ # Average rate over rate samples in a time range.
31
+ attr_reader :rate_avg
32
+
33
+ # Number of rate samples used to calcuate _rate_avg_.
34
+ attr_reader :rate_samples
35
+
36
+ # The rate low between date_0 and date_1.
37
+ attr_reader :rate_lo
38
+
39
+ # The rate high between date_0 and date_1.
40
+ attr_reader :rate_hi
41
+
42
+ # The rate at date_0.
43
+ attr_reader :rate_date_0
44
+
45
+ # The rate at date_1.
46
+ attr_reader :rate_date_1
47
+
48
+ # The lowest date of sampled rates.
49
+ attr_reader :date_0
50
+
51
+ # The highest date of sampled rates.
52
+ # This is non-inclusive during searches to allow seamless tileings of
53
+ # time with rate buckets.
54
+ attr_reader :date_1
55
+
56
+ def initialize(c1, c2, c1_to_c2_rate, source = "UNKNOWN", date = nil, derived = nil, reciprocal = nil, opts = nil)
57
+ @c1 = c1
58
+ @c2 = c2
59
+ @rate = c1_to_c2_rate
60
+ raise ::Currency::Exception::InvalidRate,
61
+ [
62
+ "rate is not positive",
63
+ :rate, @rate,
64
+ :c1, c1,
65
+ :c2, c2,
66
+ ] unless @rate && @rate >= 0.0
67
+ @source = source
68
+ @date = date
69
+ @derived = derived
70
+ @reciprocal = reciprocal
71
+
72
+ #
73
+ @rate_avg =
74
+ @rate_samples =
75
+ @rate_lo =
76
+ @rate_hi =
77
+ @rate_date_0 =
78
+ @rate_date_1 =
79
+ @date_0 =
80
+ @date_1 =
81
+ nil
82
+
83
+ if opts
84
+ opts.each_pair do | k, v |
85
+ self.instance_variable_set("@#{k}", v)
86
+ end
87
+ end
88
+ end
89
+
90
+
91
+ # Returns a cached reciprocal Rate object from c2 to c1.
92
+ def reciprocal
93
+ @reciprocal ||= @rate == 1.0 ? self :
94
+ self.class.new(@c2, @c1,
95
+ 1.0 / @rate,
96
+ @source,
97
+ @date,
98
+ "reciprocal(#{derived || "#{c1.code}#{c2.code}"})", self,
99
+ {
100
+ :rate_avg => @rate_avg && 1.0 / @rate_avg,
101
+ :rate_samples => @rate_samples,
102
+ :rate_lo => @rate_lo && 1.0 / @rate_lo,
103
+ :rate_hi => @rate_hi && 1.0 / @rate_hi,
104
+ :rate_date_0 => @rate_date_0 && 1.0 / @rate_date_0,
105
+ :rate_date_1 => @rate_date_1 && 1.0 / @rate_date_1,
106
+ :date_0 => @date_0,
107
+ :date_1 => @date_1,
108
+ }
109
+ )
110
+ end
111
+
112
+
113
+ # Converts from _m_ in Currency _c1_ to the opposite currency.
114
+ def convert(m, c1)
115
+ m = m.to_f
116
+ if @c1 == c1
117
+ # $stderr.puts "Converting #{@c1} #{m} to #{@c2} #{m * @rate} using #{@rate}"
118
+ m * @rate
119
+ else
120
+ # $stderr.puts "Converting #{@c2} #{m} to #{@c1} #{m / @rate} using #{1.0 / @rate}; recip"
121
+ m / @rate
122
+ end
123
+ end
124
+
125
+
126
+ # Collect rate samples into rate_avg, rate_hi, rate_lo, rate_date_0, rate_date_1, date_0, date_1.
127
+ def collect_rates(rates)
128
+ rates = [ rates ] unless rates.kind_of?(Enumerable)
129
+ rates.each do | r |
130
+ collect_rate(r)
131
+ end
132
+ self
133
+ end
134
+
135
+ # Collect rates samples in to this Rate.
136
+ def collect_rate(rate)
137
+ # Initial.
138
+ @rate_samples ||= 0
139
+ @rate_sum ||= 0
140
+ @source ||= rate.source
141
+ @c1 ||= rate.c1
142
+ @c2 ||= rate.c2
143
+
144
+ # Reciprocal?
145
+ if @c1 == rate.c2 && @c2 == rate.c1
146
+ collect_rate(rate.reciprocal)
147
+ elsif ! (@c1 == rate.c1 && @c2 == rate.c2)
148
+ raise ::Currency::Exception::InvalidRate,
149
+ [
150
+ "Cannot collect rates between different currency pairs",
151
+ :rate1, self,
152
+ :rate2, rate,
153
+ ]
154
+ else
155
+ # Multisource?
156
+ @source = "<<multiple-sources>>" unless @source == rate.source
157
+
158
+ # Calculate rate average.
159
+ @rate_samples += 1
160
+ @rate_sum += rate.rate || (rate.rate_lo + rate.rate_hi) * 0.5
161
+ @rate_avg = @rate_sum / @rate_samples
162
+
163
+ # Calculate rates ranges.
164
+ r = rate.rate_lo || rate.rate
165
+ unless @rate_lo && @rate_lo < r
166
+ @rate_lo = r
167
+ end
168
+ r = rate.rate_hi || rate.rate
169
+ unless @rate_hi && @rate_hi > r
170
+ @rate_hi = r
171
+ end
172
+
173
+ # Calculate rates on date range boundaries
174
+ r = rate.rate_date_0 || rate.rate
175
+ d = rate.date_0 || rate.date
176
+ unless @date_0 && @date_0 < d
177
+ @date_0 = d
178
+ @rate_date_0 = r
179
+ end
180
+
181
+ r = rate.rate_date_1 || rate.rate
182
+ d = rate.date_1 || rate.date
183
+ unless @date_1 && @date_1 > d
184
+ @date_1 = d
185
+ @rate_date_1 = r
186
+ end
187
+
188
+ @date ||= rate.date || rate.date0 || rate.date1
189
+ end
190
+ self
191
+ end
192
+
193
+
194
+ def to_s(extended = false)
195
+ extended = "#{date_0} #{rate_date_0} |< #{rate_lo} #{rate} #{rate_hi} >| #{rate_date_1} #{date_1}" if extended
196
+ extended ||= ''
197
+ "#<#{self.class.name} #{c1.code} #{c2.code} #{rate} #{source.inspect} #{date ? date.strftime('%Y/%m/%d-%H:%M:%S') : 'nil'} #{derived && derived.inspect} #{extended}>"
198
+ end
199
+
200
+ def inspect; to_s; end
201
+
202
+ end # class
203
+
204
+
205
+ class Currency::Exchange::Rate::Writable < Currency::Exchange::Rate
206
+ attr_writer :source
207
+ attr_writer :derived
208
+ attr_writer :rate
209
+ def initialize(c1 = nil, c2 = nil, rate = nil, *opts)
210
+ super(c1, c2, 0.0, *opts)
211
+ @rate = rate
212
+ end
213
+ end # class
214
+
@@ -0,0 +1,157 @@
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
+ require 'currency/exchange/rate/source/base'
6
+
7
+ # The Currency::Exchange::Rate::Deriver class calculates derived rates
8
+ # from base Rates from a rate Source by pivoting against a pivot currency or by
9
+ # generating reciprocals.
10
+ #
11
+ class Currency::Exchange::Rate::Deriver < Currency::Exchange::Rate::Source::Base
12
+
13
+ # The source for base rates.
14
+ attr_accessor :source
15
+
16
+
17
+ def name
18
+ source.name
19
+ end
20
+
21
+
22
+ def initialize(opt = { })
23
+ @source = nil
24
+ @pivot_currency = nil
25
+ @derived_rates = { }
26
+ @all_rates = { }
27
+ super
28
+ end
29
+
30
+
31
+ def pivot_currency
32
+ @pivot_currency || @source.pivot_currency || :USD
33
+ end
34
+
35
+
36
+ # Return all currencies.
37
+ def currencies
38
+ @source.currencies
39
+ end
40
+
41
+
42
+ # Flush all cached Rates.
43
+ def clear_rates
44
+ @derived_rates.clear
45
+ @all_rates.clear
46
+ @source.clear_rates
47
+ super
48
+ end
49
+
50
+
51
+ # Returns all combinations of rates except identity rates.
52
+ def rates(time = nil)
53
+ time = time && normalize_time(time)
54
+ all_rates(time)
55
+ end
56
+
57
+
58
+ # Computes all rates.
59
+ # time is assumed to be normalized.
60
+ def all_rates(time = nil)
61
+ if x = @all_rates["#{time}"]
62
+ return x
63
+ end
64
+
65
+ x = @all_rates["#{time}"] = [ ]
66
+
67
+ currencies = self.currencies
68
+
69
+ currencies.each do | c1 |
70
+ currencies.each do | c2 |
71
+ next if c1 == c2
72
+ c1 = ::Currency::Currency.get(c1)
73
+ c2 = ::Currency::Currency.get(c2)
74
+ rate = rate(c1, c2, time)
75
+ x << rate
76
+ end
77
+ end
78
+
79
+ x
80
+ end
81
+
82
+
83
+ # Determines and creates the Rate between Currency c1 and c2.
84
+ #
85
+ # May attempt to use a pivot currency to bridge between
86
+ # rates.
87
+ #
88
+ def get_rate(c1, c2, time)
89
+ rate = get_rate_reciprocal(c1, c2, time)
90
+
91
+ # Attempt to use pivot_currency to bridge
92
+ # between Rates.
93
+ unless rate
94
+ pc = ::Currency::Currency.get(pivot_currency)
95
+
96
+ if pc &&
97
+ (rate_1 = get_rate_reciprocal(c1, pc, time)) &&
98
+ (rate_2 = get_rate_reciprocal(pc, c2, time))
99
+ c1_to_c2_rate = rate_1.rate * rate_2.rate
100
+ rate = new_rate(c1, c2,
101
+ c1_to_c2_rate,
102
+ rate_1.date || rate_2.date || time,
103
+ "pivot(#{pc.code},#{rate_1.derived || "#{rate_1.c1.code}#{rate_1.c2.code}"},#{rate_2.derived || "#{rate_2.c1}#{rate_2.c2}"})")
104
+ end
105
+ end
106
+
107
+ rate
108
+ end
109
+
110
+
111
+ # Get a matching base rate or its reciprocal.
112
+ def get_rate_reciprocal(c1, c2, time)
113
+ rate = get_rate_base_cached(c1, c2, time)
114
+ unless rate
115
+ if rate = get_rate_base_cached(c2, c1, time)
116
+ rate = (@rate["#{c1}:#{c2}:#{time}"] ||= rate.reciprocal)
117
+ end
118
+ end
119
+
120
+ rate
121
+ end
122
+
123
+
124
+ # Returns a cached base Rate.
125
+ #
126
+ def get_rate_base_cached(c1, c2, time)
127
+ rate = (@rate["#{c1}:#{c2}:#{time}"] ||= get_rate_base(c1, c2, time))
128
+ rate
129
+ end
130
+
131
+
132
+ # Returns a base Rate from the Source.
133
+ def get_rate_base(c1, c2, time)
134
+ if c1 == c2
135
+ # Identity rates are timeless.
136
+ new_rate(c1, c2, 1.0, nil, "identity")
137
+ else
138
+ source.rate(c1, c2, time)
139
+ end
140
+ end
141
+
142
+
143
+ def load_rates(time = nil)
144
+ all_rates(time)
145
+ end
146
+
147
+
148
+ # Returns true if the underlying rate provider is available.
149
+ def available?(time = nil)
150
+ source.available?(time)
151
+ end
152
+
153
+
154
+ end # class
155
+
156
+
157
+