mumboe-currency 0.5
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/bin/currency_historical_rate_load +105 -0
- data/examples/ex1.rb +13 -0
- data/examples/xe1.rb +20 -0
- data/lib/currency/active_record.rb +265 -0
- data/lib/currency/config.rb +91 -0
- data/lib/currency/core_extensions.rb +41 -0
- data/lib/currency/currency/factory.rb +228 -0
- data/lib/currency/currency.rb +175 -0
- data/lib/currency/currency_version.rb +6 -0
- data/lib/currency/exception.rb +119 -0
- data/lib/currency/exchange/rate/deriver.rb +157 -0
- data/lib/currency/exchange/rate/source/base.rb +166 -0
- data/lib/currency/exchange/rate/source/failover.rb +63 -0
- data/lib/currency/exchange/rate/source/federal_reserve.rb +160 -0
- data/lib/currency/exchange/rate/source/historical/rate.rb +184 -0
- data/lib/currency/exchange/rate/source/historical/rate_loader.rb +186 -0
- data/lib/currency/exchange/rate/source/historical/writer.rb +220 -0
- data/lib/currency/exchange/rate/source/historical.rb +79 -0
- data/lib/currency/exchange/rate/source/new_york_fed.rb +127 -0
- data/lib/currency/exchange/rate/source/provider.rb +120 -0
- data/lib/currency/exchange/rate/source/test.rb +50 -0
- data/lib/currency/exchange/rate/source/the_financials.rb +191 -0
- data/lib/currency/exchange/rate/source/timed_cache.rb +198 -0
- data/lib/currency/exchange/rate/source/xe.rb +165 -0
- data/lib/currency/exchange/rate/source.rb +89 -0
- data/lib/currency/exchange/rate.rb +214 -0
- data/lib/currency/exchange/time_quantitizer.rb +111 -0
- data/lib/currency/exchange.rb +50 -0
- data/lib/currency/formatter.rb +290 -0
- data/lib/currency/macro.rb +321 -0
- data/lib/currency/money.rb +295 -0
- data/lib/currency/money_helper.rb +13 -0
- data/lib/currency/parser.rb +151 -0
- data/lib/currency.rb +143 -0
- data/test/string_test.rb +54 -0
- data/test/test_base.rb +44 -0
- metadata +90 -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,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,111 @@
|
|
1
|
+
# Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
|
2
|
+
# See LICENSE.txt for details.
|
3
|
+
|
4
|
+
# = Currency::Exchange::TimeQuantitizer
|
5
|
+
#
|
6
|
+
# The Currency::Exchange::TimeQuantitizer quantitizes time values
|
7
|
+
# such that money values and rates at a given time
|
8
|
+
# can be turned into a hash key, depending
|
9
|
+
# on the rate source's temporal accuracy.
|
10
|
+
#
|
11
|
+
class Currency::Exchange::TimeQuantitizer
|
12
|
+
|
13
|
+
def self.current; @current ||= self.new; end
|
14
|
+
def self.current=(x); @current = x; end
|
15
|
+
|
16
|
+
# Time quantitization size.
|
17
|
+
# Defaults to 1 day.
|
18
|
+
attr_accessor :time_quant_size
|
19
|
+
|
20
|
+
# Time quantization offset in seconds.
|
21
|
+
# This is applied to epoch time before quantization.
|
22
|
+
# If nil, uses Time#utc_offset.
|
23
|
+
# Defaults to nil.
|
24
|
+
attr_accessor :time_quant_offset
|
25
|
+
|
26
|
+
def initialize(*opt)
|
27
|
+
@time_quant_size ||= 60 * 60 * 24
|
28
|
+
@time_quant_offset ||= nil
|
29
|
+
opt = Hash[*opt]
|
30
|
+
opt.each_pair{|k,v| self.send("#{k}=", v)}
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
# Normalizes time to a quantitized value.
|
35
|
+
# For example: a time_quant_size of 60 * 60 * 24 will
|
36
|
+
# truncate a rate time to a particular day.
|
37
|
+
#
|
38
|
+
# Subclasses can override this method.
|
39
|
+
def quantitize_time(time)
|
40
|
+
# If nil, then nil.
|
41
|
+
return time unless time
|
42
|
+
|
43
|
+
# Get bucket parameters.
|
44
|
+
was_utc = time.utc?
|
45
|
+
quant_offset = time_quant_offset
|
46
|
+
quant_offset ||= time.utc_offset
|
47
|
+
# $stderr.puts "quant_offset = #{quant_offset}"
|
48
|
+
quant_size = time_quant_size.to_i
|
49
|
+
|
50
|
+
# Get offset from epoch.
|
51
|
+
time = time.tv_sec
|
52
|
+
|
53
|
+
# Remove offset (timezone)
|
54
|
+
time += quant_offset
|
55
|
+
|
56
|
+
# Truncate to quantitize size.
|
57
|
+
time = (time.to_i / quant_size) * quant_size
|
58
|
+
|
59
|
+
# Add offset (timezone)
|
60
|
+
time -= quant_offset
|
61
|
+
|
62
|
+
# Convert back to Time object.
|
63
|
+
time = Time.at(time)
|
64
|
+
|
65
|
+
# Quant to day?
|
66
|
+
# NOTE: is this due to a Ruby bug, or
|
67
|
+
# some wierd UTC time-flow issue, like leap-seconds.
|
68
|
+
if quant_size == 60 * 60 * 24
|
69
|
+
time = time + 60 * 60
|
70
|
+
if was_utc
|
71
|
+
time = time.getutc
|
72
|
+
time = Time.utc(time.year, time.month, time.day, 0, 0, 0, 0)
|
73
|
+
else
|
74
|
+
time = Time.local(time.year, time.month, time.day, 0, 0, 0, 0)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Convert back to UTC?
|
79
|
+
time = time.getutc if was_utc
|
80
|
+
|
81
|
+
time
|
82
|
+
end
|
83
|
+
|
84
|
+
# Returns a Range of Time such that:
|
85
|
+
#
|
86
|
+
# range.include?(time)
|
87
|
+
# ! range.include?(time + time_quant_size)
|
88
|
+
# ! range.include?(time - time_quant_size)
|
89
|
+
# range.exclude_end?
|
90
|
+
#
|
91
|
+
# The range.max is end-exclusive to avoid precision issues:
|
92
|
+
#
|
93
|
+
# t = Time.now
|
94
|
+
# => Thu Feb 15 15:32:34 EST 2007
|
95
|
+
# x.quantitize_time_range(t)
|
96
|
+
# => Thu Feb 15 00:00:00 EST 2007...Fri Feb 16 00:00:00 EST 2007
|
97
|
+
#
|
98
|
+
def quantitize_time_range(time)
|
99
|
+
time_0 = quantitize_time(time)
|
100
|
+
time_1 = time_0 + time_quant_size.to_i
|
101
|
+
time_0 ... time_1
|
102
|
+
end
|
103
|
+
|
104
|
+
# Returns a simple string rep.
|
105
|
+
def to_s
|
106
|
+
"#<#{self.class.name} #{quant_offset} #{quant_size}>"
|
107
|
+
end
|
108
|
+
|
109
|
+
end # class
|
110
|
+
|
111
|
+
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
|
2
|
+
# See LICENSE.txt for details.
|
3
|
+
|
4
|
+
require 'currency'
|
5
|
+
|
6
|
+
# The Currency::Exchange package is responsible for
|
7
|
+
# the buying and selling of currencies.
|
8
|
+
#
|
9
|
+
# This feature is currently unimplemented.
|
10
|
+
#
|
11
|
+
# Exchange rate sources are configured via Currency::Exchange::Rate::Source.
|
12
|
+
#
|
13
|
+
module Currency::Exchange
|
14
|
+
@@default = nil
|
15
|
+
@@current = nil
|
16
|
+
|
17
|
+
# Returns the default Currency::Exchange object.
|
18
|
+
#
|
19
|
+
# If one is not specfied an instance of Currency::Exchange::Base is
|
20
|
+
# created. Currency::Exchange::Base cannot service any
|
21
|
+
# conversion rate requests.
|
22
|
+
def self.default
|
23
|
+
@@default ||= raise :Currency::Exception::Unimplemented, :default
|
24
|
+
end
|
25
|
+
|
26
|
+
# Sets the default Currency::Exchange object.
|
27
|
+
def self.default=(x)
|
28
|
+
@@default = x
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns the current Currency::Exchange object used during
|
32
|
+
# explicit and implicit Money trading.
|
33
|
+
#
|
34
|
+
# If #current= has not been called and #default= has not been called,
|
35
|
+
# then UndefinedExchange is raised.
|
36
|
+
def self.current
|
37
|
+
@@current || self.default || (raise ::Currency::Exception::UndefinedExchange, "Currency::Exchange.current not defined")
|
38
|
+
end
|
39
|
+
|
40
|
+
# Sets the current Currency::Exchange object used during
|
41
|
+
# explicit and implicit Money conversions.
|
42
|
+
def self.current=(x)
|
43
|
+
@@current = x
|
44
|
+
end
|
45
|
+
|
46
|
+
end # module
|
47
|
+
|
48
|
+
require 'currency/exchange/rate'
|
49
|
+
require 'currency/exchange/rate/source'
|
50
|
+
|