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.
- data/COPYING.txt +339 -0
- data/LICENSE.txt +62 -0
- data/Manifest.txt +37 -14
- data/README.txt +8 -0
- data/Rakefile +42 -8
- data/Releases.txt +26 -0
- data/TODO.txt +1 -0
- data/examples/ex1.rb +3 -3
- data/examples/xe1.rb +3 -2
- data/lib/currency.rb +71 -9
- data/lib/currency/active_record.rb +138 -21
- data/lib/currency/core_extensions.rb +7 -5
- data/lib/currency/currency.rb +94 -177
- data/lib/currency/{currency_factory.rb → currency/factory.rb} +46 -25
- data/lib/currency/currency_version.rb +3 -3
- data/lib/currency/exception.rb +14 -14
- data/lib/currency/exchange.rb +14 -12
- data/lib/currency/exchange/rate.rb +159 -28
- data/lib/currency/exchange/rate/deriver.rb +146 -0
- data/lib/currency/exchange/rate/source.rb +84 -0
- data/lib/currency/exchange/rate/source/base.rb +156 -0
- data/lib/currency/exchange/rate/source/failover.rb +57 -0
- data/lib/currency/exchange/rate/source/historical.rb +79 -0
- data/lib/currency/exchange/rate/source/historical/rate.rb +181 -0
- data/lib/currency/exchange/rate/source/historical/writer.rb +203 -0
- data/lib/currency/exchange/rate/source/new_york_fed.rb +91 -0
- data/lib/currency/exchange/rate/source/provider.rb +105 -0
- data/lib/currency/exchange/rate/source/test.rb +50 -0
- data/lib/currency/exchange/rate/source/the_financials.rb +190 -0
- data/lib/currency/exchange/rate/source/timed_cache.rb +144 -0
- data/lib/currency/exchange/rate/source/xe.rb +166 -0
- data/lib/currency/exchange/time_quantitizer.rb +111 -0
- data/lib/currency/formatter.rb +159 -0
- data/lib/currency/macro.rb +321 -0
- data/lib/currency/money.rb +90 -64
- data/lib/currency/money_helper.rb +6 -5
- data/lib/currency/parser.rb +153 -0
- data/test/ar_column_test.rb +6 -3
- data/test/ar_simple_test.rb +5 -2
- data/test/ar_test_base.rb +39 -33
- data/test/ar_test_core.rb +64 -0
- data/test/formatter_test.rb +81 -0
- data/test/historical_writer_test.rb +184 -0
- data/test/macro_test.rb +109 -0
- data/test/money_test.rb +72 -4
- data/test/new_york_fed_test.rb +57 -0
- data/test/parser_test.rb +60 -0
- data/test/test_base.rb +13 -3
- data/test/time_quantitizer_test.rb +136 -0
- data/test/xe_test.rb +29 -5
- metadata +41 -18
- data/lib/currency/exchange/base.rb +0 -84
- data/lib/currency/exchange/test.rb +0 -39
- data/lib/currency/exchange/xe.rb +0 -250
data/lib/currency/exception.rb
CHANGED
@@ -1,14 +1,11 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
end
|
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
|
+
class Base < ::Exception
|
7
|
+
end
|
9
8
|
|
10
|
-
module Currency
|
11
|
-
module Exception
|
12
9
|
# Error during string parsing.
|
13
10
|
class InvalidMoneyString < Base
|
14
11
|
end
|
@@ -33,13 +30,16 @@ module Currency
|
|
33
30
|
class UnknownCurrency < Base
|
34
31
|
end
|
35
32
|
|
36
|
-
# Error if an Exchange cannot provide an Exchange::Rate.
|
33
|
+
# Error if an Exchange Rate Source cannot provide an Exchange::Rate.
|
37
34
|
class UnknownRate < Base
|
38
35
|
end
|
39
36
|
|
40
37
|
# Error if an Exchange::Rate is not valid.
|
41
38
|
class InvalidRate < Base
|
42
39
|
end
|
43
|
-
|
44
|
-
|
45
|
-
|
40
|
+
|
41
|
+
# Error if a subclass is responsible for implementing a method.
|
42
|
+
class SubclassResponsibility < Base
|
43
|
+
end
|
44
|
+
|
45
|
+
end # module
|
data/lib/currency/exchange.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
|
-
#
|
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.
|
2
6
|
#
|
3
|
-
#
|
7
|
+
# This feature is currently unimplemented.
|
4
8
|
#
|
5
|
-
#
|
6
|
-
# the conversion between currencies.
|
9
|
+
# Exchange rate sources are configured via Currency::Exchange::Rate::Source.
|
7
10
|
#
|
8
|
-
|
9
|
-
module Currency
|
10
|
-
module Exchange
|
11
|
+
module Currency::Exchange
|
11
12
|
@@default = nil
|
12
13
|
@@current = nil
|
13
14
|
|
@@ -17,7 +18,7 @@ module Exchange
|
|
17
18
|
# created. Currency::Exchange::Base cannot service any
|
18
19
|
# conversion rate requests.
|
19
20
|
def self.default
|
20
|
-
@@default ||=
|
21
|
+
@@default ||= raise("UNIMPLEMENTED")
|
21
22
|
end
|
22
23
|
|
23
24
|
# Sets the default Currency::Exchange object.
|
@@ -31,7 +32,7 @@ module Exchange
|
|
31
32
|
# If #current= has not been called and #default= has not been called,
|
32
33
|
# then UndefinedExchange is raised.
|
33
34
|
def self.current
|
34
|
-
@@current || self.default || (raise Exception::UndefinedExchange.new("Currency::Exchange.current not defined"))
|
35
|
+
@@current || self.default || (raise ::Currency::Exception::UndefinedExchange.new("Currency::Exchange.current not defined"))
|
35
36
|
end
|
36
37
|
|
37
38
|
# Sets the current Currency::Exchange object used during
|
@@ -39,8 +40,9 @@ module Exchange
|
|
39
40
|
def self.current=(x)
|
40
41
|
@@current = x
|
41
42
|
end
|
42
|
-
end
|
43
|
-
end
|
44
43
|
|
45
|
-
|
44
|
+
end # module
|
45
|
+
|
46
46
|
require 'currency/exchange/rate'
|
47
|
+
require 'currency/exchange/rate/source'
|
48
|
+
|
@@ -1,40 +1,100 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
1
|
+
# Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
|
2
|
+
# See LICENSE.txt for details.
|
3
|
+
|
4
|
+
# Represents a convertion rate between two currencies at a given period of time.
|
5
|
+
class Currency::Exchange::Rate
|
6
|
+
# The first Currency.
|
7
|
+
attr_reader :c1
|
8
|
+
|
9
|
+
# The second Currency.
|
10
|
+
attr_reader :c2
|
11
|
+
|
12
|
+
# The rate between c1 and c2.
|
13
|
+
#
|
14
|
+
# To convert between m1 in c1 and m2 in c2:
|
15
|
+
#
|
16
|
+
# m2 = m1 * rate.
|
17
|
+
attr_reader :rate
|
18
|
+
|
19
|
+
# The source of the rate.
|
20
|
+
attr_reader :source
|
21
|
+
|
22
|
+
# The Time of the rate.
|
23
|
+
attr_reader :date
|
24
|
+
|
25
|
+
# If the rate is derived from other rates, this describes from where it was derived.
|
26
|
+
attr_reader :derived
|
27
|
+
|
28
|
+
# Average rate over rate samples in a time range.
|
29
|
+
attr_reader :rate_avg
|
30
|
+
|
31
|
+
# Number of rate samples used to calcuate _rate_avg_.
|
32
|
+
attr_reader :rate_samples
|
33
|
+
|
34
|
+
# The rate low between date_0 and date_1.
|
35
|
+
attr_reader :rate_lo
|
36
|
+
|
37
|
+
# The rate high between date_0 and date_1.
|
38
|
+
attr_reader :rate_hi
|
39
|
+
|
40
|
+
# The rate at date_0.
|
41
|
+
attr_reader :rate_date_0
|
42
|
+
|
43
|
+
# The rate at date_1.
|
44
|
+
attr_reader :rate_date_1
|
45
|
+
|
46
|
+
# The lowest date of sampled rates.
|
47
|
+
attr_reader :date_0
|
48
|
+
|
49
|
+
# The highest date of sampled rates.
|
50
|
+
# This is non-inclusive during searches to allow seamless tileings of
|
51
|
+
# time with rate buckets.
|
52
|
+
attr_reader :date_1
|
53
|
+
|
54
|
+
def initialize(c1, c2, c1_to_c2_rate, source = "UNKNOWN", date = nil, derived = nil, reciprocal = nil, opts = nil)
|
6
55
|
@c1 = c1
|
7
56
|
@c2 = c2
|
8
57
|
@rate = c1_to_c2_rate
|
9
|
-
raise Exception::InvalidRate.new(@rate) unless @rate
|
58
|
+
raise ::Currency::Exception::InvalidRate.new(@rate) unless @rate && @rate >= 0.0
|
10
59
|
@source = source
|
11
|
-
@date = date
|
12
|
-
@
|
13
|
-
|
14
|
-
|
15
|
-
def c1
|
16
|
-
@c1
|
17
|
-
end
|
60
|
+
@date = date
|
61
|
+
@derived = derived
|
62
|
+
@reciprocal = reciprocal
|
18
63
|
|
19
|
-
|
20
|
-
|
64
|
+
if opts
|
65
|
+
opts.each_pair do | k, v |
|
66
|
+
self.instance_variable_set("@#{k}", v)
|
67
|
+
end
|
68
|
+
end
|
21
69
|
end
|
22
70
|
|
23
|
-
def rate
|
24
|
-
@rate
|
25
|
-
end
|
26
71
|
|
27
|
-
|
28
|
-
|
72
|
+
# Returns a cached reciprocal Rate object from c2 to c1.
|
73
|
+
def reciprocal
|
74
|
+
@reciprocal ||= @rate == 1.0 ? self :
|
75
|
+
self.class.new(@c2, @c1,
|
76
|
+
1.0 / @rate,
|
77
|
+
@source,
|
78
|
+
@date,
|
79
|
+
"reciprocal(#{derived || "#{c1.code}#{c2.code}"})", self,
|
80
|
+
{
|
81
|
+
:rate_avg => @rate_avg && 1.0 / @rate_avg,
|
82
|
+
:rate_samples => @rate_samples,
|
83
|
+
:rate_lo => @rate_lo && 1.0 / @rate_lo,
|
84
|
+
:rate_hi => @rate_hi && 1.0 / @rate_hi,
|
85
|
+
:rate_date_0 => @rate_date_0 && 1.0 / @rate_date_0,
|
86
|
+
:rate_date_1 => @rate_date_1 && 1.0 / @rate_date_1,
|
87
|
+
:date_0 => @date_0,
|
88
|
+
:date_1 => @date_1,
|
89
|
+
}
|
90
|
+
)
|
29
91
|
end
|
30
92
|
|
31
|
-
def date
|
32
|
-
@date
|
33
|
-
end
|
34
93
|
|
94
|
+
# Converts from _m_ in Currency _c1_ to the opposite currency.
|
35
95
|
def convert(m, c1)
|
36
96
|
m = m.to_f
|
37
|
-
if
|
97
|
+
if @c1 == c1
|
38
98
|
# $stderr.puts "Converting #{@c1} #{m} to #{@c2} #{m * @rate} using #{@rate}"
|
39
99
|
m * @rate
|
40
100
|
else
|
@@ -42,10 +102,81 @@ module Exchange
|
|
42
102
|
m / @rate
|
43
103
|
end
|
44
104
|
end
|
45
|
-
end
|
46
105
|
|
47
|
-
|
48
|
-
|
49
|
-
|
106
|
+
|
107
|
+
# Collect rate samples into rate_avg, rate_hi, rate_lo, rate_0, rate_1, date_0, date_1.
|
108
|
+
def collect_rates(rates)
|
109
|
+
rates = [ rates ] unless rates.kind_of?(Enumerable)
|
110
|
+
rates.each do | r |
|
111
|
+
collect_rate(r)
|
112
|
+
end
|
113
|
+
self
|
114
|
+
end
|
115
|
+
|
116
|
+
# Collect rates samples in to this Rate.
|
117
|
+
def collect_rate(rate)
|
118
|
+
# Initial.
|
119
|
+
@rate_samples ||= 0
|
120
|
+
@rate_sum ||= 0
|
121
|
+
@src ||= rate
|
122
|
+
@c1 ||= rate.c1
|
123
|
+
@c2 ||= rate.c2
|
124
|
+
@date ||= rate.date
|
125
|
+
@src ||= rate.src
|
126
|
+
|
127
|
+
# Reciprocal?
|
128
|
+
if @c1 == rate.c2 && @c2 == rate.c1
|
129
|
+
collect_rate(rate.reciprocal)
|
130
|
+
elsif ! (@c1 == rate.c1 && @c2 == rate.c2)
|
131
|
+
raise("Cannot collect rates between different currency pairs")
|
132
|
+
else
|
133
|
+
# Multisource?
|
134
|
+
@src = "<<multiple-sources>>" unless @src == rate.source
|
135
|
+
|
136
|
+
# Calculate rate average.
|
137
|
+
@rate_samples += 1
|
138
|
+
@rate_sum += rate.rate || (rate.rate_lo + rate.rate_hi) * 0.5
|
139
|
+
@rate_avg = @rate_sum / @rate_samples
|
140
|
+
|
141
|
+
# Calculate rates ranges.
|
142
|
+
r = rate.rate_lo || rate.rate
|
143
|
+
unless @rate_lo && @rate_lo < r
|
144
|
+
@rate_lo = r
|
145
|
+
end
|
146
|
+
r = rate.rate_hi || rate.rate
|
147
|
+
unless @rate_hi && @rate_hi > r
|
148
|
+
@rate_hi = r
|
149
|
+
end
|
150
|
+
|
151
|
+
# Calculate rates on date range boundaries
|
152
|
+
r = rate.rate_date_0 || rate.rate
|
153
|
+
d = rate.date_0 || rate.date
|
154
|
+
unless @date_0 && @date_0 < d
|
155
|
+
@date_0 = d
|
156
|
+
@rate_0 = r
|
157
|
+
end
|
158
|
+
|
159
|
+
r = rate.rate_date_1 || rate.rate
|
160
|
+
d = rate.date_1 || rate.date
|
161
|
+
unless @date_1 && @date_1 > d
|
162
|
+
@date_1 = d
|
163
|
+
@rate_0 = r
|
164
|
+
end
|
165
|
+
|
166
|
+
@date ||= rate.date || rate.date0 || rate.date1
|
167
|
+
end
|
168
|
+
self
|
169
|
+
end
|
170
|
+
|
171
|
+
|
172
|
+
def to_s(extended = false)
|
173
|
+
extended = "#{date_0} #{rate_0} |< #{rate_lo} #{rate} #{rate_hi} >| #{rate_1} #{date_1}" if extended
|
174
|
+
extended ||= ''
|
175
|
+
"#<#{self.class.name} #{c1.code} #{c2.code} #{rate} #{source.inspect} #{date ? date.strftime('%Y/%m/%d-%H:%M:%S') : 'nil'} #{derived && derived.inspect} #{extended}>"
|
176
|
+
end
|
177
|
+
|
178
|
+
def inspect; to_s; end
|
179
|
+
|
180
|
+
end # class
|
50
181
|
|
51
182
|
|
@@ -0,0 +1,146 @@
|
|
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_rate.clear
|
45
|
+
@all_rates.clear
|
46
|
+
@source.clear
|
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
|
+
end # class
|
144
|
+
|
145
|
+
|
146
|
+
|